412. Java 文件操作基础 - 用装饰者模式定制 BufferedReader 实现结构化文本读取

412. Java 文件操作基础 - 用装饰者模式定制 BufferedReader 实现结构化文本读取

🎯 目标

  • 理解 文本文件的特殊结构(文件头、诗歌编号、正文、结束标识)
  • 学会如何 扩展 BufferedReader 来实现特定需求
  • 熟悉 Decorator 装饰模式在 I/O 中的应用
  • 实战演示如何逐个读取 154 首十四行诗

1️⃣ 文件结构分析

莎士比亚的十四行诗文件(Gutenberg 提供的 pg1041.txt)结构如下:

  1. 前 32 行:版权说明、项目介绍等 → 我们不需要

  2. 从第 33 行开始:进入十四行诗正文部分

  3. 每首诗的格式

    • 一些空行
    • 一个罗马数字(表示第几首诗)
    • 可能的额外空行
    • 诗歌正文(连续多行,没有空行)
    • 空行表示诗歌结束
  4. 文件结尾:以

    java 复制代码
    *** END OF THE PROJECT GUTENBERG EBOOK

    开始的行标记结束

👉 因此,代码要做三件事:

  • 跳过文件头
  • 跳过每首诗的头部(罗马数字部分)
  • 读取诗歌正文,直到空行或文件结束

2️⃣ SonnetReader 类(装饰 BufferedReader)

通过继承 BufferedReader,我们可以在保留其功能的基础上,添加 跳过头部读取诗歌 的方法。

java 复制代码
class SonnetReader extends BufferedReader {

    // 支持从 Reader 构造
    public SonnetReader(Reader reader) {
        super(reader);
    }

    // 支持从 InputStream 构造
    public SonnetReader(InputStream inputStream) {
        this(new InputStreamReader(inputStream));
    }

    // 跳过文件前 N 行(版权声明等)
    public void skipLines(int lines) throws IOException {
        for (int i = 0; i < lines; i++) {
            readLine();
        }
    }

    // 跳过十四行诗的"标题"(罗马数字 + 空行)
    private String skipSonnetHeader() throws IOException {
        String line = readLine();
        while (line != null && line.trim().isEmpty()) {
            line = readLine();
        }
        // 遇到文件结束标记,返回 null
        if (line != null && line.startsWith("*** END OF THE PROJECT GUTENBERG EBOOK")) {
            return null;
        }
        // 跳过诗编号(罗马数字)
        line = readLine();
        while (line != null && line.trim().isEmpty()) {
            line = readLine();
        }
        return line;
    }

    // 读取一首十四行诗
    public Sonnet readNextSonnet() throws IOException {
        String line = skipSonnetHeader();
        if (line == null) {
            return null; // 已到文件末尾
        } else {
            Sonnet sonnet = new Sonnet();
            while (line != null && !line.trim().isEmpty()) {
                sonnet.add(line);
                line = readLine();
            }
            return sonnet;
        }
    }
}

要点讲解

  • 继承 BufferedReader,依然能用在 try-with-resources 中自动关闭。
  • skipLines() 用来跳过文件头。
  • skipSonnetHeader() 用来跳过罗马数字部分,保证返回第一行正文。
  • readNextSonnet() 读取正文,直到遇到空行 → 一首诗结束。

3️⃣ Sonnet 类(封装诗歌)

java 复制代码
class Sonnet {
    private List<String> lines = new ArrayList<>();

    public void add(String line) {
        lines.add(line);
    }

    @Override
    public String toString() {
        return String.join("\n", lines);
    }
}

好处

  • 让代码更清晰:比直接操作 List<String> 更符合语义。
  • 可以扩展:未来可以在 Sonnet 中加上 编号、作者、行数统计 等功能。

4️⃣ 读取并分析十四行诗

java 复制代码
import java.io.*;
import java.nio.file.*;
import java.util.*;

public class AnalyzeSonnets {
    public static void main(String[] args) {
        Path path = Paths.get("files/sonnets.txt");
        List<Sonnet> sonnets = new ArrayList<>();
        int start = 33; // 从第 33 行开始是正文

        try (InputStream inputStream = Files.newInputStream(path);
             SonnetReader reader = new SonnetReader(inputStream)) {

            reader.skipLines(start);
            Sonnet sonnet = reader.readNextSonnet();
            while (sonnet != null) {
                sonnets.add(sonnet);
                sonnet = reader.readNextSonnet();
            }

        } catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println("# sonnets = " + sonnets.size());
        System.out.println("First sonnet:\n" + sonnets.get(0));
    }
}

运行结果:

java 复制代码
# sonnets = 154
First sonnet:
From fairest creatures we desire increase,
That thereby beauty's rose might never die,
...

5️⃣ 可以引导的问题

  • 为什么我们不直接用 BufferedReader,而要自己扩展? 👉 因为需要"跳过头部"和"自定义规则读取",这就是装饰模式的应用场景。
  • 如果文本文件结构改变(比如换成别的作者的诗集),该如何调整? 👉 修改 skipSonnetHeader()readNextSonnet() 的逻辑即可。
  • 为什么要定义 Sonnet 类,而不是直接用 List<String>? 👉 提升代码可读性和扩展性。
相关推荐
w_t_y_y8 小时前
VUE3(一)VUE3语法
前端·javascript·vue.js
逍遥德8 小时前
SpringBoot自带TaskScheduler 接口使用详解:(02)微服务多实例模式下,爆发任务重复执行问题
spring boot·分布式·后端·微服务·中间件
builderwfy8 小时前
VUE子页面调用父页面实现方式
前端·javascript·vue.js
考虑考虑8 小时前
JDK26中的LazyConstant
java·后端·java ee
喵桑丶8 小时前
Skills
前端
之歆8 小时前
DAY_13JavaScript DOM 操作完全指南:实战案例、性能优化与业务价值(下)
开发语言·前端·javascript·性能优化·ecmascript
Darling噜啦啦8 小时前
前端三权分立与AI编程工具实践:从Clock案例看现代前端开发
前端
Gauss松鼠会8 小时前
【GaussDB】基于SpringBoot实现操作GaussDB(DWS)的项目实战
java·数据库·经验分享·spring boot·后端·sql·gaussdb