这是一个非常核心且重要的 Java I/O 概念,我会从多个维度进行对比,并提供清晰的示例来帮助你理解。

核心概念:根本性的不同
理解这两者区别的关键在于它们处理数据的基本单位不同。
-
字节流
- 基本单位:字节,1 字节 = 8 位。
- 处理方式:直接与底层数据打交道,读写的是原始的二进制数据(0 和 1 的序列)。
- 特点:它是最通用的流,可以处理任何类型的数据,比如图片、音频、视频、文本文件等,因为所有文件在底层都是以字节形式存储的。
- 抽象基类:
InputStream(输入流) 和OutputStream(输出流)。
-
字符流
- 基本单位:字符,在 Java 中,字符是
char类型,由 2 个字节组成(使用 UTF-16 编码)。 - 处理方式:它不直接处理二进制数据,而是先将字节解码成字符,或将字符编码成字节,它专门为处理文本数据而设计。
- 特点:它内置了字符编码和解码机制,因此可以更方便、更安全地处理文本文件,避免了因编码不一致(如 UTF-8 和 GBK)而乱码的问题。
- 抽象基类:
Reader(输入流) 和Writer(输出流)。
- 基本单位:字符,在 Java 中,字符是
详细对比表格
| 特性维度 | 字节流 | 字符流 |
|---|---|---|
| 基本单位 | 字节 | 字符 |
| 处理数据 | 原始二进制数据 | 文本数据 |
| 适用场景 | 任何类型的文件:图片、音频、视频、.class 文件等 |
仅文本文件:.txt, .java, .csv, .xml, .json 等 |
| 核心抽象类 | InputStream (输入)OutputStream (输出) |
Reader (输入)Writer (输出) |
| 子类示例 | FileInputStreamFileOutputStreamBufferedInputStreamDataOutputStream |
FileReaderFileWriterBufferedReaderPrintWriter |
| 编码/解码 | 不提供,需要开发者手动处理。 | 自动提供,内部使用字符集(Charset)进行转换。 |
| 性能 | 对于非文本数据,性能更高,因为它没有额外的编码/解码开销。 | 对于文本数据,性能可能略低(因为有编码/解码过程),但更安全、更方便。 |
| 乱码风险 | 高,如果用字节流读取文本文件且指定了错误的编码,就会乱码。 | 低,只要在创建流时指定了正确的编码,就能有效避免乱码。 |
深入解析:编码与解码
这是字符流和字节流最本质的区别所在。

