1. 引言
我们先从最简单的说起:二进制文件与文本文件有什么不同?文本文件是可以用普通的记事本打开并看到字母、数字、空格和其他符号的文件。例如,my_notes.txt 或 poem.txt。
二进制文件是包含任意字节而非文本的文件。它可能是图片(.jpg、.png)、音乐(.mp3)、压缩包(.zip)、可执行文件(.exe)、视频(.mp4)、数据库文件等。如果你在记事本中打开这类文件,会看到类似 ÿØÿà 的东西,或一整屏莫名其妙的符号。这很正常!计算机“理解”的只有字节——对它来说,不论文本、图片还是视频,都只是字节序列。在文本文件中,这些字节可以被解释为字符;而在二进制文件中,它们是给程序处理的“原始”数据,不是给人阅读的。
用于处理二进制文件的基本类
在 Java 中,处理二进制文件使用字节流:
- InputStream — 读取字节的基类。
- OutputStream — 写入字节的基类。
用于文件的具体实现有:
- FileInputStream — 从文件读取字节。
- FileOutputStream — 将字节写入文件。
如果你听说过 FileReader 和 FileWriter,要知道:它们处理的是字符,只适用于文本。对于二进制文件,请只使用 InputStream/OutputStream 及其子类。
2. 读取二进制文件
逐字节读取
最简单的方法是逐字节读取。它直观,但非常慢。
try (FileInputStream in = new FileInputStream("image.jpg")) {
int b;
while ((b = in.read()) != -1) {
// b 是 0 到 255 的数值(字节),-1 表示文件结束
// 可以处理该字节,例如统计所有字节的总和
}
}
方法 read() 将下一个字节以 int 返回(从 0 到 255),当读到文件末尾时返回 -1。通常只有在需要做非常特定的事情(例如分析文件结构)时才逐字节读取。
分块读取(带缓冲)
逐字节读取就像每次只去商店买一个苹果。更高效的做法是一次拿一整袋!在 Java 中,可以使用 read(byte[] buffer) 方法,它会用文件中的字节填充数组。
try (FileInputStream in = new FileInputStream("image.jpg")) {
byte[] buffer = new byte[4096]; // 4 KB 的缓冲区
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
// buffer 中包含从文件读取的 bytesRead 个字节
// 可以处理这些字节,例如把它们保存到其他地方
}
}
方法 read(buffer) 会返回实际读取了多少字节(可能小于缓冲区大小,特别是在最后一次读取时)。这种方式要快得多,因为与磁盘的交互更少。
示例:复制文件
我们来写一个简单的程序,把任意二进制文件(例如图片)从一个位置复制到另一个位置。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class BinaryCopyExample {
public static void main(String[] args) {
String source = "cat.jpg";
String dest = "cat_copy.jpg";
try (FileInputStream in = new FileInputStream(source);
FileOutputStream out = new FileOutputStream(dest)) {
byte[] buffer = new byte[8192]; // 对于大多数场景,8 KB 是较优的大小
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
System.out.println("复制完成!");
} catch (IOException e) {
System.out.println("复制时出错:" + e.getMessage());
}
}
}
很简单:从源文件按块读取,然后立刻写入新文件。该方式适用于任意文件:图片、压缩包、视频等。
3. 写入二进制文件
写入字节数组
如果你有一个字节数组(例如从网络获得或在程序中生成),可以像这样把它写入文件:
byte[] data = new byte[] {1, 2, 3, 4, 5}; // 示例数组
try (FileOutputStream out = new FileOutputStream("data.bin")) {
out.write(data); // 将整个数组写入文件
}
方法 write(byte[]) 会把数组中的所有字节写入文件。也可以只写一部分:out.write(data, offset, length)。
分段写入文件(例如用于复制)
与读取类似,通常会使用缓冲区:
try (FileInputStream in = new FileInputStream("source.bin");
FileOutputStream out = new FileOutputStream("dest.bin")) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
这里把从文件读取到的内容立即写入另一个文件。这类代码在压缩程序、下载器、图像处理器等中很常见。
4. 实用细节
为什么不能对二进制文件使用 Reader/Writer?
Reader 和 Writer 处理的是字符(char),而不是字节。它们会按照某种编码(例如 UTF-8)自动在字节与字符之间转换。这对文本很方便,但对二进制文件则是致命的!
如果你尝试通过 FileWriter 写一张图片,会得到一个损坏且无法打开的文件。请记住:对任何非文本文件,只使用 InputStream/OutputStream!
处理二进制文件的重要区别与注意事项
- 缓冲区大小:缓冲区过小会变慢(磁盘访问过多),过大则会占用额外内存。4–16 KB 通常较为合适。
- 错误处理:务必处理 IOException —— 文件可能不存在、被锁定,或磁盘空间不足。
- 关闭流:使用 try-with-resources —— 即使出错也能确保文件被关闭。
- 覆盖文件:如果通过 new FileOutputStream("file.bin") 打开文件,它会被覆盖。若要追加到末尾,请使用带有 append = true 参数的构造函数。
- 访问权限:如果程序无法打开文件,请检查读/写权限。
- readAllBytes():可一行代码将整个文件读入字节数组。不要对大文件使用,以免耗尽内存!
5. 处理二进制文件的常见错误
错误 №1:对二进制文件使用 FileReader/FileWriter。这会导致数据损坏,因为这些类会在字节与字符之间转换,而对图片、压缩包等来说这是灾难性的。
错误 №2:忽略 read() 的返回值。方法 read(byte[]) 可能读取的字节数少于你请求的,尤其是在最后一个块。务必使用返回值来知道实际处理了多少字节。
错误 №3:忘记关闭流。如果不关闭文件,它可能会一直被锁定,且数据(尤其在写入时)可能无法完全写出。请使用 try-with-resources。
错误 №4:不考虑大小就把整个文件一次性读入内存。对于大文件,这会导致 OutOfMemoryError。请使用缓冲区分块读取。
错误 №5:不处理异常。文件操作随时可能失败:文件未找到、无权限、磁盘已满。别忘了处理 IOException。
GO TO FULL VERSION