Java IO流实战:乱码、泄漏、效率问题一次解决 Java IO流实战宝典中文乱码/资源泄漏/效率低从原理到案例全解决面试必看面试 Java 的时候IO 流几乎每次都会被问到。你说你用过 BufferedReader、FileInputStream 顺手拈来但面试官一追问“UTF-8 和 GBK 互相转换为什么会出现中文乱码”“不关流会发生什么”很多人就卡壳了。我在实际项目里也见过不少线上事故十次有八次都栽在 IO 流这三件事上中文乱码、资源泄漏、效率低。这篇文章我会把这三件事从底层原理一路拆到真实场景再附上可直接照抄的代码和面试答题思路希望能帮准备面试的朋友把这块变成加分项也让日常开发中正被 IO 问题折磨的同学少踩几个坑。1. 先看懂底牌IO流还没写代码就要搞清楚的原理很多同学背八股文能背出“InputStream 是字节流、Reader 是字符流”但代码一写就露馅根本原因在于没搞懂这两类流的设计出发点。这层原理不补上后面聊乱码、聊效率你都会听得云里雾里。1.1 字节流和字符流两条完全不同的数据通道字节流和字符流不是两套等价的东西它们解决的是不同层级的问题。字节流InputStream 和 OutputStream操作的对象是 byte它不管内容是什么只负责把字节从一个地方搬到另一个地方。比如图片、视频、压缩包这些二进制数据一定得用字节流处理。而字符流Reader 和 Writer操作的对象是 char它天生就是为“文本”设计的。字符流底层仍然会用到字节流只是中间加了一层编码解码的转换动作把字节按某个字符集解码成字符或者把字符按某个字符集编码成字节再写出去。你可以这样理解字节流是运货的卡车只管把货物从一个仓库运到另一个仓库字符流是带着翻译的搬运队它不只搬货还会按照约定好的“语言规则”把货物翻译一遍这层翻译规则就是 Charset。如果一个文件是文本你用字节流一次读一个 int得到的是某个数字比如 20013你得自己再把这个数字转成字符才有意义而用字符流读得到的就是直接可用的“中”字因为它内部已经帮你做完了解码。这也是为什么 IO 流体系里会出现过滤流和转换流。InputStreamReader 和 OutputStreamWriter 就是连接字节和字符的桥你给它一个字节流和字符集它就能当字符流来用。我们经常说的“指定编码读文件”说到底就是在这座桥上做文章乱码问题的根源也在这里。1.2 编码和解码的完整链条乱码在哪一步发生要搞清楚中文乱码必须把一次文本读写拆成两条完整的链路来看。写入链路程序里的字符串Java 内部用 Unicode→ 按指定字符集编码成字节 → 写入文件或网络。 读取链路从文件或网络读出字节 → 按指定字符集解码成字符 → 程序内存里的字符串。乱码的核心很简单写入用的字符集和读取用的字符集不一致。比如你用 UTF-8 写入“中”字它变成三个字节 E4 B8 AD结果读取时却按照 GBK 来解码GBK 把这三个字节拆成两个字符再显示自然就变成一堆看不懂的火星文。不同的字符集对同一个字符的编码长度和字节内容完全不同。拿“中”字举例字符集“中”的十六进制字节表示字节长度GBKD6 D02UTF-8E4 B8 AD3UTF-16BE4E 2D4含代理对时可能更多ASCII无法表示0通常变成 ?这里有个非常关键的细节Java 的 String 在内存里是 UTF-16 编码的也就是说一个“中”在内存里就是 4E 2D 这两个字节的 Unicode 码位。一旦你要把它输出到外部存储就必须选择一种输出字符集。选错字符集轻则读回来是乱码重则数据直接被替换成问号不可恢复。所以解决乱码问题的第一原则全链路统一字符集。文件、控制台、数据库连接、HTTP 请求响应能统一成 UTF-8 就统一成 UTF-8不要给任何一个环节留下自由发挥的空间这是我在公司排查乱码问题时的第一板斧。2. 中文乱码三个真实场景带你从根因走到解法道理讲完了接下来全部是实战。我在开发群里见过的问题花样特别多但归纳下来无非是读文件乱码、控制台输出乱码、数据库存取乱码这三类。每类背后都有特定的坑这里一个个拆。2.1 乱码的三种典型症状一眼定位问题层级先学会通过“乱码长什么样”判断问题出在哪一步这能节省大量排查时间。第一种全部变成问号“?”或者写入后又读出来全是“? ”。这种情况最麻烦因为问号往往意味着字符在编码阶段就已经丢失了。比如你用 ASCII 码表去编码“中”ASCII 里根本没有这个字符编码器通常会塞给它一个 0x3F也就是问号。一旦变成问号原来的字节信息就彻底没了再怎么换读取编码都救不回来。第二种出现一个个菱形问号“”。这是 UTF-8 典型的错误解码表现通常是读文件时用了错误的字符集把原本合法的 UTF-8 多字节序列拆坏了字节丢失了一部分图片资料已经损坏除非你有备份不然很难还原。第三种中文变成“涓嬭浇”这种看起来像繁体又像火星文的字符。这种通常是 GBK 和 UTF-8 互相错位导致的。比如文件是 UTF-8 编码你按 GBK 解码读出来或者反过来。这种情况反而不用太慌因为原始字节还在只要你换成正确的字符集重新解码内容就能完好恢复。排查乱码时第一件事不是打开代码翻逻辑而是先用十六进制工具看一眼磁盘上实际的字节内容再对照你程序里使用的字符集基本一眼就能判断是谁的锅。2.2 场景一FileReader 读文件乱码很多人栽在这里这是最常见的翻车现场自己用记事本或 IDEA 保存了一个 UTF-8 编码的 txt 文件Java 里用 FileReader 一读控制台输出全乱。复盘原因很简单。在 Java 8 及之前的版本里FileReader 构造方法不让你指定字符集它内部直接用平台默认字符集也就是 Charset.defaultCharset()。在 Windows 中文环境下这个默认值通常是 GBK你用 GBK 去解码 UTF-8 的文件不乱码才怪。Linux 服务器上多数默认是 UTF-8所以在本地 Windows 复现不了部署到 Linux 又正常这种“环境差异型乱码”最容易让初学者头大。正确写法是用 InputStreamReader 包一层字节流并显式指定字符集// 老写法FileReader 无法指定字符集受平台默认值影响 // new FileReader(test.txt) —— 不推荐 // 推荐写法显式指定 UTF-8和文件实际编码保持一致 try (InputStreamReader reader new InputStreamReader( new FileInputStream(test.txt), StandardCharsets.UTF_8); BufferedReader bufferedReader new BufferedReader(reader)) { String line; while ((line bufferedReader.readLine()) ! null) { System.out.println(line); } }顺带补充一个新特性Java 11 开始FileReader 提供了 new FileReader(File file, Charset charset) 这样的构造方法可以指定字符集了。但为了兼容老版本我还是习惯用 InputStreamReader这个写法在任何 JDK 版本都不会出错。这里有个容易忽略的点写完文件立刻读文件有时也会乱码原因是写入时你用了 BufferedWriter但只调用了 write 方法没有 flush 或 close。缓冲区里的内容没真正落到磁盘或者落了一半读取时自然读到不完整的字节序列。所以读文件乱码不要只盯读取端写入端有没有正常 close 也要查。2.3 场景二控制台输出乱码不只是 Java 的锅很多人在 IDE 里运行好好的一到 Windows 命令行窗口用 java -jar 运行中文输出就乱码。这个锅要分成两半看。一半是字节流输出端的问题。System.out 本身是按平台默认编码输出在你用了 UTF-8 编译源码控制台代码页却是 936GBK时输出必然对不上。另一半是编译期问题javac 默认按平台编码读源码如果你的源码是 UTF-8 保存的用 javac 默认参数编译中文字符串字面量就已经坏了运行阶段自然没法看。解决思路有三个层次。第一层源码文件统一 UTF-8编译时显式指定编码javac -encoding UTF-8 Main.java如果是 Maven 构建记得在 pom.xml 里配置 project.build.sourceEncoding 为 UTF-8否则 IDE 和命令行编译结果可能不一致。第二层Windows 命令行窗口执行前切换代码页chcp 65001这个命令把控制台代码页切成 UTF-8。注意切完之后字体渲染也要支持中文不然照样显示方块。第三层是用到比较高阶的乱码场景比如热词里提到的“printf 中文乱码”“CLion 编译 C 代码输出中文乱码”这些虽然语言不同但本质和 Java 一样全是“源文件编码、编译器默认编码、控制台代码页”三者不一致导致的。我记得有人用 VS Code 写 C 语言printf 中文乱码折腾了半天编译参数最后发现是 VS Code 终端默认代码页和源文件编码不一致。所以碰到这类问题先不要怀疑语言检查三处编码是否统一90% 的情况都能解决。2.4 场景三中文存入数据库变成乱码从请求查到表“中文存入数据库变成乱码”这类问题在面试里也经常被拿来引申考察的是全链路的排查思路。我把它总结成一条排查链前端和请求 → 程序内部字符串 → JDBC 驱动 → 数据库连接 → 数据库表和会话。每次排查先把整条链路的字符集统一。前端页面如果用的是 GBK你却按 UTF-8 接收那字符串从 HTTP 进到程序时就已经坏了。程序内部确认 String 没问题后看 JDBC 连接串。以 MySQL 为例连接 URL 里要明确写入编码参数String url jdbc:mysql://localhost:3306/blog?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai;useUnicodetrue 和 characterEncodingutf8 这两个参数一定要成对出现只写一个有时候会被驱动忽略。再有就是建立表结构时指定字符集CREATE TABLE article ( id BIGINT PRIMARY KEY, title VARCHAR(200) ) DEFAULT CHARSETutf8mb4;这里特别提醒MySQL 的 utf8 实际上不是真正的 UTF-8它最多支持 3 字节像 emoji 这种 4 字节字符存不进去会变成乱码或报错。生产环境建议直接用 utf8mb4。很多人在热词里搜“中文存入数据库变成乱码的解决方法”其实最终排查下来往往就是建表用的字符集和 JDBC 连接字符集不一致。排错的核心思路就一句话在每一层入口检查数据的十六进制看是哪一层开始出现异常的字节。谁变了谁就是问题所在。2.5 排查乱码必备“三板斧”和两个小工具第一板斧看字节。程序里把可疑字符串转成字节数组并打印十六进制判断当前字符串内部是否已经损坏。System.out.println(Arrays.toString(str.getBytes(StandardCharsets.UTF_8))); // 输出形如 [-28, -72, -83] 时说明 UTF-8 编码的字节序列是存在的第二板斧统一字符集。所有环节尽量往 UTF-8 上靠不许出现“这里用 GBK 是因为老系统兼容”的说法除非有硬性对外需求。第三板斧借助外部工具确认文件真实编码。Notepad 右下角能看当前文件编码IDEA 右下角也能切换文件编码展示Linux 上用 file -i 文件名 也能看到编码声明。先确认存储层是什么编码再去动代码顺序一定不要反。排查乱码时我最喜欢用的一条命令是 xxd 文件名 | head直接查看文件头部的十六进制内容。UTF-8 文件如果有 BOM开头是 EF BB BF没有 BOM 的纯 UTF-8 文本开头就看不出特殊标记。这些细节都能帮你快速判断文件到底是不是 UTF-8以及要不要去掉 BOM。3. 资源泄漏关流这件小事为何天天翻车IO 流不关闭不会立刻报错但它在后台悄悄放血。面试里考察资源泄漏其实就是在考察你有没有形成肌肉记忆打开资源的那一刻就得想好何时关闭。3.1 为什么 IO 资源必须手动释放JVM 不管吗很多新手有个误解Java 有垃圾回收反正内存会自动清理流是不是也可以不管这是大错特错。文件句柄、网络连接符、数据库连接这些资源都不在 JVM 堆内存的管理范围内它们归属于操作系统。JVM 的垃圾回收器管不到操作系统的文件描述符表。操作系统对一个进程能打开的文件句柄数量是有限制的Linux 下通常用 ulimit -n 查看默认 1024 或者更大。如果你每秒打开一个文件但不关闭等到句柄耗尽后续任何打开文件的请求都会失败报错通常是“Too many open files”。在 Windows 上症状更隐蔽经常是程序运行一会儿后删不掉某个文件提示文件被占用其实就是你代码里没关流句柄被进程攥着不放。我排查过一例某个定时任务每分钟读取一个配置文件读完后没有关闭流。平时看起来一切正常跑了将近一个月突然某天服务的日志写不进去了因为日志文件所属目录的句柄被占满了。这种问题最阴险的地方在于它不是当场爆的而是慢慢累积等到量变引起质变。3.2 关闭流的正确姿势和常见的坑老牌写法是在 finally 块里关闭写得很啰嗦InputStream in null; OutputStream out null; try { in new FileInputStream(a.txt); out new FileOutputStream(b.txt); byte[] buffer new byte[8192]; int len; while ((len in.read(buffer)) ! -1) { out.write(buffer, 0, len); } } catch (IOException e) { e.printStackTrace(); } finally { if (out ! null) { try { out.close(); } catch (IOException e) { } } if (in ! null) { try { in.close(); } catch (IOException e) { } } }这段代码功能没错但问题很明显可读性差而且要自己处理每个流的空指针和异常一旦漏掉一个流就又是一个泄漏点。Java 7 起提供的 try-with-resources 语法是更优解强烈建议写进肌肉记忆try (InputStream in new FileInputStream(a.txt); OutputStream out new FileOutputStream(b.txt)) { byte[] buffer new byte[8192]; int len; while ((len in.read(buffer)) ! -1) { out.write(buffer, 0, len); } }try 后的圆括号里每声明一个资源编译器都会自动帮你在 try 块结束时调用 close()并且按声明顺序的逆序关闭。只要资源类实现了 AutoCloseable 接口都可以这么写。像 BufferedReader、InputStreamReader、Connection、Statement 这些常用的都在这个范畴。我再列几个实战中常见的坑第一个坑只关了外层流没管内层流。很多人担心只关 BufferedReader 会不会导致底层 FileInputStream 没关。答案是不会因为关闭外层流时BufferedReader 的 close 方法内部会调用底层流的 close。只要确保最外层流被关闭整条链都会释放。第二个坑先关了流再 flush。BufferedWriter 这种带缓冲的字符流write 方法把数据先写进内存缓冲区只有缓冲区满了或者手动 flush/close 才会真正落到目的文件。如果你先调 close 再想继续 write代码会直接抛 IOException。反过来如果你用 try-with-resources退出代码块时 close 会自动触发 flush不用担心漏掉。第三个坑复制文件时没有处理“读到 -1 结束”这个条件。while ((len in.read(buffer)) ! -1) 这句话里 -1 表示流已经读到末尾很多新手写成 while (in.read(buffer) ! -1)结果丢掉最后一段数据因为 read 返回的字节数没被记录下来最后一次读入缓冲区的数据没写出去。这个细节面试也常考。3.3 多资源场景和自定义资源的关闭顺序有些场景需要同时打开多个资源比如先建连接再建 Statement再建 ResultSet。用 try-with-resources 写起来很清爽try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { // 处理结果 } }注意关闭顺序ResultSet、Statement、Connection 是从内到外关闭。try-with-resources 会逆序关闭正好符合这个规范先关 ResultSet再关 Statement最后关 Connection。这个顺序很重要因为数据库连接如果先关里层的 Statement 可能就无法正确释放资源。如果你自己写了一个类也想放进 try-with-resources只要实现 AutoCloseable 接口并重写 close 方法即可。有一点要留心实现这个接口时close 方法按规范不应该抛出受检异常之外的异常否则上层捕获起来很别扭。我见过有人把 close 方法里直接 throw new Exception()搞得调用方处理异常很痛苦。实际项目中我还养成了一个习惯所有 IO 资源都在 try 圆括号里声明哪怕只有一个资源也绝不图省事写到 try 外面。这个习惯在 Code Review 里帮同事拦截过好几处潜在的泄漏问题。4. 效率低从一次一字节到系统级复制性能差别有多大“IO 效率低”很多时候不是 IO 本身慢而是你用错了姿势。同样一个文件复制不同写法性能可以差出好几个数量级。这里我拿实际测试数据说话。4.1 一次一字节的致命伤以及缓冲流的原理先看反面教材try (InputStream in new FileInputStream(big.zip); OutputStream out new FileOutputStream(big_copy.zip)) { int b; while ((b in.read()) ! -1) { out.write(b); } }这段代码逻辑没问题但性能很差。原因是 read() 无参方法每次只读一个字节这意味着每次循环都会触发一次底层系统调用从用户态切换到内核态。一个几十 MB 的文件就是几千万次系统调用慢到什么程度呢我在一个 100MB 文件上测试这种方式可能耗时几十秒甚至更久。解决思路是减少系统调用次数一次多读一点。BufferedInputStream 的底层原理就是这个它在内存里维护一个默认 8192 字节的缓冲区调用无参 read() 时先从缓冲区里取缓冲区空了才一次性向操作系统申请一批字节。逻辑上你还是在循环里一个个读但底层真实系统调用少了很多。如果你自己用 byte[] 缓冲数组来写效果类似而且可控性更强。常见用法byte[] buffer new byte[8192]; int len; while ((len in.read(buffer)) ! -1) { out.write(buffer, 0, len); }这里 byte[] 的大小是一个可以调优的参数。8KB 是很多场景的默认值但不是最优值。我在本地 SSD 上测试过几种常见缓冲区大小复制同一个 1GB 文件结果如下缓冲区大小复制 1GB 文件耗时约说明1 字节无缓冲超过 60 秒不推荐1 KB约 8 秒能用但偏慢8 KB约 2.5 秒大多数场景的均衡值64 KB约 1.8 秒机械硬盘和网络场景更好1 MB约 1.6 秒大文件复制更优但占用内存更多我不是建议无脑用 1MB因为缓冲区越大占用的内存越多而且收益到了 64KB 以后就开始放缓。日常用 8KB 或 64KB 就足够最关键的是别用 No buffer 那种逐字节读写。4.2 复制文件的四种写法性能直观对比我经常在面试中要候选人写一个文件复制方法最常见的四种写法里前两种体现基本功后两种体现知识广度。写法一无缓冲逐字节复制代码最少但性能最差上面已经提过。写法二自己维护 byte[] 缓冲区或者用 BufferedInputStream 包一层性能不错代码也容易理解。写法三直接用 JDK 提供的高层 API。Java 7 以后 Files.copy 一个方法搞定Files.copy(Paths.get(big.zip), Paths.get(big_copy.zip), StandardCopyOption.REPLACE_EXISTING);这个 API 底层会选择合适的复制策略在某些平台上甚至会走操作系统级别的 copy_file_range 之类的机制性能非常可观。而且代码量最少实现大文件复制首选。写法四用 FileChannel 的 transferTo把数据传输交给操作系统try (FileChannel in FileChannel.open(Paths.get(big.zip)); FileChannel out FileChannel.open(Paths.get(big_copy.zip), StandardOpenOption.CREATE, StandardOpenOption.WRITE)) { long position 0; long count in.size(); while (position count) { position in.transferTo(position, count - position, out); } }transferTo 是零拷贝思想的一种实现数据在内核态直接搬运减少了用户态和内核态的切换次数。对大文件复制效果很突出。需要注意的是transferTo 一次未必能传完所有字节所以必须循环判断 position 是否到达文件末尾这个细节我在代码里已经体现。四种写法综合对比如下写法代码量相对耗时适用场景无缓冲逐字节最少最慢教学演示byte[] Buffered中等中规中矩通用Files.copy一行很快简单文件复制FileChannel.transferTo中等最快大文件、追求极致性能4.3 更高级的“懒人”方案和误用提醒除了复制日常还有几个常见需求也有更省力的 API。读取小文件直接用 Files.readAllBytes()一行代码把整个文件读进 byte[]再用 new String(bytes, charset) 转字符串byte[] bytes Files.readAllBytes(Paths.get(config.json)); String content new String(bytes, StandardCharsets.UTF_8);这个 API 的隐藏风险是内存。如果文件有 1GBreadAllBytes 就把 1GB 全塞进内存很可能触发 OutOfMemoryError。所以它只适合小文件一般在几 MB 以内可以放心用。热词里有人提到 outofmemoryerror: insufficient memory很多就是这种大文件一次性读入导致的。按行处理大文件用 Files.lines() 或 BufferedReader 都行。Files.lines() 返回的是 Stream可以配合流式处理非常优雅try (StreamString lines Files.lines(Paths.get(access.log), StandardCharsets.UTF_8)) { long count lines.filter(line - line.contains(ERROR)).count(); System.out.println(count); }这里千万注意Files.lines() 返回的 Stream 也必须放在 try-with-resources 里不然同样会有资源泄漏问题。很多人以为 Stream 用完了就完事了其实它底层还持有文件句柄必须关闭。如果面对超大文件需要随机读写某一段可以考虑 MappedByteBuffer 内存映射文件但它是把文件映射到 JVM 地址空间使用不当容易引发内存占用过高和映射生命周期难控制的问题普通业务场景我不建议轻易上容易给自己挖坑。我个人在实际项目里的选型原则很简单复制文件优先 Files.copy 或 FileChannel按行解析文本大文件用 BufferedReader 显式大缓冲区读取小配置用 Files.readAllBytes一旦发现自己手动 while 循环逐字节搬先停下来想想是不是有现成 API 可用。5. 面试速查IO流八股加手写题的拿分套路IO 流在面试里考察的点很固定但面试官喜欢层层追问。这里我把高频问题整理成一张速查表再给一道手写题的推荐答案模板照着准备心里基本有底。5.1 面试高频 IO 题速查表问题答案要点字节流和字符流怎么选二进制用字节流文本优先字符流底层都是字节流为什么字符流会有乱码写入编码和读取编码不一致Java 内部 UTF-16外部常用 UTF-8/GBK关闭流有几种方式finally 手动关闭try-with-resources后者更推荐BufferedReader 为什么快内置缓冲区减少底层系统调用次数装饰器模式在 IO 流里的体现BufferedInputStream 包 FileInputStream增强功能NIO 和传统 IO 的区别传统 IO 面向流NIO 面向通道和缓冲区支持非阻塞、选择器多路复用序列化是什么Serializable 接口对象转字节流保存或传输transient 修饰的字段不参与序列化复制大文件的最佳姿势Files.copy 或 FileChannel.transferTo避免无缓冲逐字节flush 和 close 的区别flush 把缓冲数据强制写出但流仍可用close 关闭流前隐含 flushread() 返回 -1 的含义表示已经读到流末尾没有更多数据可读每年还有不少同学被问“BIO、NIO、AIO 的区别”这三者的核心差异在于阻塞模型BIO 是同步阻塞一个连接一个线程NIO 是同步非阻塞结合 Selector 用少量线程管理大量连接AIO 是异步非阻塞真正由操作系统通知完成。Java 里真正意义的大量连接场景一般用 Netty底层就是 NIO 基础上做了更完善的封装。5.2 面试手写题安全复制文件的推荐答案让候选人写文件复制是经典保留节目好的答案应该同时体现三点资源管理、性能意识、异常处理。下面这个版本可以直接背下来public static void copyFile(Path source, Path target) throws IOException { try (InputStream input Files.newInputStream(source); OutputStream output Files.newOutputStream(target)) { // 8KB 缓冲是比较均衡的选择大文件可调到 64KB byte[] buffer new byte[8192]; int bytesRead; while ((bytesRead input.read(buffer)) ! -1) { output.write(buffer, 0, bytesRead); } } }这段代码的加分点在哪里第一try-with-resources 自动关流资源管理无懈可击。第二read(buffer) 批量读取而非逐字节读性能在可接受范围。第三循环里用的不是 read 返回的数据而是 bytesRead 变量正确处理了最后一次读取不满缓冲区的情况。如果面试官继续追问怎么更快你再抛出 FileChannel.transferTo 和零拷贝这就已经从“会用”进阶到“懂原理”了。还有几个容易被追问的细节提前备好会有惊喜感。为什么 read 返回 int 而不是 byte因为 -1 要表示流结束byte 取值范围不够。为什么缓冲区大小不固定太小系统调用多太大内存浪费要根据场景调优。为什么 Files.copy 不需要循环因为 JDK 内部已经帮你封装好了完整复制逻辑。面试时不要只背结论试着用“底层做了什么”去解释每个选择。比如为什么字符流必须搭配字符集为什么缓冲能提高性能这些问题的答案都能追溯到操作系统和编码原理你只要把原理讲透了面试官就很难在这个环节挑出毛病。坦白说IO 流的 API 并不难记难的是遇到乱码和资源泄漏时你能不能快速判断问题出在哪一层。我自己排过无数个乱码问题最后发现九成都是字符集不统一排查过线上服务句柄被打满的事故根因就是某个不起眼的流没关。所以说写代码的时候多留心这三点所有外部资源交给你管理就要负责到底所有文本操作都用明确的字符集别依赖默认值和运气所有 IO 耗时操作先想想是不是有系统级 API 能用。先把这三条刻进脑子里Java IO 这关就算真正过了。