- 字节流:它就像一个搬运工,只负责把文件里的“箱子”(字节)从一个地方搬到另一个地方,它不关心箱子里装的是什么。
- 字符流:它像一个翻译官,它从文件里拿到“箱子”(字节),然后根据指定的“翻译规则”(字符集,如 UTF-8)把箱子里的内容翻译成“文字”(字符),写入时,它先把“文字”翻译成“箱子”(字节),再存入文件。
举个例子: 汉字 "中" 在 UTF-8 编码下占 3 个字节,在 GBK 编码下占 2 个字节。
- 如果你用 字节流 读取一个 UTF-8 编码的文本文件,它会把 "中" 的 3 个字节原封不动地读出来,如果你不知道它是 UTF-8,你可能会误以为它是 3 个独立的字符。
- 如果你用 字符流 (
FileReader) 并指定了 UTF-8 编码来读取,它会自动将这 3 个字节组合成一个 "中" 字符返回给你,非常方便。
代码示例对比
假设我们有一个文本文件 test.txt为 "你好,Java!"。
示例 1:使用字节流读写
import java.io.*;
public class ByteStreamExample {
public static void main(String[] args) {
// --- 写入 ---
try (FileOutputStream fos = new FileOutputStream("test_byte.txt")) {
String content = "你好,Java!";
// String.getBytes() 将字符串按平台默认编码转换为字节数组
fos.write(content.getBytes());
System.out.println("字节流写入成功");
} catch (IOException e) {
e.printStackTrace();
}
// --- 读取 ---
try (FileInputStream fis = new FileInputStream("test_byte.txt")) {
byte[] buffer = new byte[1024];
int len;
// 读取到的也是字节数组
while ((len = fis.read(buffer)) != -1) {
// 将字节数组按默认编码转换回字符串,可能会乱码
String str = new String(buffer, 0, len);
System.out.println("字节流读取到: " + str);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
分析:这里我们依赖了操作系统的默认字符集,如果文件是用 UTF-8 写入的,而系统默认是 GBK,读取时就会出现乱码。
示例 2:使用字符流读写(更安全)
import java.io.*;
import java.nio.charset.StandardCharsets; // 推荐使用 StandardCharsets
public class CharacterStreamExample {
public static void main(String[] args) {
// --- 写入 ---
try (FileWriter fw = new FileWriter("test_char.txt", StandardCharsets.UTF_8)) {
String content = "你好,Java!";
// FileWriter 会自动将字符按 UTF-8 编码成字节再写入
fw.write(content);
System.out.println("字符流写入成功");
} catch (IOException e) {
e.printStackTrace();
}
// --- 读取 ---
try (FileReader fr = new FileReader("test_char.txt", StandardCharsets.UTF_8)) {
char[] cbuf = new char[1024];
int len;
// 读取到的是字符数组
while ((len = fr.read(cbuf)) != -1) {
// 直接使用字符数组创建字符串,无需额外编码
String str = new String(cbuf, 0, len);
System.out.println("字符流读取到: " + str);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
分析:通过显式指定 StandardCharsets.UTF_8,我们确保了读写过程使用的是同一种编码规则,从而从根本上避免了乱码问题。注意:FileReader 和 FileWriter 的构造函数在 Java 11 之后才支持直接传入 Charset,在旧版本中,需要使用 InputStreamReader 和 OutputStreamWriter 来包装字节流并指定编码。

何时使用哪个?
这是一个非常实际的问题,遵循以下原则即可:
使用字符流 Reader/Writer 的情况:
- 当你的数据源或目标明确是纯文本时,读写
.txt,.csv,.properties,.xml,.json,.java等文件。 - 当你需要避免乱码,并且关心字符编码时,这是使用字符流最主要的原因,它能让你更专注于业务逻辑,而不是底层的字节转换。
使用字节流 InputStream/OutputStream 的情况:
- 当处理非文本文件时,读写图片(
.jpg,.png)、音频(.mp3)、视频(.mp4)、可执行文件(.exe)、数据库文件等,这些文件如果用字符流处理,文件会被损坏。 - 当你需要直接操作二进制数据时,进行网络数据传输、处理加密数据、或需要精确控制字节级别的读写时。
- 当你追求极致性能,且数据量巨大时,对于纯文本,如果性能是首要考虑因素且你能保证编码正确,字节流可能快一点点(但通常不推荐,因为风险高)。
关系与桥梁
字符流和字节流不是完全孤立的,它们之间有一个重要的桥梁:
InputStreamReader:它是一个字节流到字符流的桥接器,它包装一个InputStream,并使用指定的字符集将读取的字节解码为字符。OutputStreamWriter:它是一个字符流到字节流的桥接器,它包装一个OutputStream,并使用指定的字符集将写入的字符编码为字节。
这个设计非常巧妙,它让你既能利用字节流的底层操作能力,又能享受字符流的便捷性和安全性。
示例(旧版 Java 中指定编码读写文本文件的标准做法):
// 读取
try (InputStream fis = new FileInputStream("test.txt");
InputStreamReader isr = new InputStreamReader(fis, "UTF-8"); // 指定编码
BufferedReader br = new BufferedReader(isr)) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
// 写入
try (OutputStream fos = new FileOutputStream("output.txt");
OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8"); // 指定编码
BufferedWriter bw = new BufferedWriter(osw)) {
bw.write("你好,世界!");
} catch (IOException e) {
e.printStackTrace();
}
| 区别点 | 字节流 | 字符流 |
|---|---|---|
| 一句话总结 | 处理原始二进制数据的“搬运工” | 处理文本数据的“翻译官” |
| 核心差异 | 无编码/解码,操作字节 | 内置编码/解码,操作字符 |
| 选择原则 | 非文本文件、二进制操作 | 文本文件、避免乱码 |
记住这个核心区别:字节流处理字节,字符流处理字符(并自动处理编码),在绝大多数处理文本的场景下,都应优先选择字符流,因为它更安全、更符合人类的阅读习惯。
