





















最近在整理项目代码时,突然意识到一个看似简单的问题——"文件复制"——背后竟藏着Java I/O体系最核心的设计哲学。这篇文章从一道竞赛题出发,聊聊字符缓冲流和字节缓冲流的本质差异,以及为什么字节流才是真正的"万能复制"方案。
初学Java I/O时,很多人(包括我)都曾困惑:既然FileInputStream/FileOutputStream能复制任何文件,为什么还要有FileReader/FileWriter?
直到遇到这样一个场景:
.txt文件,要求按行处理内容,最后统计字符数.jpg图片,要求内容完全一致,不能有任何字节丢失这两个场景对I/O流的要求截然不同。
// 典型的字符缓冲流复制方案
public static void copyTextFile(String src, String dest) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(src));
BufferedWriter bw = new BufferedWriter(new FileWriter(dest))) {
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine(); // 自动适配系统换行符
}
}
}
核心优势:
FileReader默认使用系统编码(UTF-8),自动处理字符编码转换readLine()让文本处理变得优雅,无需手动处理 或 BufferedReader内部维护char[]缓冲区,减少系统调用次数我曾用字符流复制一张图片,结果打开后发现图片损坏。原因很微妙:
0xFF 0xD8),可能被误判为某个字符或编码边界结论:字符流是"有损"的(对二进制数据而言),它只适合人类可读的文本内容。
// 经典的字节缓冲流复制方案(万能复制)
public static void copyAnyFile(String src, String dest) throws IOException {
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(src));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dest))) {
byte[] buffer = new byte[1024]; // 1KB缓冲区,平衡内存与速度
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
}
}
万能的本质:
BufferedInputStream通过byte[]缓冲减少IO次数,8KB缓冲区的性能通常接近最优我做过一个简单测试(复制100MB文件):
| 缓冲区大小 | 耗时 |
|---|---|
| 1字节(无缓冲) | 约120秒 |
| 512字节 | 约2.5秒 |
| 1KB(1024) | 约1.8秒 |
| 8KB(8192) | 约1.2秒 |
| 1MB | 约1.1秒 |
| 10MB | 约1.15秒 |
发现:
| 维度 | 字符缓冲流 | 字节缓冲流 |
|---|---|---|
| 处理单位 | char(2字节) | byte(1字节) |
| 编码处理 | 自动编解码 | 不处理编码 |
| 适用场景 | 文本文件(.txt, .java, .xml) | 任何文件(图片、视频、可执行文件) |
| 换行处理 | 支持readLine()/newLine() |
需手动处理字节级别的换行符 |
| 数据保真 | 可能因编码问题丢失原始字节 | 100%保真 |
| 缓冲数组 | char[] |
byte[] |
// 不推荐:裸流,每次读写都进行系统调用
FileInputStream fis = new FileInputStream("a.jpg");
// 推荐:Buffered包装,减少90%以上的系统调用
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("a.jpg"));
try (InputStream in = new BufferedInputStream(new FileInputStream(src));
OutputStream out = new BufferedOutputStream(new FileOutputStream(dest))) {
// 自动关闭,无需finally块
}
Java NIO提供了更高效的方案,但在理解基础I/O之前,掌握字符流和字节流的区别仍是必修课:
// Java NIO 零拷贝方案(了解即可)
public static void nioCopy(String src, String dest) throws IOException {
try (FileChannel source = new FileInputStream(src).getChannel();
FileChannel target = new FileOutputStream(dest).getChannel()) {
target.transferFrom(source, 0, source.size()); // 内核态直接传输
}
}
回到最初的问题:字符流和字节流不是替代关系,而是不同抽象层次的工具。字符流是"懂内容"的流,字节流是"只搬运"的流。理解它们的边界,才能在复制文件、网络传输、序列化等场景中做出正确选择。
一句话总结:文本用字符缓冲流,其他全部用字节缓冲流。这不是教条,而是对Java I/O设计哲学的尊重。
欢迎在评论区分享你在I/O流使用中遇到的坑,或者讨论NIO与传统IO的性能差异。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。