这是一个非常核心且基础的 Java I/O 概念,理解它对于正确处理数据至关重要。
核心区别:一张图看懂
| 特性 | 字节流 | 字符流 |
|---|---|---|
| 处理单位 | 字节 | 字符 |
| 父类 | InputStream / OutputStream |
Reader / Writer |
| 本质 | 原始的二进制数据,不关心编码 | 经过解码的字符,与编码格式强相关 |
| 用途 | 处理所有类型的二进制数据(图片、音频、视频、.class文件等) |
处理文本数据(.txt, .java, .xml, .json等) |
| 编码/解码 | 不进行编码/解码,直接读写原始字节 | 自动进行编码(写时)和解码(读时) |
| 性能 | 通常更快,因为直接操作内存中的基本单位 | 相对较慢,因为涉及字符集转换 |
| 典型类 | FileInputStream, FileOutputStream, BufferedInputStream |
FileReader, FileWriter, BufferedReader |
深入解析
字节流 - 万能的搬运工
字节流是 Java I/O 的基础,它以 8 位(1字节) 为单位进行数据读写,它不关心这些字节代表什么,是文本、图片还是声音,它都一视同仁地当作二进制数据来处理。
- 核心思想:原始、底层、高效。
- 工作方式:就像一个搬运工,只负责把一箱箱(字节)货物从一个地方搬到另一个地方,不关心箱子里装的是什么。
- 典型应用场景:
- 复制图片、音频、视频等非文本文件。
- 读取
.class字节码文件。 - 网络传输中传输原始数据包。
- 任何不关心内容格式,只关心数据完整性的场景。
示例代码:使用字节流复制一张图片
import java.io.*;
public class ByteStreamExample {
public static void main(String[] args) {
// try-with-resources 语句,自动关闭资源
try (FileInputStream fis = new FileInputStream("source.jpg");
FileOutputStream fos = new FileOutputStream("destination.jpg")) {
byte[] buffer = new byte[1024]; // 创建一个 1KB 的缓冲区
int length; // 记录每次读取到的字节数
// 循环读取,直到文件末尾
while ((length = fis.read(buffer)) > 0) {
fos.write(buffer, 0, length); // 将读取到的字节写入目标文件
}
System.out.println("图片复制成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
字符流 - 文本的翻译官
字符流以 字符 为单位进行数据读写,它内部使用了一种叫做字符流的机制,负责在字节和字符之间进行转换。
- 核心思想:面向文本、智能、方便。
- 工作方式:就像一个翻译官,在读取时,它会将文件中的字节按照指定的字符集(如 UTF-8, GBK) 解码成字符;在写入时,它会将字符按照指定的字符集编码成字节,然后再写入文件。
- 典型应用场景:
- 读写
.txt,.java,.csv,.xml,.json等纯文本文件。 - 需要处理多语言文本的场景,因为它能正确处理不同编码的字符。
- 读写
关键点:字符集编码 这是字符流的灵魂,如果读取文件时使用的编码与文件实际保存的编码不一致,就会出现乱码,Java 会使用平台的默认字符集,但这在不同环境下(如 Windows/Linux)可能会导致问题,因此最好显式指定。
示例代码:使用字符流读取一个文本文件
import java.io.*;
public class CharacterStreamExample {
public static void main(String[] args) {
// 假设文件 source.txt 是用 UTF-8 编码保存的
// FileReader 会使用 JVM 默认的字符集,可能出错
// FileReader fr = new FileReader("source.txt"); // 不推荐
// 推荐方式:显式指定字符集
try (FileReader fr = new FileReader("source.txt", StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(fr)) { // 使用 BufferedReader 提高效率
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
注意:在 Java 11 及以上版本,FileReader 和 FileWriter 的构造函数可以直接接受 Charset 参数,在早期版本中,你需要使用 InputStreamReader 和 OutputStreamWriter 来包装字节流并指定字符集。
为什么需要字符流?—— 解决乱码问题
假设我们要用字节流来读取一个用 UTF-8 编码的中文文本文件 "你好.txt"。
- "你" 这个字符在 UTF-8 中占用 3 个字节。
- "好" 这个字符在 UTF-8 中也占用 3 个字节。
如果使用 FileInputStream 读取,你得到的是 6 个独立的字节:[-28, -67, -96, -27, -91, -67],这些字节本身没有意义,除非你知道它们应该被组合成 "你好" 这两个汉字。
而使用 FileReader (底层是 InputStreamReader) 时,它会:
- 读取到第一个字节
-28。 - 知道 UTF-8 编码规则,它会继续读取接下来的两个字节
-67和-96。 - 将这 3 个字节解码成一个字符 '你'。
- 同理,将接下来的 3 个字节解码成一个字符 '好'。
这样,你得到的直接就是 "你好" 这个字符串,而不是一堆无意义的字节。
如何选择?
这是一个非常简单的决策流程:
-
问自己:我要处理的文件内容是纯文本吗?
- 是 -> 优先使用字符流 (
Reader/Writer系列)。- 如果处理的是文件,用
FileReader/FileWriter(或带Charset参数的版本)。 - 为了提高效率,用
BufferedReader/BufferedWriter包装它们。
- 如果处理的是文件,用
- 不是 -> 必须使用字节流 (
InputStream/OutputStream系列)。- 如果处理的是文件,用
FileInputStream/FileOutputStream。 - 为了提高效率,用
BufferedInputStream/BufferedOutputStream包装它们。
- 如果处理的是文件,用
- 是 -> 优先使用字符流 (
-
特殊情况:
- 需要同时处理文本和二进制数据:一个 HTTP 响应可能同时包含头部文本和图片二进制数据,这时,你需要使用字节流来处理整个响应流,或者使用
InputStreamReader来专门处理文本部分。 - 网络编程:底层的 Socket 通信是基于字节流的,你发送和接收的都是原始字节,当你需要传输文本时,你需要在应用层手动进行编码(将字符串转成字节数组)和解码(将字节数组转成字符串)。
- 需要同时处理文本和二进制数据:一个 HTTP 响应可能同时包含头部文本和图片二进制数据,这时,你需要使用字节流来处理整个响应流,或者使用
| 字节流 | 字符流 | |
|---|---|---|
| 形象比喻 | 搬运工,只管箱子(字节) | 翻译官,理解箱子里的内容(字符) |
| 处理单位 | 字节 | 字符 |
| 编码处理 | 无 | 自动 |
| 核心优势 | 通用、高效 | 专为文本设计,避免乱码 |
| 选择依据 | 非文本文件(图片、音频等) | 文本文件(.txt, .java等) |
记住这个核心原则:处理文本用字符流,处理二进制用字节流,这是 Java I/O 编程中一个必须掌握的基本准则。
