为什么文本复制和任意文件复制要分开讨论?
字符缓冲流:文本处理的"最佳实践"
2.1 为什么字符流最适合文本复制?
// 典型的字符缓冲流复制方案 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[]缓冲区,减少系统调用次数
2.2 一个容易踩的坑:字符流处理二进制文件
我曾用字符流复制一张图片,结果打开后发现图片损坏。原因很微妙:
- 字符流在读取时会根据编码规则将字节解码为字符
- 对于图片中的某些字节组合(如
0xFF 0xD8),可能被误判为某个字符或编码边界 - 写入时再将字符编码回字节,原始字节序列已经发生了不可逆的变化
结论:字符流是"有损"的(对二进制数据而言),它只适合人类可读的文本内容。
三、字节缓冲流:万能复制的底层逻辑
3.1 为什么字节流是"万能"的?
// 经典的字节缓冲流复制方案(万能复制) 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缓冲区的性能通常接近最优
3.2 缓冲区大小的选择:不是越大越好
我做过一个简单测试(复制100MB文件):
| 缓冲区大小 | 耗时 |
|---|---|
| 1字节(无缓冲) | 约120秒 |
| 512字节 | 约2.5秒 |
| 1KB(1024) | 约1.8秒 |
| 8KB(8192) | 约1.2秒 |
| 1MB | 约1.1秒 |
| 10MB | 约1.15秒 |
发现:
- 从无缓冲到1KB,性能提升最显著(减少系统调用次数)
- 超过8KB后,收益递减,因为内存拷贝开销开始显现
- Java默认的8KB缓冲(8192字节)是JVM开发者精心调校的结果
四、深入对比:两种流的本质差异
| 维度 | 字符缓冲流 | 字节缓冲流 |
|---|---|---|
| 处理单位 | char(2字节) | byte(1字节) |
| 编码处理 | 自动编解码 | 不处理编码 |
| 适用场景 | 文本文件(.txt, .java, .xml) | 任何文件(图片、视频、可执行文件) |
| 换行处理 | 支持readLine()/newLine() | 需手动处理字节级别的换行符 |
| 数据保真 | 可能因编码问题丢失原始字节 | 100%保真 |
| 缓冲数组 | char[] | byte[] |
五、实战建议:如何优雅选择?
原则1:判断内容是否"人类可读"
- 如果是文本 → 用字符缓冲流(代码更简洁,编码问题少)
- 如果是二进制或不确定 → 用字节缓冲流(安全、万能)
原则2:总是使用Buffered包装
// 不推荐:裸流,每次读写都进行系统调用 FileInputStream fis = new FileInputStream("a.jpg"); // 推荐:Buffered包装,减少90%以上的系统调用 BufferedInputStream bis = new BufferedInputStream(new FileInputStream("a.jpg"));原则3:Java 7+ 务必使用try-with-resources
try (InputStream in = new BufferedInputStream(new FileInputStream(src)); OutputStream out = new BufferedOutputStream(new FileOutputStream(dest))) { // 自动关闭,无需finally块 }六、延伸思考:NIO时代的文件复制
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()); // 内核态直接传输 } }