412. Java 文件操作基础 - 用装饰者模式定制 BufferedReader 实现结构化文本读取
🎯 目标
- 理解 文本文件的特殊结构(文件头、诗歌编号、正文、结束标识)
- 学会如何 扩展 BufferedReader 来实现特定需求
- 熟悉 Decorator 装饰模式在 I/O 中的应用
- 实战演示如何逐个读取 154 首十四行诗
1️⃣ 文件结构分析
莎士比亚的十四行诗文件(Gutenberg 提供的 pg1041.txt)结构如下:
-
前 32 行:版权说明、项目介绍等 → 我们不需要
-
从第 33 行开始:进入十四行诗正文部分
-
每首诗的格式:
- 一些空行
- 一个罗马数字(表示第几首诗)
- 可能的额外空行
- 诗歌正文(连续多行,没有空行)
- 空行表示诗歌结束
-
文件结尾:以
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>? 👉 提升代码可读性和扩展性。