我大二的第一个「正式」bug:程序读完文件不报错但内容是乱码,查了一小时,发现是文件是 GBK 编的、读的代码按 UTF-8 解。这一章从这类事故出发,把 Java IO 的两套体系(字节流/字符流)讲清,再把本章真正的重头戏讲透:try-with-resources 和缓冲的意义。前者是 Java 对「资源必须关闭」这个千古难题的官方答案,后者是操作系统课第 8 章(页缓存)在应用层的回响。
Java 把一切输入输出抽象成「流」:数据像水管里的水,顺序进出。两套体系按数据单位分:
分类记住一个原则:文本用字符流,二进制用字节流。拿字符流读图片(经过编码转换)必坏,拿字节流读中文(不解释编码)必乱码。
先看这个模式,它值得整段背下来:
try (BufferedReader br = new BufferedReader(new FileReader("a.txt"))) {
String line;
while ((line = br.readLine()) != null) // readLine 返回 null = 读完了
System.out.println(line);
} catch (IOException e) {
e.printStackTrace();
} // ← 走到这里,br 自动关闭(无论正常结束还是异常)
括号里声明的资源(AutoCloseable 的对象),在 try 块结束时保证关闭,异常路径也不例外。这是 Java 7 的语法糖(编译器自动生成 finally + close + 异常抑制),它治的是 C 时代的老病:open 了忘 close(文件句柄泄漏,长期服务耗尽句柄号,操作系统课第 9 章的 fd 表)。C 的 FILE* 要手动 fclose、Python 的 with 是同款思想(本站 Python 课会再讲),Java 的版本还能一次管多个资源(括号里分号隔开)。
对比一下反面教材(Java 6 以前的老写法):try 里 open,finally 里判空再 close,close 自己还可能抛异常要再包一层 try,三行变八行。try-with-resources 之后,「忘关资源」这类 bug 在新代码里几乎绝迹,这是语言设计直接影响代码质量的实例。
裸的 FileInputStream 每次 read 都可能触发一次系统调用(第 1 章 strace 见过的 read),一次进一次出,内核态切换的开销(第 1 章算过,微秒级)在百万次小读取下变成灾难。缓冲流是解药:
new BufferedReader(new FileReader("a.txt")) // 缓冲包在文件流外面
new BufferedInputStream(new FileInputStream("a.bin"))
原理:缓冲流一次向内核要 8KB,攒在内存里,应用每次 read 只从内存取,系统调用次数从「每字节一次」降到「每 8KB 一次」。实测体验:复制一个大文件,包不包缓冲差一个数量级。这层设计还顺带示范了装饰器模式:BufferedInputStream 和 FileInputStream 实现同一个接口(都是 InputStream),缓冲流只是「包了一层、功能增强」,用法完全不变。想再加一层(如数据流 readInt)继续包,套娃即可。IO 类库的几十个类看着吓人,记住「都是同一接口的装饰器堆叠」就理顺了。
try (InputStream in = new FileInputStream("src.jpg");
OutputStream out = new FileOutputStream("dst.jpg")) {
byte[] buf = new byte[8192];
int len;
while ((len = in.read(buf)) != -1) { // read 返回实际读到的字节数,-1 是 EOF
out.write(buf, 0, len); // 只写 len,不写整个 buf!
}
}
两个细节:read 返回 -1 表示流结束;write 必须写 buf, 0, len(最后一次读可能不满 8KB,写整个 buf 会把垃圾也拷进去)。这个模板是字节流操作的母题,音视频处理、网络传输全是它的变体。
字符流在「字节 ↔ 字符」之间做翻译,翻译依据是字符集:ASCII(1 字节,英文)、GBK(中文,12 字节)、UTF-8(变长 14 字节,兼容 ASCII,互联网事实标准)、UTF-16(Java 内存里 String 的存储方式)。
乱码的全部根源:写时编码与读时编码不一致。GBK 写的文件按 UTF-8 读,中文变「锟斤拷」;UTF-8 写的按 GBK 读,变问号或方块。Java 默认编码随平台变(Windows 中文环境曾是 GBK,Linux 几乎都是 UTF-8),所以跨平台代码必须显式指定:
new InputStreamReader(new FileInputStream("a.txt"), StandardCharsets.UTF_8);
new OutputStreamWriter(new FileOutputStream("b.txt"), StandardCharsets.UTF_8);
// Java 18+ 默认 UTF-8(JEP 400),老 JDK 必须手动
顺带把字与字节的账算清:一个 char 是 2 字节(UTF-16 码元),一个汉字在 UTF-8 文件里占 3 字节,「中文长度」的问题(char 数、字节数、显示宽度)从此有了三个不同答案,处理文本时先问自己要哪个。
File 表示路径(不是内容):exists、createNewFile、mkdir、delete、listFiles(目录树遍历用递归,第 6 章的递归练手题)。RandomAccessFile 能 seek 到任意位置读写(数据库课第 8 章的页管理、断点续传的底层)。对象序列化(ObjectOutputStream + implements Serializable)能把整个对象写进文件,但跨语言/跨版本脆弱,真实项目多用 JSON(第三方库),了解即可。