IO流概述
白歌(架构师):"小崔,想象一下水管里的水流。数据就像水,从源头到目的地,流是它的载体。Java IO的设计灵感正来自于此——把数据传输抽象为'流'。"
一、什么是流
流(Stream) 是Java中对数据传输的抽象。它将数据的源(Source)和目的地(Sink)解耦,无论数据来自文件、网络Socket、内存数组还是键盘输入,程序都可以用统一的方式读写数据。
流的本质:
- 流不是容器(不同于数组或集合),不存储数据本身
- 流是单向的、顺序的数据通道——一旦数据流过,就无法回溯
- 流的两端可以是对称的(文件到文件),也可以是不对称的(键盘到文件)
二、流的分类体系
Java IO流从三个维度进行分类:
分类定义速查表
| 分类维度 | 类型 | 说明 | 典型类 |
|---|---|---|---|
| 数据流向 | 输入流 | 从外部设备读取数据到程序 | InputStream, Reader 的子类 |
| 数据流向 | 输出流 | 从程序写出数据到外部设备 | OutputStream, Writer 的子类 |
| 处理单位 | 字节流 | 以字节(8 bit)为单位处理,适合所有数据类型 | FileInputStream, FileOutputStream |
| 处理单位 | 字符流 | 以字符(16 bit)为单位处理,只适合文本数据 | FileReader, FileWriter |
| 功能角色 | 节点流 | 直接与数据源/目的地相连 | FileInputStream, FileReader |
| 功能角色 | 处理流 | 包装已有流对象,增强功能 | BufferedInputStream, InputStreamReader |
三、四个抽象基类
Java IO的整个类体系构建在四个抽象基类之上:
核心方法速查
| 基类 | 核心方法 | 返回值 | 说明 |
|---|---|---|---|
| InputStream | int read() | 0~255的字节值,返回-1表示结束 | 每次读取一个字节,效率低 |
| InputStream | int read(byte[] b) | 实际读取的字节数,返回-1表示结束 | 批量读取到字节数组 |
| OutputStream | void write(int b) | 无 | 写出单个字节的低8位 |
| OutputStream | void write(byte[] b) | 无 | 写出整个字节数组 |
| OutputStream | void flush() | 无 | 强制将缓冲区数据写出 |
| Reader | int read() | 0~65535的字符值,返回-1表示结束 | 每次读取一个字符 |
| Reader | int read(char[] cbuf) | 实际读取的字符数 | 批量读取到字符数组 |
| Writer | void write(String str) | 无 | 写出字符串(字符流独有) |
关键区别:Writer 可以直接
write(String),而 OutputStream 没有此方法——这体现了字符流对文本处理的天然优势。
四、节点流 vs 处理流 —— 装饰器模式的体现
4.1 节点流
节点流是直接与数据源/目的地连接的"裸流"。它们提供了最基本的读写能力,但没有缓冲、编码转换等高级功能。
| 节点流 | 对应基类 | 数据源/目的地 |
|---|---|---|
FileInputStream | InputStream | 文件(字节) |
FileOutputStream | OutputStream | 文件(字节) |
FileReader | Reader | 文件(字符) |
FileWriter | Writer | 文件(字符) |
ByteArrayInputStream | InputStream | 内存字节数组 |
ByteArrayOutputStream | OutputStream | 内存字节数组 |
4.2 处理流 —— 装饰器模式的经典应用
处理流不直接连接数据源,而是包装(装饰) 已有的流对象,在原有功能上叠加新能力。这完美体现了装饰器设计模式。
核心关系:所有处理流都同时继承基类并持有基类的引用——这是装饰器模式的标志性结构。
// 装饰器模式的结构特征
public class BufferedInputStream extends FilterInputStream {
// FilterInputStream 继承 InputStream 并持有 InputStream 引用
// 这就是"自己也是流,内部又包装了一个流"
}
| 处理流 | 装饰后增强的能力 |
|---|---|
BufferedInputStream | 添加内部缓冲区,减少物理读取次数 |
BufferedReader | 添加内部缓冲 + readLine() 按行读取 |
InputStreamReader | 将字节流转换为字符流(桥梁) |
DataInputStream | 按Java基本数据类型读取 |
ObjectInputStream | 反序列化Java对象 |
PrintWriter | 添加 print() / println() 方法 |
五、IO流家族全景图
记忆技巧:字节流以
Stream结尾,字符流以Reader/Writer结尾。凡是带Buffered的都是处理流,凡是带File的大多是节点流。
六、完整示例:飞翔科技员工数据读取
场景描述
飞翔科技技术部小崔需要读取存储在 employees.txt 中的员工信息。文件内容如下:
大翔|技术总监|38
白歌|架构师|32
小崔|初级后端|24
李眉|高级后端|29
示例一:用字节流读取(偏底层)
import java.io.FileInputStream;
import java.io.IOException;
public class EmployeeByteReader {
public static void main(String[] args) {
// 使用 try-with-resources 自动关闭流(JDK 7+)
try (FileInputStream fis = new FileInputStream("C:/data/employees.txt")) {
byte[] buffer = new byte[1024];
int len;
StringBuilder sb = new StringBuilder();
while ((len = fis.read(buffer)) != -1) {
// 将字节数组转为字符串
sb.append(new String(buffer, 0, len));
}
System.out.println("=== 飞翔科技员工列表(字节流读取)===");
System.out.println(sb.toString());
} catch (IOException e) {
System.err.println("读取员工文件失败:" + e.getMessage());
}
}
}
运行输出:
=== 飞翔科技员工列表(字节流读取)===
大翔|技术总监|38
白歌|架构师|32
小崔|初级后端|24
李眉|高级后端|29
注意:字节流按字节读取,对纯英文/数字文本没有问题,但遇到中文字符时如果拆得不巧会出现乱码——这是因为一个中文字符占多个字节。
示例二:用处理流组合装饰
import java.io.*;
public class EmployeeBufferedReader {
public static void main(String[] args) {
// 装饰器模式:FileInputStream -> InputStreamReader -> BufferedReader
// FileInputStream: 节点流,连接文件
// InputStreamReader: 处理流,字节->字符(桥梁)
// BufferedReader: 处理流,添加缓冲和按行读取
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream("C:/data/employees.txt"), "UTF-8"))) {
String line;
System.out.println("=== 飞翔科技员工列表(装饰器链读取)===");
while ((line = br.readLine()) != null) {
String[] parts = line.split("\\|");
System.out.printf("姓名:%-6s 职位:%-8s 年龄:%s%n",
parts[0], parts[1], parts[2]);
}
} catch (IOException e) {
System.err.println("读取员工文件失败:" + e.getMessage());
}
}
}
运行输出:
=== 飞翔科技员工列表(装饰器链读取)===
姓名:大翔 职位:技术总监 年龄:38
姓名:白歌 职位:架构师 年龄:32
姓名:小崔 职位:初级后端 年龄:24
姓名:李眉 职位:高级后端 年龄:29
七、易错场景
反例一:流未关闭导致资源泄漏
小崔最初写文件操作时忘了关闭流:
// ❌ 错误:流未关闭,文件句柄泄漏
FileInputStream fis = new FileInputStream("data.txt");
byte[] data = new byte[1024];
fis.read(data);
// 没有 fis.close(); —— 文件句柄一直被占用
// 多次调用后可能报错:Too many open files
后果:操作系统对单个进程可打开的文件句柄数有限制(Linux通常为1024),泄漏会导致"Too many open files"错误,甚至整个服务不可用。
反例二:在 finally 中关闭流但忽略了空指针和异常
// ❌ 错误:close() 在 finally 中但没处理 null 和异常
FileInputStream fis = null;
try {
fis = new FileInputStream("data.txt");
// ...操作
} catch (IOException e) {
e.printStackTrace();
} finally {
fis.close(); // 如果 fis 为 null 或 close() 本身抛异常呢?
}
正确做法:try-with-resources(JDK 7+)
// ✅ 正确:try-with-resources 自动关闭,代码简洁安全
try (FileInputStream fis = new FileInputStream("data.txt");
BufferedInputStream bis = new BufferedInputStream(fis)) {
byte[] data = new byte[1024];
int len;
while ((len = bis.read(data)) != -1) {
System.out.write(data, 0, len);
}
} catch (IOException e) {
System.err.println("IO操作失败:" + e.getMessage());
}
// try 块结束时,bis.close() 和 fis.close() 自动按逆序调用
原理:
try-with-resources要求资源对象实现AutoCloseable接口。所有 IO 流都实现了此接口。退出 try 块时,JVM 自动按声明的逆序调用close(),并抑制次要异常。
八、面试考点
Q1:字节流和字符流有什么区别?什么时候用哪个?
字节流以字节(8 bit)为单位处理,可以处理任意数据类型(文本、图片、视频、音频),但处理文本时需要程序员自行管理字符编码。字符流以字符(16 bit Unicode)为单位,自动处理编码转换,只能处理文本数据。
选择原则:凡是处理人类可读的文本(JSON、XML、日志、源代码),用字符流;凡是处理二进制数据(图片、PDF、视频、加密文件),用字节流。
Q2:节点流和处理流的区别是什么?
节点流是直接与数据源/目的地连接的"底层流",如
FileInputStream直接操作文件。处理流通过包装(装饰)已有流对象来增强功能,如BufferedInputStream在普通流之上添加缓冲能力。判断标准:
new FileInputStream("a.txt")接受字符串路径的是节点流;new BufferedInputStream(另一个Stream对象)接受流对象的是处理流。
Q3:什么是装饰器模式?Java IO如何体现?
装饰器模式是一种结构型设计模式,允许在不修改原有类的情况下,动态地给对象添加新功能。Java IO 中,
FilterInputStream/FilterOutputStream/FilterReader/FilterWriter是装饰器的基类——它们同时继承基类(is-a 关系)并持有基类引用(has-a 关系)。通过层层嵌套,可以灵活组合功能:new DataInputStream(new BufferedInputStream(new FileInputStream("data.bin"))) // 从底层到高层:文件读取 -> 缓冲加速 -> 按数据类型解析
Q4:InputStream的read()返回int而非byte,为什么?
因为
byte的取值范围是-128 ~ 127,而read()需要用-1表示流结束。如果返回byte,无法区分"读到了值为255(转为byte是-1)的字节"和"流已结束"。因此返回值设计为int,用-1专门表示流结束,0~255表示有效字节数据。
大翔:"把流的概念搞清楚了,后面的具体类就一通百通了。下面我们从最直接的字节流开始,看看怎么读写二进制文件。"