面试(六)——Java IO 流
在 Java 编程中,IO 流是连接程序与外部数据载体(文件、网络、内存等)的核心桥梁。多数开发者在使用 IO 流时,常停留在 “调用 API” 的表层阶段,对其底层设计逻辑、数据传输原理及基础组件的核心作用缺乏深入理解。本文将从专业基础视角,逐层拆解 IO 流的核心概念、类结构与底层实现,帮你构建扎实的 IO 流认知体系。
一、IO 流的底层核心概念:理解数据传输的本质
在深入代码之前,必须先明确 IO 流的三个核心底层概念,这是理解所有 IO 操作的基础。
1. 流的定义:数据的 “管道化传输”
IO 流(Input/Output Stream)本质是数据的有序传输序列,它将数据从 “源”(如文件、键盘、网络)传输到 “目的地”(如内存、文件、控制台),如同一条 “管道”:
- 输入流(Input Stream):数据从外部载体流入程序内存,是 “读” 操作的载体;
- 输出流(Output Stream):数据从程序内存流出到外部载体,是 “写” 操作的载体;
- 流的方向性:IO 流是单向的 —— 输入流只能读、输出流只能写,不存在 “双向流”(需双向传输时需分别创建输入 / 输出流)。
2. 数据单位:字节与字符的底层差异
IO 流分为字节流与字符流,核心差异在于数据传输的最小单位,这直接决定了它们的适用场景:
- 字节(byte):1 字节 = 8 位(bit),是计算机底层存储的最小单位,可表示所有二进制数据(图片、视频、文本的二进制编码等);
- 字符(char):1 字符 = 2 字节(Java 中),是文本数据的最小单位,对应 Unicode 编码(可表示中文、英文等各类字符);
- 关键关联:字符本质是 “字节的语义化封装”—— 文本文件的底层存储仍是字节,字符流通过 “编码表”(如 UTF-8、GBK)将字节转换为字符,避免手动处理编码逻辑。
3. 流的生命周期:从创建到关闭的 “资源管理”
IO 流属于系统级资源(依赖操作系统的文件句柄、网络端口等),其生命周期必须严格管理,否则会导致资源泄漏:
- 创建流:通过构造方法关联外部载体(如new FileInputStream("test.txt")关联本地文件);
- 操作流:调用read()/write()等方法传输数据;
- 关闭流:调用close()方法释放系统资源(必须执行,即使操作报错);
- 自动关闭:Java 7 + 的try-with-resources语法可自动关闭实现AutoCloseable接口的流(IO 流均实现此接口),避免手动关闭遗漏。
二、字节流的底层解析:二进制数据的 “原生传输”
字节流以byte为最小单位,是所有 IO 流的 “基础骨架”,直接操作二进制数据,不涉及编码转换。其核心父类是InputStream(输入字节流)和OutputStream(输出字节流),所有字节流实现类均继承自这两个抽象类。
1. 核心父类:InputStream 与 OutputStream 的抽象定义
这两个类通过抽象方法定义了字节流的核心行为,子类需根据具体场景(文件、内存、网络)实现这些方法。
(1)InputStream:输入字节流的 “行为规范”
InputStream是所有输入字节流的父类,核心抽象方法与常用功能如下:
方法签名 | 核心作用 | 底层逻辑 |
abstract int read() | 读取 1 个字节,返回字节值(0~255);若已到流末尾,返回 - 1 | 从关联的外部载体(如文件)读取 1 个字节到内存,阻塞直到有数据或流结束 |
int read(byte[] b) | 读取多个字节到字节数组b,返回实际读取的字节数;末尾返回 - 1 | 批量读取数据,减少 IO 次数(比单字节读取效率高 10~100 倍) |
int read(byte[] b, int off, int len) | 读取len个字节到数组b,从索引off开始存储 | 更灵活的批量读取,避免覆盖数组已有数据 |
long skip(long n) | 跳过n个字节,返回实际跳过的字节数 | 移动流的 “读取指针”,不读取数据(如跳过文件头部的固定标识) |
void close() | 关闭流,释放系统资源 | 通知操作系统释放关联的文件句柄 / 端口,必须调用 |
关键注意点:read()方法返回的是 “字节的无符号值”(0~255),若直接强转为char可能出现乱码(需通过字符流处理)。
(2)OutputStream:输出字节流的 “行为规范”
OutputStream是所有输出字节流的父类,核心抽象方法与常用功能如下:
方法签名 | 核心作用 | 底层逻辑 |
abstract void write(int b) | 写入 1 个字节(仅取int的低 8 位,高 24 位忽略) | 将内存中的 1 个字节写入外部载体,阻塞直到写入完成 |
void write(byte[] b) | 将字节数组b的所有字节写入 | 批量写入,减少 IO 次数 |
void write(byte[] b, int off, int len) | 将数组b中从off开始的len个字节写入 | 灵活的批量写入(如只写入数组的部分数据) |
void flush() | 强制刷新缓冲区,将缓冲中的数据写入外部载体 | 若流有缓冲区(如BufferedOutputStream),需调用此方法确保数据不滞留 |
void close() | 关闭流,释放系统资源 | 关闭前会自动调用flush(),确保缓冲数据写入 |
关键注意点:write(int b)方法接收int类型参数,但仅使用低 8 位(因为 1 字节 = 8 位),例如write(0x1234)实际写入的是0x34(低 8 位)。
2. 基础实现类:FileInputStream 与 FileOutputStream
这两个类是字节流中最常用的实现,直接关联本地文件,实现文件的二进制读写,是理解 “文件 IO” 的基础。
(1)FileInputStream:读取本地文件的字节流
构造方法(核心):
- FileInputStream(String name):通过文件路径关联文件(如new FileInputStream("D:/test.dat"));
- FileInputStream(File file):通过File对象关联文件(更灵活,可先判断文件是否存在)。
底层原理:创建FileInputStream时,会调用操作系统的 “打开文件” 接口(如 Windows 的CreateFile、Linux 的open),获取文件句柄(一个标识文件的整数),后续的read()操作均通过文件句柄与操作系统交互,读取文件的二进制数据到内存。
实战:单字节读取与批量读取的效率对比
public class FileInputStreamDemo {public static void main(String[] args) throws IOException {String filePath = "large_file.bin"; // 100MB的二进制文件// 1. 单字节读取(效率极低)long start1 = System.currentTimeMillis();try (FileInputStream fis = new FileInputStream(filePath)) {int b;while ((b = fis.read()) != -1) {// 仅读取,不处理(模拟空操作)}}System.out.println("单字节读取耗时:" + (System.currentTimeMillis() - start1) + "ms");// 2. 批量读取(效率高)long start2 = System.currentTimeMillis();try (FileInputStream fis = new FileInputStream(filePath)) {byte[] buffer = new byte[8192]; // 8KB缓冲区(推荐大小:4KB~64KB)int len;while ((len = fis.read(buffer)) != -1) {// 仅读取,不处理}}System.out.println("批量读取耗时:" + (System.currentTimeMillis() - start2) + "ms");}
}
运行结果(参考):单字节读取耗时约 5000ms,批量读取耗时约 20ms—— 批量读取通过减少 “用户态与内核态的切换次数”(IO 操作需从用户程序切换到操作系统内核),大幅提升效率。
(2)FileOutputStream:写入本地文件的字节流
构造方法(核心):
- FileOutputStream(String name):通过路径关联文件,默认 “覆盖写入”(若文件已存在,清空原有内容);
- FileOutputStream(String name, boolean append):append为true时 “追加写入”(在文件末尾添加数据,不覆盖原有内容);
- FileOutputStream(File file)/FileOutputStream(File file, boolean append):通过File对象关联文件。
底层原理:创建时同样会获取文件句柄,write()操作通过文件句柄将内存中的字节写入操作系统的 “文件缓冲区”,最终由操作系统异步写入磁盘(若需立即写入磁盘,需调用flush()或使用RandomAccessFile的 “同步写入” 模式)。
实战:追加写入日志文件
public class FileOutputStreamDemo {public static void writeLog(String logContent) throws IOException {// 追加写入日志,避免覆盖历史日志try (FileOutputStream fos = new FileOutputStream("app.log", true)) {// 拼接日志时间与内容String log = LocalDateTime.now() + " - " + logContent + "\n";fos.write(log.getBytes(StandardCharsets.UTF_8)); // 显式指定编码,避免平台默认编码问题}}public static void main(String[] args) throws IOException {writeLog("用户[123]登录成功");writeLog("用户[456]查询数据");}
}
关键细节:getBytes(StandardCharsets.UTF_8)显式指定编码为 UTF-8,避免依赖操作系统的默认编码(如 Windows 默认 GBK、Linux 默认 UTF-8),确保日志在不同平台下读取无乱码。
3. 缓冲优化类:BufferedInputStream 与 BufferedOutputStream
基础字节流(如FileInputStream)的每次read()/write()都会直接触发系统 IO 操作,而缓冲字节流通过在内存中开辟独立的缓冲区,减少系统 IO 次数,是 “高性能 IO” 的基础。
(1)核心原理:内存缓冲区的 “批量转发”
- BufferedInputStream:创建时默认分配 8KB 的内存缓冲区,调用read()时,先从缓冲区读取数据;若缓冲区为空,一次性从底层流(如FileInputStream)读取 8KB 数据到缓冲区,再从缓冲区返回数据 —— 原本 1000 次read()操作,只需 1 次系统 IO(若每次读 1 字节,8KB 缓冲区可减少 7999 次系统 IO)。
- BufferedOutputStream:创建时默认分配 8KB 缓冲区,调用write()时,先将数据写入缓冲区;若缓冲区满,一次性将 8KB 数据写入底层流 —— 同样减少系统 IO 次数。
(2)构造方法与关键方法
- 构造方法:BufferedInputStream(InputStream in)(默认 8KB 缓冲区)、BufferedInputStream(InputStream in, int size)(自定义缓冲区大小,如new BufferedInputStream(fis, 65536)设置 64KB 缓冲区);
- 关键方法:flush()(仅BufferedOutputStream需调用,强制将缓冲区数据写入底层流,避免数据滞留)、close()(关闭时会自动调用flush(),无需手动调用,但主动调用更安全)。
实战:缓冲流与基础流的效率对比(复制大文件)
public class BufferedStreamDemo {public static void copyFile(String sourcePath, String targetPath, boolean useBuffer) throws IOException {long start = System.currentTimeMillis();if (useBuffer) {// 使用缓冲流复制try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(sourcePath));BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(targetPath))) {byte[] buffer = new byte[8192];int len;while ((len = bis.read(buffer)) != -1) {bos.write(buffer, 0, len);}}} else {// 使用基础流复制try (FileInputStream fis = new FileInputStream(sourcePath);FileOutputStream fos = new FileOutputStream(targetPath)) {byte[] buffer = new byte[8192];int len;while ((len = fis.read(buffer)) != -1) {fos.write(buffer, 0, len);}}}System.out.println((useBuffer ? "缓冲流" : "基础流") + "复制耗时:" + (System.currentTimeMillis() - start) + "ms");}public static void main(String[] args) throws IOException {String source = "large_video.mp4"; // 500MB视频文件String target1 = "target1.mp4";String target2 = "target2.mp4";copyFile(source, target1, false); // 基础流复制copyFile(source, target2, true); // 缓冲流复制}
}
运行结果(参考):基础流复制耗时约 1500ms,缓冲流复制耗时约 100ms—— 缓冲流通过减少系统 IO 次数,将效率提升一个数量级,是处理大文件的 “必备工具”。
(3)缓冲区大小的选择原则
- 默认 8KB:适用于多数场景(如文本文件、中小型二进制文件);
- 大文件(100MB+):可将缓冲区调至 64KB~256KB(如new BufferedInputStream(in, 262144)),减少缓冲区满的频率,但需避免过大(如超过 1MB)导致内存浪费;
- 小文件(1KB 以下):无需自定义缓冲区,默认 8KB 足够(缓冲区过大反而会占用多余内存)。
三、字符流的底层解析:文本数据的 “语义化传输”
字符流以char为最小单位,专为文本处理设计,核心解决 “字节与字符的编码转换” 问题。其核心父类是Reader(输入字符流)和Writer(输出字符流),所有字符流实现类均继承自这两个抽象类。
1. 核心父类:Reader 与 Writer 的抽象定义
字符流的父类与字节流结构相似,但方法参数和返回值以char(字符)和char[](字符数组)为主,内置编码转换逻辑。
(1)Reader:输入字符流的 “行为规范”
Reader是所有输入字符流的父类,核心抽象方法与常用功能如下:
方法签名 | 核心作用 | 底层逻辑 |
abstract int read() | 读取 1 个字符,返回字符的 Unicode 值(0~65535);末尾返回 - 1 | 从底层字节流读取字节,通过编码表转换为字符(如 UTF-8 编码:1~4 字节对应 1 个字符) |
int read(char[] cbuf) | 读取多个字符到字符数组cbuf,返回实际读取的字符数;末尾返回 - 1 | 批量读取字符,减少编码转换次数 |
int read(char[] cbuf, int off, int len) | 读取len个字符到数组cbuf,从索引off开始存储 | 灵活的批量读取 |
long skip(long n) | 跳过n个字符,返回实际跳过的字符数 | 移动字符流的 “读取指针”(需先将字节转换为字符,再跳过) |
void close() | 关闭流,释放资源 | 关闭底层字节流,释放编码转换相关的缓冲区 |
关键差异:与InputStream的read()返回 “字节值(0~255)” 不同,Reader的read()返回 “字符的 Unicode 值(0~65535)”,可直接强转为char使用(如char c = (char) reader.read())。
(2)Writer:输出字符流的 “行为规范”
Writer 是所有输出字符流的父类,核心抽象方法与常用功能如下:
方法签名 | 核心作用 | 底层逻辑 |
abstract void write(int c) | 写入 1 个字符(仅取 int 的低 16 位,因为 1 个 char=16 位) | 将字符的 Unicode 编码,通过指定编码表(如 UTF-8)转换为字节数组,再写入底层字节流 |
void write(char[] cbuf) | 将字符数组 cbuf 的所有字符写入 | 批量转换字符为字节,减少编码转换次数 |
void write(char[] cbuf, int off, int len) | 将数组 cbuf 中从 off 开始的 len 个字符写入 | 灵活的批量写入(如只写入字符数组的部分数据) |
void write(String str) | 直接写入字符串(字符流的核心便利) | 先将字符串转换为字符数组,再按字符数组写入逻辑处理 |
void write(String str, int off, int len) | 写入字符串 str 从 off 开始的 len 个字符 | 避免创建完整字符数组,减少内存占用 |
void flush() | 强制刷新缓冲区,将缓冲的字符数据写入底层流 | 若流有缓冲区(如 BufferedWriter),需调用此方法确保字符数据不滞留 |
void close() | 关闭流,释放资源 | 关闭前自动调用 flush (),并释放编码转换缓冲区与底层字节流 |
关键注意点:write(int c) 接收 int 类型参数,但仅使用低 16 位(对应 1 个 char 的长度),例如 write(0x123456) 实际写入的是 0x3456(低 16 位)对应的字符。
2. 字符流的核心:编码转换原理(字节→字符 / 字符→字节)
字符流的本质是 “字节流 + 编码表” 的封装,其核心解决的问题是:如何将底层二进制的字节数据,正确转换为人类可理解的文本字符(输入流),以及如何将文本字符转换为二进制字节数据存储 / 传输(输出流)。
(1)编码转换的核心组件:Charset(字符集)
Java 中通过 java.nio.charset.Charset 类管理编码表,常用字符集包括:
- UTF-8:国际通用字符集,1 个英文占 1 字节,1 个中文占 3 字节,支持所有 Unicode 字符;
- GBK:中文专用字符集,1 个英文占 1 字节,1 个中文占 2 字节,不支持其他语言;
- ISO-8859-1:西欧字符集,仅支持英文等西欧语言,1 个字符占 1 字节,不支持中文(中文会被转为 ?)。
字符流的编码转换逻辑如下:
- 输入字符流(如 InputStreamReader):底层字节流读取字节数组 → 通过指定 Charset 将字节数组解码为字符数组 → 提供给上层读取;
- 输出字符流(如 OutputStreamWriter):上层写入字符 / 字符串 → 通过指定 Charset 将字符数组编码为字节数组 → 底层字节流写入外部载体。
(2)编码转换的常见问题:乱码的根源与解决
乱码的本质是 “编码与解码使用的字符集不统一”,例如:
- 用 GBK 编码写入的文本,用 UTF-8 解码读取 → 中文会显示为乱码(如 “锘胯揪”);
- 用 UTF-8 编码写入的文本,用 ISO-8859-1 解码读取 → 中文会显示为 ?。
解决原则:编码与解码必须使用相同的字符集,且优先使用 UTF-8(跨平台、兼容性强)。
3. 基础实现类:InputStreamReader 与 OutputStreamWriter(字节流→字符流的桥梁)
FileReader 与 FileWriter 是字符流的常用实现,但它们本质是 InputStreamReader 与 OutputStreamWriter 的 “简化版”(默认使用系统编码)。而 InputStreamReader 与 OutputStreamWriter 是真正的 “字节流→字符流桥梁”,支持显式指定字符集,是专业开发的首选。
(1)InputStreamReader:字节输入流→字符输入流的转换
- 核心作用:将底层字节输入流(如 FileInputStream)的字节数据,通过指定字符集解码为字符数据,供上层按字符读取;
- 构造方法:
- InputStreamReader(InputStream in):默认使用系统编码(不推荐,易导致跨平台乱码);
- InputStreamReader(InputStream in, Charset cs):显式指定字符集(推荐,如 Charset.forName("UTF-8"));
- InputStreamReader(InputStream in, String charsetName):通过字符集名称指定(如 "UTF-8")。
实战:指定 UTF-8 编码读取 GBK 文本(模拟乱码与解决)
public class InputStreamReaderDemo {public static void main(String[] args) throws IOException {String gbkFilePath = "gbk_text.txt"; // 用 GBK 编码保存的文本文件(内容:"你好,Java")// 1. 错误示例:用 UTF-8 解码 GBK 文本(导致乱码)try (InputStreamReader isrError = new InputStreamReader(new FileInputStream(gbkFilePath), StandardCharsets.UTF_8)) {char[] buffer = new char[1024];int len = isrError.read(buffer);System.out.println("UTF-8 解码 GBK 文本(乱码):" + new String(buffer, 0, len));// 输出:UTF-8 解码 GBK 文本(乱码):浣犲ソ锛孒ava}// 2. 正确示例:用 GBK 解码 GBK 文本(正常显示)try (InputStreamReader isrCorrect = new InputStreamReader(new FileInputStream(gbkFilePath), "GBK")) {char[] buffer = new char[1024];int len = isrCorrect.read(buffer);System.out.println("GBK 解码 GBK 文本(正常):" + new String(buffer, 0, len));// 输出:GBK 解码 GBK 文本(正常):你好,Java}}
}
(2)OutputStreamWriter:字符输出流→字节输出流的转换
- 核心作用:将上层写入的字符数据,通过指定字符集编码为字节数据,再交给底层字节输出流(如 FileOutputStream)写入外部载体;
- 构造方法:与 InputStreamReader 对应,支持显式指定字符集(推荐 Charset 或字符集名称)。
实战:用 UTF-8 编码写入多语言文本(确保跨平台兼容)
public class OutputStreamWriterDemo {public static void writeMultiLangText(String filePath) throws IOException {// 显式指定 UTF-8 编码,支持中文、英文、日文try (OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(filePath), StandardCharsets.UTF_8)) {osw.write("中文:你好,Java IO 流\n");osw.write("English: Hello, Java IO Stream\n");osw.write("日文:こんにちは、Java IO ストリーム\n");osw.flush(); // 缓冲数据写入,确保即时生效}}public static void main(String[] args) throws IOException {String multiLangPath = "multi_lang.txt";writeMultiLangText(multiLangPath);// 验证读取(同样用 UTF-8 解码)try (InputStreamReader isr = new InputStreamReader(new FileInputStream(multiLangPath), StandardCharsets.UTF_8)) {char[] buffer = new char[1024];int len = isr.read(buffer);System.out.println("读取多语言文本:\n" + new String(buffer, 0, len));}}
}
关键细节:StandardCharsets.UTF_8 是 Java 7+ 提供的常量,比手动写字符串 "UTF-8" 更安全(避免拼写错误,如 "UTF8" 或 "utf-8")。
4. 简化实现类:FileReader 与 FileWriter(默认系统编码的便捷类)
FileReader 继承自 InputStreamReader,FileWriter 继承自 OutputStreamWriter,它们的核心特点是 “默认使用系统编码”,简化了文本文件的读写代码,但存在跨平台乱码风险。
(1)核心局限性
- 编码不可控:默认使用 Charset.defaultCharset()(系统编码),例如 Windows 系统默认 GBK,Linux/macOS 系统默认 UTF-8;
- 乱码风险高:在 Windows 用 FileWriter 写入的文本,复制到 Linux 用 FileReader 读取,会因编码不统一导致乱码。
(2)适用场景
仅适用于 “本地单机、无需跨平台” 的简单文本处理(如临时日志、本地配置文件),专业开发中优先使用 InputStreamReader/OutputStreamWriter 并显式指定字符集。
示例:FileReader 读取本地文本(简单场景)
public class FileReaderDemo {public static void main(String[] args) throws IOException {// 本地临时文本文件(仅在当前系统使用)try (FileReader fr = new FileReader("local_temp.txt");BufferedReader br = new BufferedReader(fr)) { // 结合缓冲流提升效率String line;while ((line = br.readLine()) != null) {System.out.println("本地文本内容:" + line);}}}
}
5. 缓冲优化类:BufferedReader 与 BufferedWriter(字符流的性能加速器)
与字节流的缓冲类类似,BufferedReader 与 BufferedWriter 通过在内存中开辟字符缓冲区,减少编码转换与系统 IO 次数,同时提供了文本处理的便捷方法(如 readLine() 读取整行文本)。
(1)核心原理
- BufferedReader:默认分配 8KB 字符缓冲区,调用 read() 时先从缓冲区读取字符;若缓冲区为空,一次性从底层字符流(如 InputStreamReader)读取批量字符到缓冲区,减少编码转换次数;
- BufferedWriter:默认分配 8KB 字符缓冲区,调用 write() 时先将字符写入缓冲区;若缓冲区满,一次性将字符编码为字节并写入底层流,减少系统 IO 次数。
(2)核心便捷方法
- BufferedReader.readLine():读取整行文本(以 \n、\r\n 或流末尾为换行标识),返回该行字符串(不含换行符);若已到流末尾,返回 null(文本处理的核心高效方法);
- BufferedWriter.newLine():写入与平台无关的换行符(Windows 写入 \r\n,Linux/macOS 写入 \n),避免手动处理跨平台换行问题。
实战:用缓冲字符流处理大文本文件(按行读取并过滤内容)
public class BufferedCharStreamDemo {// 读取大文本文件,过滤包含 "ERROR" 的日志行并保存public static void filterErrorLogs(String sourcePath, String targetPath) throws IOException {try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(sourcePath), StandardCharsets.UTF_8));BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(targetPath), StandardCharsets.UTF_8))) {String line;// 按行读取(效率远高于单字符读取)while ((line = br.readLine()) != null) {// 过滤包含 "ERROR" 的行if (line.contains("ERROR")) {bw.write(line);bw.newLine(); // 跨平台换行}}bw.flush(); // 确保缓冲数据写入目标文件}}public static void main(String[] args) throws IOException {String largeLogPath = "app_large.log"; // 1GB 大日志文件String errorLogPath = "error_only.log";long start = System.currentTimeMillis();filterErrorLogs(largeLogPath, errorLogPath);System.out.println("过滤完成,耗时:" + (System.currentTimeMillis() - start) + "ms");// 输出:过滤完成,耗时:约 2000ms(缓冲流高效处理大文本)}
}
(3)性能优化建议
- 缓冲区大小:默认 8KB 适用于多数文本场景,处理超大型文本(10GB+)可将缓冲区调至 64KB~256KB(如 new BufferedReader(reader, 65536));
- 避免频繁转换:尽量使用 readLine() 按行处理,避免将 char[] 频繁转换为 String(减少内存开销);
- 批量写入:若需写入大量文本,可先拼接为 StringBuilder,再一次性 write()(减少 write() 调用次数)。
四、字节流与字符流的核心区别与选择原则
通过前面的底层解析,我们可以总结出字节流与字符流的核心差异,并明确不同场景下的选择依据。
1. 核心区别对比
对比维度 | 字节流(InputStream/OutputStream) | 字符流(Reader/Writer) |
数据单位 | 字节(byte,8 位) | 字符(char,16 位,Unicode 编码) |
编码处理 | 不处理编码,直接传输字节 | 内置编码转换(需指定字符集) |
核心作用 | 处理所有二进制数据(图片、视频、文本的二进制形式) | 仅处理文本数据(.txt、.log、.properties 等) |
关键方法 | read(byte[])、write(byte[]) | read(char[])、write(char[])、readLine() |
底层依赖 | 直接依赖操作系统的字节 IO 接口 | 依赖字节流 + 字符集编码表 |
2. 选择原则(开发实战指南)
(1)优先判断数据类型:
- 若处理二进制数据(图片、视频、音频、可执行文件)→ 必须用字节流;
- 若处理文本数据(无论何种语言)→ 必须用字符流(避免手动编码转换,减少乱码);
(2)字符流必须显式指定编码:
- 禁止使用 FileReader/FileWriter(默认系统编码),必须用 InputStreamReader/OutputStreamWriter 并指定 UTF-8;
(3)大文件必须用缓冲流:
- 字节流用 BufferedInputStream/BufferedOutputStream;
- 字符流用 BufferedReader/BufferedWriter;
(4)避免混合使用:
- 同一文件不建议同时用字节流和字符流操作(可能导致文件指针混乱,数据读写异常)。
五、总结:IO 流的底层逻辑与实践闭环
Java IO 流的设计本质是 “分层封装”:
- 底层:字节流直接操作二进制数据,对接操作系统 IO 接口,是所有 IO 操作的基础;
- 上层:字符流封装字节流 + 编码表,解决文本处理的语义化问题;
- 优化层:缓冲流通过内存缓冲区,减少系统 IO 与编码转换次数,提升性能。
掌握 IO 流的核心在于:
- 明确数据类型:二进制用字节流,文本用字符流;
- 控制编码统一:字符流必须显式指定 UTF-8,避免乱码;
- 优先使用缓冲:大文件 / 频繁读写场景,缓冲流是性能关键;
- 规范资源管理:始终用 try-with-resources 自动关闭流,避免资源泄漏。
通过本文的底层解析与实战案例,希望你能跳出 “API 调用” 的表层认知,建立起 IO 流的 “原理→实践→优化” 完整认知体系,在实际开发中能根据场景灵活选择流的组合,写出高效、健壮的 IO 操作代码。