在Java字符流体系中,FileWriter是最常被拿来写文本文件的类之一。它继承自OutputStreamWriter,内部通过FileOutputStream把字符转成字节写入磁盘。看似简单的一行new FileWriter("a.txt"),背后却藏着缓冲、编码、刷盘、关闭等多个容易出错的环节。理解这些细节,才能解释为什么很多初学者会觉得FileWriter写文件不稳定。

一、FileWriter的底层机制与默认行为
FileWriter本身并没有自己实现复杂的缓冲逻辑,它只是一个方便创建字符输出流的包装类。构造时如果不指定第二个boolean参数,默认是以覆盖模式打开文件;指定为true则是追加模式。它依赖父类OutputStreamWriter中的StreamEncoder来完成字符到字节的编码,而真正的字节写出由底层的FileOutputStream和系统调用承担。
问题在于,FileWriter每一次write操作,在很多JDK实现里都会尽量把数据推到操作系统层面,但并不会保证立刻物理落盘。操作系统的页缓存机制意味着数据可能暂存在内存中。如果此时进程被强制杀死,或者没有正常走close流程,那部分尚未刷入磁盘的数据就会丢失。下面的代码演示了最基础的用法,也是最容易出问题的写法:
import java.io.FileWriter;
import java.io.IOException;
public class BasicWrite {
public static void main(String[] args) throws IOException {
// 默认覆盖模式,不缓冲,不显式flush
FileWriter fw = new FileWriter("test.txt");
fw.write("第一行数据n");
fw.write("第二行数据n");
// 如果这里程序异常退出,上面内容可能没落盘
fw.close();
}
}
上面这段程序在正常运行并调用close时没有问题,因为close方法内部会执行flush。但如果在两次write之间抛出了未捕获的异常,close就不会被执行,数据就会滞留在缓冲或系统缓存中。这就是所谓不稳定的第一个根源:缺乏可靠的资源释放机制。
二、常见陷阱一:忘记关闭或异常导致未刷盘
很多人在写工具方法时,把FileWriter写在try块里,却在catch中只打印了异常,最后没有finally关闭。一旦写的过程中出现磁盘满、权限不足等情况,流就不会被关闭,数据丢失且文件句柄泄漏。在长时间运行的服务中,这种泄漏会累积到无法打开新文件。
使用try-with-resources是从语法层面规避该问题的首选方案。它可以保证无论是否异常,close都会被调用。改写后的代码更加安全:
import java.io.FileWriter;
import java.io.IOException;
public class SafeWrite {
public static void main(String[] args) {
// try-with-resources自动关闭
try (FileWriter fw = new FileWriter("test.txt", true)) {
fw.write("追加一行n");
fw.write("再追加一行n");
} catch (IOException e) {
e.printStackTrace();
}
}
}
这里第二个参数true表示追加,适合日志类场景。即便写入时发生IO异常,fw.close()依然会被调用,从而触发flush,最大限度避免丢数据。不过要注意,close抛出的异常如果在try块中已被抑制,需要通过getSuppressed方法查看,但这已属于进阶处理。
三、常见陷阱二:未使用缓冲造成性能与碎片化
当业务需要循环写入上千条短文本时,直接使用FileWriter会产生大量细小的系统调用。每一次write都要跨越用户态与内核态,CPU开销明显,吞吐量下降。此时应该用BufferedWriter包裹FileWriter,把多次字符累积到内存缓冲,满一定量再一次性写出。
下面示例展示了缓冲写法,并演示了手动flush控制节奏:
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
public class BufferedWrite {
public static void main(String[] args) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter("log.txt", true))) {
for (int i = 0; i < 1000; i++) {
bw.write("事件编号:" + i + "n");
// 每200条强制刷一次,兼顾性能与安全
if (i % 200 == 0) {
bw.flush();
}
}
// 退出try前自动close,会再flush剩余
} catch (IOException e) {
e.printStackTrace();
}
}
}
BufferedWriter默认缓冲大小是8192字符,对绝大多数文本写入已经足够。如果单条记录非常大,可以适当调小flush频率;如果是高并发日志,还要考虑加锁或使用异步Appender。但无论如何,比起裸用FileWriter,缓冲是提升稳定性的低成本改动。
四、常见陷阱三:编码不明确引发乱码
FileWriter的另外一个隐形坑是编码。它的构造器没有直接接收Charset参数的历史版本,会使用平台默认编码。在中文Windows上可能是GBK,而在Linux服务器上通常是UTF-8。同一份代码在不同环境写出文件,用错编码打开就是乱码,看起来也像数据写错或丢失。
较新的JDK提供了FileWriter(File file, Charset charset)之类的构造器,或者更推荐直接使用OutputStreamWriter配合FileOutputStream来显式指定编码:
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.io.IOException;
public class EncodeWrite {
public static void main(String[] args) {
try (BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream("utf8.txt", true),
StandardCharsets.UTF_8))) {
bw.write("明确以UTF-8写入中文n");
} catch (IOException e) {
e.printStackTrace();
}
}
}
这种方式把字节流与字符编码解耦,比依赖FileWriter默认行为更可控。如果维护老项目不能改JDK版本,也可以通过设置系统属性file.encoding统一环境编码,但那属于外部约束,不如代码内显式声明清晰。
五、最佳实践总结
综合上述分析,让FileWriter稳定工作的核心做法可以归纳为几点:第一,永远用try-with-resources或finally保证流关闭;第二,写频繁短文本时用BufferedWriter包装;第三,明确字符编码,不要依赖平台默认值;第四,对不能丢失的数据在关键节点调用flush;第五,根据场景选择覆盖或追加模式。
下表列出了不同写法在可靠性与性能上的差异,方便在做技术选型时快速对照:
| 写法 | 资源释放 | 吞吐表现 | 编码可控 |
|---|---|---|---|
| 裸FileWriter+手动close | 易遗漏 | 较差 | 否 |
| try-with-resources+FileWriter | 安全 | 较差 | 视构造器 |
| BufferedWriter包装 | 安全 | 良好 | 视外层 |
| OutputStreamWriter显式编码 | 安全 | 良好 | 是 |
把这些实践落到日常代码里,FileWriter就不再是那个让人头疼的不稳定组件,而是一把顺手的文本写入工具。面对更高并发或分布式场景,再进一步考虑使用日志框架或内存映射文件,但基础原理始终离不开缓冲、编码与刷盘这三个支点。
JavaFileWriterIO缓冲修改时间:2026-08-04 16:30:34