学习前提

理解Java异常与try/catch;文件操作在临时或独立目录执行。

本课完成后你能做什么

读写文本并在异常路径也关闭资源。

先理解机制

Path描述文件位置,Files提供常用操作。相对路径相对于进程工作目录,而不一定是源文件目录。本课创建临时文件,避免覆盖已有笔记。

字符必须通过编码转换为字节。明确指定UTF-8,使中文在不同平台保持一致。读取与写入使用相同编码;乱码通常不是修改字符串拼写可以解决的。

try-with-resources会关闭实现AutoCloseable的reader,包括读取抛出异常的路径。外层finally负责删除练习文件,两层职责不同:关闭资源不等于删除文件。

可运行示例

使用JDK 21。将代码保存为Main.java,在独立练习目录执行 javac -encoding UTF-8 Main.java,再执行 java Main。每章独立运行,不把多个public Main类放进同一文件。

import java.io.*;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
public class Main {
    public static void main(String[] args) throws IOException {
        Path file = Files.createTempFile("honeysuck-", ".txt");
        try {
            Files.writeString(file, "Java\n数据库\n", StandardCharsets.UTF_8);
            try (BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
                String line;
                int count = 0;
                while ((line = reader.readLine()) != null) {
                    System.out.println(line);
                    count++;
                }
                System.out.println("lines=" + count);
            }
        } finally { Files.deleteIfExists(file); }
    }
}

推演与验证

输出Java、数据库和lines=2;行终止符不会包含在readLine返回的字符串中。

常见错误与边界

本例main把IOException交给运行时显示,适合学习定位错误。产品程序应在边界给出明确提示;不要将读取失败误报为空文件。

动手练习

加入一个空行并分别统计全部行数与非空行数。

参考解法与理由

每次readLine成功都增加总数;只有!line.isBlank()时增加非空数。Java、空行、数据库应得到总数3和非空2。

深入实验:读取文件时资源何时释放

Files.lines返回惰性流,打开后不一定立即读完整个文件;在流之外继续操作前,应关闭关联资源。这个程序创建临时文件,显式使用UTF-8,通过try-with-resources关闭流,再在finally删除练习文件。

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
public class Main {
    public static void main(String[] args) throws Exception {
        Path file = Files.createTempFile("honeysuck-", ".txt");
        try {
            Files.writeString(file, "HTML\n\nCSS\n", StandardCharsets.UTF_8);
            try (Stream<String> lines = Files.lines(file, StandardCharsets.UTF_8)) {
                System.out.println(lines.filter(line -> !line.isBlank()).count());
            }
        } finally {
            Files.deleteIfExists(file);
        }
    }
}

预期观察

输出2;中间空行不计入。正常结束后临时文件被清理。

原因、取舍与适用边界

不要从已经关闭的try块返回尚未消费的Stream,让调用者再count。readAllLines适合已知较小的文件,优点是简单,但会把全部行放进内存;lines可以逐行处理,仍需关闭,且其中的sorted等操作可能重新积累全部数据。示例main用throws简化演示,实际界面应给读取失败和解码失败合理提示,而不是把缺文件当零行。

进阶练习

需要统计大文件中以#开头的非空注释行,是否要先把所有行转成List?

解法与推理

不需要。在关闭范围内map(String::stripLeading),再filter(line -> line.startsWith("#"))并count。明确前导空格是否算注释的一部分;按需求决定是否stripLeading,而不是无条件改变原始文本再保存。

进一步阅读