为什么 Java字符流不能直接用来安全地读取/拷贝二进制文件(如图片、视频)?
Java 字符流(如 FileReader、FileWriter)不能用来读取或拷贝图片、视频等二进制文件的核心原因在于:字符流在读写过程中会进行“字符编码与解码(Character Encoding/Decoding)”,这个过程会导致二进制数据的不可逆损坏。
下面我们深度解析其背后的底层原因、工作原理以及会导致的具体后果。
1. 核心原因:编码/解码造成的“信息失真”
字符流的设计初衷是处理文本。它的工作流程是:
- 读取时:将磁盘上的字节(Byte)根据某种字符集(如 UTF-8、GBK)解码成 Java 内存中的字符(Char,UTF-16 编码)。
- 写入时:将内存中的字符根据字符集编码回对应的字节写入磁盘。
然而,二进制文件(图片、视频、PDF、MP3 等)的字节数据是任意的(范围在 0x00 到 0xFF 之间),它们并不是按照任何字符集规范组织的。当用字符流处理它们时,会发生以下致命问题:
① 无法识别的字节被替换(乱码与信息丢失)
在很多字符集(如 UTF-8)中,某些特定的字节组合是“非法”的(不符合字符编码规则)。
- 发生什么: 当字符流遇到这些无法识别的字节时,它无法将其映射到任何字符,于是会用一个默认的占位字符来代替(通常是 Unicode 替换字符 ``,即
\uFFFD)。 - 后果: 原本的高清图片数据被替换成了“问号”字符。当再次写入时,这些 `` 会被编码成 UTF-8 的
0xEF 0xBF 0xBD。原始的二进制信息丢失,文件彻底损坏。
② 映射关系不对等(多对一或多字节合并)
- 发生什么: 在多字节编码(如 UTF-8)中,字符流会尝试把连续的 2 个、3 个甚至 4 个字节合并解码为一个字符。
- 后果: 原本独立的 3 个二进制字节,被强行合成为 1 个字符。写入时,由于编码器的规范,输出的字节序列可能已经和输入的完全不同。
2. 具体案例对比
假设我们有一个图片的片段,包含两个字节:[0x89, 0x50](这是 PNG 图片的标志性开头部分)。
情况 A:使用字节流(Safe)
- 读取:直接读取
0x89和0x50。 - 写入:直接写入
0x89和0x50。 - 结果:数据 100% 一致,图片正常打开。
情况 B:使用字符流(UTF-8 编码,Unsafe)
- 读取:字符流读取
0x89。在 UTF-8 中,0x89是一个非法的首字节(UTF-8 多字节字符的首字节不能是10xxxxxx格式)。 - 解码失败:JVM 将其识别为无效字符,替换为 `` (
\uFFFD)。 - 写入:字符流将
编码写回文件。在 UTF-8 中,被编码为三个字节:0xEF, 0xBF, 0xBD。 - 结果:原本的
0x89变成了0xEF, 0xBF, 0xBD。图片文件头被破坏,图片报错损坏,无法打开。
3. 其他潜在问题:平台换行符转换
有些字符流(或高级包装流,如 PrintWriter、BufferedReader.readLine())会针对不同的操作系统自动处理换行符(如将 Windows 的 \r\n 转换为 Unix 的 \n,或者反过来)。
在二进制文件中,0x0D(CR)和 0x0A(LF)可能仅仅代表图片中的两个像素点颜色,一旦被当作换行符进行了转换,文件结构立刻错位,彻底报废。
Summary:总结与正确做法
| 特性 | 字节流 (Byte Stream) | 字符流 (Character Stream) |
|---|---|---|
| 代表类 | InputStream, OutputStream |
Reader, Writer |
| 操作单位 | 字节 (byte / 8-bit) |
字符 (char / 16-bit) |
| 编码转换 | 无(原样搬运,最安全) | 有(按字符集解码/编码) |
| 适用场景 | 视频、图片、音频、压缩包、任何二进制文件 | 纯文本文件(TXT, HTML, CSV) |
黄金法则:
- 只要文件能用“记事本”打开且内容看得懂的(文本),用字符流。
- 只要是记事本打开是乱码的(图片、视频、可执行文件等),必须使用字节流。
拷贝二进制文件的正确代码示例(Java 7+):
java
// 使用 Java NIO,高效且安全
Path source = Paths.get("input.jpg");
Path target = Paths.get("output.jpg");
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);