一、IO 流编程进阶:字节流、字符流与 NIO 实战

1.1 本章学习目标与重点
💡 掌握 Java IO 流的核心体系(字节流、字符流、缓冲流、转换流)及设计模式
💡 深入理解字节流与字符流的本质差异,熟练处理文件、网络等场景的 IO 操作
💡 精通 NIO(New IO)的核心组件(Channel、Buffer、Selector)及非阻塞 IO 原理
💡 掌握 IO 流性能优化技巧,解决大文件读写、并发 IO 等实际开发问题
⚠️ 重点关注流的关闭机制、编码问题、NIO 非阻塞模型等高频面试考点
✅ 最终目标:能够根据业务场景灵活选择 IO 方案,实现高效、安全的 IO 操作,应对大文件、高并发等复杂场景
1.2 Java IO 核心概念与体系结构
在深入学习具体 IO 流实现之前,我们先明确 IO 的核心定义、设计思想与整体体系,为后续学习奠定基础。
1.2.1 IO 的核心定义
IO(Input/Output)即输入/输出,是程序与外部设备(如文件、网络、键盘、显示器)之间的数据传输过程:
- 输入流(Input Stream):数据从外部设备流入程序(如读取文件内容到程序);
- 输出流(Output Stream):数据从程序流出到外部设备(如将程序数据写入文件)。
💡 笔记:Java IO 设计的核心思想是“流”——数据以字节或字符为单位,像水流一样连续传输,程序通过操作流来实现数据的读写,无需关注底层设备的具体实现。
1.2.2 IO 流的核心设计模式
Java IO 流基于两大设计模式,保证了体系的灵活性和扩展性:
-
装饰器模式(Decorator Pattern):
- 核心:以“基础流 + 装饰流”的方式组合功能,基础流提供核心读写能力,装饰流增强功能(如缓冲、编码转换);
- 示例:
FileInputStream(基础流)+BufferedInputStream(缓冲装饰流),通过装饰流为基础流添加缓冲功能,提升读写效率; - 优势:无需修改原有类,即可动态扩展功能,符合“开闭原则”。
-
适配器模式(Adapter Pattern):
- 核心:将一个类的接口转换为另一个类的接口,使不兼容的类能够一起工作;
- 示例:
InputStreamReader(转换流)将字节流接口适配为字符流接口,解决字节流与字符流的转换问题; - 优势:解决不同接口之间的兼容性问题,简化开发。
1.2.3 Java IO 流体系结构总览
Java IO 流分为四大类,基于“字节/字符”和“输入/输出”两个维度划分,核心体系如下:
| 类型 | 输入流(读数据) | 输出流(写数据) | 核心用途 |
|---|---|---|---|
| 字节流 | InputStream(抽象基类) | OutputStream(抽象基类) | 读写二进制数据(文件、图片、视频) |
| 字符流 | Reader(抽象基类) | Writer(抽象基类) | 读写文本数据(字符串、配置文件) |
| 缓冲流 | BufferedInputStream/BufferedReader | BufferedOutputStream/BufferedWriter | 增强缓冲,提升读写效率 |
| 转换流 | InputStreamReader | OutputStreamWriter | 字节流与字符流的转换 |
| 文件流 | FileInputStream/FileReader | FileOutputStream/FileWriter | 读写本地文件 |
| 数组流 | ByteArrayInputStream/StringReader | ByteArrayOutputStream/StringWriter | 读写内存中的数组/字符串 |
| 打印流 | - | PrintStream/PrintWriter | 格式化输出(如 System.out) |
| 对象流 | ObjectInputStream | ObjectOutputStream | 对象的序列化与反序列化 |
💡 核心继承关系:
- 字节流:
InputStream→ 所有字节输入流的父类;OutputStream→ 所有字节输出流的父类; - 字符流:
Reader→ 所有字符输入流的父类;Writer→ 所有字符输出流的父类; - 所有流类均实现
Closeable接口,需手动关闭流资源(释放文件句柄、网络连接等)。
1.3 字节流深度解析:二进制数据的读写
字节流是 Java IO 的基础,以字节(byte)为单位读写数据,适用于所有二进制数据(如文件、图片、视频、网络数据包)。本节重点解析核心字节流的实现与实战。
1.3.1 字节流核心基类:InputStream 与 OutputStream
1. InputStream(字节输入流基类)
InputStream 是抽象类,定义了字节输入流的核心方法,所有字节输入流均继承自它:
public abstract class InputStream implements Closeable {
// 读取一个字节(返回值:0~255,若到达流末尾返回 -1)
public abstract int read() throws IOException;
// 读取多个字节到字节数组(返回值:实际读取的字节数,流末尾返回 -1)
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
// 读取多个字节到字节数组的指定范围(offset:起始索引,length:读取长度)
public int read(byte b[], int offset, int length) throws IOException {
// 抽象方法的默认实现,子类可重写
}
// 跳过指定字节数(返回值:实际跳过的字节数)
public long skip(long n) throws IOException {
// 默认实现,子类可重写
}
// 估计剩余可读取的字节数
public int available() throws IOException {
return 0;
}
// 关闭流(释放资源,必须调用)
public void close() throws IOException {}
// 标记流的当前位置(仅部分子类支持,如 BufferedInputStream)
public synchronized void mark(int readlimit) {}
// 重置流到上次标记的位置
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
// 判断流是否支持 mark/reset 操作
public boolean markSupported() {
return false;
}
}
💡 核心方法说明:
read():读取单个字节,返回值为 0~255 的整数(字节的无符号表示),流末尾返回 -1;read(byte[] b):批量读取字节到数组,减少 IO 次数,提升效率(推荐使用);close():必须调用,否则会导致资源泄漏(如文件句柄未释放)。
2. OutputStream(字节输出流基类)
OutputStream 也是抽象类,定义了字节输出流的核心方法:
public abstract class OutputStream implements Closeable, Flushable {
// 写入一个字节(参数 b:仅低 8 位有效,高 24 位被忽略)
public abstract void write(int b) throws IOException;
// 写入字节数组的所有字节
public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}
// 写入字节数组的指定范围
public void write(byte b[], int offset, int length) throws IOException {
// 抽象方法的默认实现,子类可重写
}
// 刷新流(将缓冲区的数据强制写入目标设备)
public void flush() throws IOException {}
// 关闭流(关闭前会自动调用 flush())
public void close() throws IOException {}
}
💡 核心方法说明:
write(int b):写入单个字节,参数需传入 0~255 的整数(若传入字符,需强转为 byte);flush():缓冲流必须调用,否则缓冲区数据可能未写入目标设备(如 BufferedOutputStream);close():关闭流时会自动刷新缓冲区,因此无需手动调用 flush() 后再关闭。
1.3.2 核心字节流实现类实战
1. FileInputStream/FileOutputStream:文件字节流
专门用于读写本地文件的字节流,是最常用的字节流实现类。
(1)FileInputStream:读取文件
/**
* 示例 1:使用 FileInputStream 读取文件(单个字节读取,效率低)
*/
public class FileInputStreamDemo1 {
public static void main(String[] args) {
FileInputStream fis = null;
try {
// 1. 创建 FileInputStream 对象,指定文件路径(相对路径或绝对路径)
fis = new FileInputStream("test.txt");
// 2. 读取文件(单个字节读取)
int data;
System.out.println("文件内容(单个字节读取):");
while ((data = fis.read()) != -1) {
// 字节转换为字符(默认使用系统默认编码)
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 3. 关闭流(必须在 finally 中执行,确保无论是否异常都能关闭)
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
(2)批量读取优化(推荐)
单个字节读取效率极低(频繁 IO 操作),实际开发中优先使用批量读取:
/**
* 示例 2:FileInputStream 批量读取文件(效率高)
*/
public class FileInputStreamDemo2 {
public static void main(String[] args) {
// try-with-resources 语法(Java 7+):自动关闭实现 Closeable 接口的资源,无需手动 close()
try (FileInputStream fis = new FileInputStream("test.txt")) {
byte[] buffer = new byte[1024]; // 缓冲区大小(1KB,可根据文件大小调整)
int len; // 实际读取的字节数
System.out.println("文件内容(批量读取):");
while ((len = fis.read(buffer)) != -1) {
// 字节数组转换为字符串(需指定编码,避免乱码)
System.out.print(new String(buffer, 0, len, StandardCharsets.UTF_8));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
💡 关键优化:
- 使用
byte[] buffer批量读取,减少 IO 次数(缓冲区越大,效率越高,但占用内存越多,推荐 1KB~8KB); - 使用
try-with-resources语法,自动关闭流资源,简化代码并避免资源泄漏; - 转换字符串时指定编码(如 UTF-8),避免系统默认编码导致的乱码问题。
(3)FileOutputStream:写入文件
/**
* 示例 3:FileOutputStream 写入文件(覆盖写入/追加写入)
*/
public class FileOutputStreamDemo {
public static void main(String[] args) {
// 追加写入:第二个参数传 true,默认 false(覆盖写入)
try (FileOutputStream fos = new FileOutputStream("output.txt", true)) {
String content = "Hello, FileOutputStream!\n";
// 字符串转换为字节数组(指定 UTF-8 编码)
byte[] data = content.getBytes(StandardCharsets.UTF_8);
// 批量写入字节数组
fos.write(data);
System.out.println("写入成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
⚠️ 注意事项:
- 若文件不存在,
FileOutputStream会自动创建文件(但不会创建父目录,需手动创建); - 覆盖写入:默认情况下,写入会覆盖文件原有内容,若需追加,需在构造函数中传入
true; - 写入后无需手动调用
flush(),因为close()会自动刷新,但若需立即写入(如日志输出),可手动调用。
2. BufferedInputStream/BufferedOutputStream:缓冲字节流
缓冲流是装饰流,通过内置缓冲区(默认 8KB)减少 IO 次数,提升读写效率(核心优化手段)。
(1)缓冲流的工作原理
- 读取时:缓冲流先从目标设备读取数据到缓冲区,程序从缓冲区读取数据,避免每次读取都触发底层 IO;
- 写入时:程序先将数据写入缓冲区,缓冲区满后自动写入目标设备,也可通过
flush()手动触发写入。
(2)缓冲流实战(大文件读写优化)
/**
* 示例 4:缓冲流读写大文件(推荐实战方案)
*/
public class BufferedStreamDemo {
public static void main(String[] args) {
// 记录耗时
long start = System.currentTimeMillis();
// 缓冲流包装文件流(装饰器模式)
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("large_file.zip"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy_large_file.zip"))) {
byte[] buffer = new byte[8192]; // 8KB 缓冲区(与默认缓冲区大小一致,可调整)
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
long end = System.currentTimeMillis();
System.out.println("大文件复制完成!耗时:" + (end - start) + "ms");
} catch (IOException e) {
e.printStackTrace();
}
}
}
💡 性能对比:
- 无缓冲流:复制 100MB 文件约耗时 500ms;
- 有缓冲流:复制 100MB 文件约耗时 80ms,效率提升 6 倍以上;
- 结论:读写文件时,必须使用缓冲流包装基础流,这是 IO 性能优化的核心手段。
3. ByteArrayInputStream/ByteArrayOutputStream:内存字节流
内存流以内存中的字节数组为读写目标,无需外部设备,读写速度极快,适用于临时数据处理(如数据转换、缓存)。
(1)ByteArrayInputStream:读取内存数组
/**
* 示例 5:ByteArrayInputStream 读取内存字节数组
*/
public class ByteArrayInputStreamDemo {
public static void main(String[] args) {
byte[] data = "Hello, ByteArrayInputStream!".getBytes(StandardCharsets.UTF_8);
// 从字节数组读取数据
try (ByteArrayInputStream bais = new ByteArrayInputStream(data)) {
byte[] buffer = new byte[10];
int len;
System.out.println("读取内存数据:");
while ((len = bais.read(buffer)) != -1) {
System.out.print(new String(buffer, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
(2)ByteArrayOutputStream:写入内存数组
/**
* 示例 6:ByteArrayOutputStream 写入内存字节数组(无大小限制)
*/
public class ByteArrayOutputStreamDemo {
public static void main(String[] args) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
// 写入多个数据
baos.write("Hello, ".getBytes());
baos.write("ByteArrayOutputStream!".getBytes());
baos.write(65); // 写入单个字节(ASCII 65 = 'A')
// 获取写入的字节数组
byte[] result = baos.toByteArray();
System.out.println("内存中的数据:" + new String(result, StandardCharsets.UTF_8));
// 输出:Hello, ByteArrayOutputStream!A
} catch (IOException e) {
e.printStackTrace();
}
}
}
💡 核心优势:
- 无大小限制:
ByteArrayOutputStream会自动扩容,无需指定初始大小; - 速度极快:读写均在内存中进行,无磁盘 IO 开销;
- 适用场景:数据转换(如字符串 → 字节数组)、临时缓存、网络数据处理(如 HTTP 响应数据)。
4. ObjectInputStream/ObjectOutputStream:对象流(序列化)
对象流用于实现对象的序列化(将对象转换为字节流)和反序列化(将字节流恢复为对象),适用于对象的持久化存储(如写入文件)或网络传输。
(1)序列化的核心要求
- 对象所属类必须实现
Serializable接口(标记接口,无抽象方法); - 类中的所有属性必须可序列化(基本类型默认可序列化,引用类型需也实现
Serializable); - 若属性无需序列化,可使用
transient关键字修饰(如密码、敏感数据)。
(2)对象流实战
import java.io.*;
import java.util.Date;
// 序列化实体类(必须实现 Serializable 接口)
class User implements Serializable {
// 序列化版本号(建议显式声明,避免类结构变化导致反序列化失败)
private static final long serialVersionUID = 1L;
private Long id;
private String name;
private int age;
private transient String password; // transient 修饰,不参与序列化
private Date createTime;
// 构造函数、getter、setter、toString() 省略
public User(Long id, String name, int age, String password) {
this.id = id;
this.name = name;
this.age = age;
this.password = password;
this.createTime = new Date();
}
}
/**
* 示例 7:对象的序列化与反序列化
*/
public class ObjectStreamDemo {
public static void main(String[] args) {
// 1. 对象序列化(写入文件)
User user = new User(1L, "张三", 25, "123456");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
oos.writeObject(user);
System.out.println("对象序列化成功!");
} catch (IOException e) {
e.printStackTrace();
}
// 2. 对象反序列化(从文件读取)
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
User deserializedUser = (User) ois.readObject();
System.out.println("对象反序列化成功!");
System.out.println("反序列化结果:" + deserializedUser);
System.out.println("密码(transient 修饰):" + deserializedUser.getPassword()); // 输出 null
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
⚠️ 注意事项:
- 序列化版本号(
serialVersionUID):显式声明后,即使类结构变化(如新增字段),仍可反序列化旧版本对象;若未声明,Java 会自动生成,类结构变化后会导致反序列化失败; transient关键字:修饰的属性不参与序列化,反序列化后值为默认值(null 或 0);- 静态字段:不参与序列化(序列化的是对象实例,而非类);
- 反序列化:无需调用构造函数,直接通过字节流恢复对象状态。
1.3.3 字节流常见问题与解决方案
1. 问题 1:流未关闭导致资源泄漏
💡 解决方案:
- 优先使用
try-with-resources语法(Java 7+),自动关闭流; - 若使用 Java 7 之前版本,必须在
finally中关闭流,且需判断流不为 null; - 错误示例(未关闭流):
// 错误
FileInputStream fis = new FileInputStream("test.txt");
fis.read();
// 未调用 fis.close(),导致文件句柄泄漏
- 正确示例:
// 正确(try-with-resources)
try (FileInputStream fis = new FileInputStream("test.txt")) {
fis.read();
} catch (IOException e) {
e.printStackTrace();
}
2. 问题 2:读取大文件时内存溢出
💡 解决方案:
- 使用批量读取(
byte[] buffer),避免一次性读取整个文件到内存; - 缓冲区大小建议 1KB~8KB(平衡内存占用与 IO 效率);
- 错误示例(一次性读取大文件):
// 错误(大文件会导致 OOM)
byte[] data = new byte[fis.available()];
fis.read(data);
- 正确示例(批量读取):
byte[] buffer = new byte[8192];
int len;
while ((len = fis.read(buffer)) != -1) {
// 处理 buffer 中的数据
}
3. 问题 3:序列化时出现 NotSerializableException
💡 解决方案:
- 确保对象所属类及所有引用属性类均实现
Serializable接口; - 对无需序列化的引用属性,使用
transient修饰; - 检查是否存在不可序列化的属性(如
Thread、Socket等),若有需排除。
1.4 字符流深度解析:文本数据的读写
字符流以字符(char)为单位读写数据,专门用于处理文本数据(如 TXT 文件、配置文件、JSON 字符串),核心优势是自动处理字符编码问题(避免字节流读取文本时的乱码)。
1.4.1 字符流核心基类:Reader 与 Writer
1. Reader(字符输入流基类)
Reader 是抽象类,定义了字符输入流的核心方法,与 InputStream 类似,但以字符为单位:
public abstract class Reader implements Closeable, Readable {
// 读取一个字符(返回值:0~65535,流末尾返回 -1)
public int read() throws IOException {
// 抽象方法的默认实现,子类可重写
}
// 批量读取字符到字符数组
public int read(char cbuf[]) throws IOException {
return read(cbuf, 0, cbuf.length);
}
// 批量读取字符到字符数组的指定范围
abstract public int read(char cbuf[], int offset, int length) throws IOException;
// 跳过指定字符数
public long skip(long n) throws IOException {
// 默认实现,子类可重写
}
// 关闭流
abstract public void close() throws IOException;
// 其他方法(mark、reset 等,与 InputStream 类似)
}
💡 核心差异:
- 字节流读取
byte(8 位),字符流读取char(16 位,Unicode 编码); - 字符流读取时会自动根据编码将字节转换为字符,避免乱码。
2. Writer(字符输出流基类)
Writer 是抽象类,定义了字符输出流的核心方法:
public abstract class Writer implements Closeable, Flushable {
// 写入一个字符
public void write(int c) throws IOException {
// 抽象方法的默认实现,子类可重写
}
// 写入字符数组
public void write(char cbuf[]) throws IOException {
write(cbuf, 0, cbuf.length);
}
// 写入字符数组的指定范围
abstract public void write(char cbuf[], int offset, int length) throws IOException;
// 写入字符串
public void write(String str) throws IOException {
write(str, 0, str.length());
}
// 写入字符串的指定范围
public void write(String str, int offset, int length) throws IOException {
// 默认实现,子类可重写
}
// 刷新缓冲区
abstract public void flush() throws IOException;
// 关闭流
abstract public void close() throws IOException;
}
💡 核心优势:
- 直接支持字符串写入(
write(String str)),无需手动转换为字节数组; - 自动处理字符编码,写入时将字符转换为指定编码的字节流。
1.4.2 核心字符流实现类实战
1. FileReader/FileWriter:文件字符流
专门用于读写文本文件的字符流,简化了文本读写操作(默认使用系统编码,建议显式指定编码)。
(1)FileReader 读取文本文件
/**
* 示例 8:FileReader 读取文本文件(指定 UTF-8 编码)
*/
public class FileReaderDemo {
public static void main(String[] args) {
// 注意:FileReader 无法直接指定编码,需通过 InputStreamReader 包装(推荐)
try (InputStreamReader isr = new InputStreamReader(
new FileInputStream("test.txt"), StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr)) {
String line;
System.out.println("文本文件内容:");
// 按行读取(BufferedReader 的特有方法,效率高)
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
💡 关键说明:
FileReader无法直接指定编码,需通过InputStreamReader包装(转换流),显式指定编码(如 UTF-8);BufferedReader的readLine()方法按行读取文本,是处理文本文件的常用方法(避免逐字符读取)。
(2)FileWriter 写入文本文件
/**
* 示例 9:FileWriter 写入文本文件(追加写入 + UTF-8 编码)
*/
public class FileWriterDemo {
public static void main(String[] args) {
// FileWriter 无法直接指定编码,需通过 OutputStreamWriter 包装
try (OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream("output.txt", true), StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw)) {
// 写入字符串
bw.write("Hello, FileWriter!");
bw.newLine(); // 写入换行符(跨平台兼容,Windows \r\n,Linux \n)
bw.write("这是中文内容,指定 UTF-8 编码避免乱码");
bw.flush(); // 缓冲流需手动刷新(或关闭时自动刷新)
System.out.println("文本写入成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
⚠️ 注意事项:
- 写入中文时必须指定编码(如 UTF-8),否则会使用系统默认编码(如 Windows 的 GBK),导致其他平台读取乱码;
BufferedWriter的newLine()方法会根据操作系统自动生成换行符,跨平台兼容(推荐使用)。
2. BufferedReader/BufferedWriter:缓冲字符流
与缓冲字节流类似,缓冲字符流通过内置缓冲区提升文本读写效率,同时提供了按行读取(readLine())等便捷方法。
(1)缓冲字符流实战(文本文件复制)
/**
* 示例 10:缓冲字符流复制文本文件(保留格式 + 无乱码)
*/
public class BufferedCharStreamDemo {
public static void main(String[] args) {
long start = System.currentTimeMillis();
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream("source.txt"), StandardCharsets.UTF_8));
BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream("target.txt"), StandardCharsets.UTF_8))) {
String line;
// 按行读取并写入(保留换行符)
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine(); // 写入换行符
}
long end = System.currentTimeMillis();
System.out.println("文本文件复制完成!耗时:" + (end - start) + "ms");
} catch (IOException e) {
e.printStackTrace();
}
}
}
💡 性能优势:
- 按行读取:
readLine()方法批量读取一行文本,效率远高于逐字符读取; - 缓冲优化:减少磁盘 IO 次数,复制大文本文件时效率提升明显;
- 编码安全:通过转换流指定编码,避免乱码。
3. InputStreamReader/OutputStreamWriter:转换流
转换流是字节流与字符流的桥梁,核心功能是实现字节流到字符流的转换(处理编码):
InputStreamReader:将字节输入流转换为字符输入流(读数据时,字节 → 字符);OutputStreamWriter:将字符输出流转换为字节输出流(写数据时,字符 → 字节)。
(1)转换流的核心作用
解决字节流读取文本时的编码问题,例如:
- 用字节流读取 UTF-8 编码的文本文件,直接转换为字符串会乱码;
- 通过
InputStreamReader指定 UTF-8 编码,自动将字节转换为正确的字符。
(2)转换流实战(处理网络字节流为文本)
import java.io.*;
import java.net.Socket;
/**
* 示例 11:转换流处理网络字节流(Socket 读取 HTTP 响应文本)
*/
public class ConvertStreamDemo {
public static void main(String[] args) {
try (Socket socket = new Socket("www.baidu.com", 80);
OutputStream os = socket.getOutputStream();
InputStream is = socket.getInputStream();
// 字节流转换为字符流(指定 UTF-8 编码)
InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr)) {
// 发送 HTTP 请求(字节流写入)
String request = "GET / HTTP/1.1\r\n" +
"Host: www.baidu.com\r\n" +
"Connection: close\r\n\r\n";
os.write(request.getBytes());
os.flush();
// 读取 HTTP 响应(字符流读取,无乱码)
String line;
System.out.println("HTTP 响应内容:");
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
💡 核心场景:
- 网络通信:网络传输的是字节流,需转换为字符流处理文本数据(如 HTTP、TCP 协议);
- 文件读写:字节流读取文本文件时,通过转换流指定编码,避免乱码;
- 设备输入:如
System.in(字节流)转换为字符流,读取键盘输入。
4. StringReader/StringWriter:字符串字符流
与字节流的 ByteArrayInputStream/ByteArrayOutputStream 类似,字符串流以字符串为读写目标,适用于内存中文本数据的处理。
(1)StringReader:读取字符串
/**
* 示例 12:StringReader 读取字符串
*/
public class StringReaderDemo {
public static void main(String[] args) {
String content = "Hello, StringReader!\n这是第二行文本。";
try (StringReader sr = new StringReader(content)) {
char[] buffer = new char[20];
int len;
System.out.println("读取字符串内容:");
while ((len = sr.read(buffer)) != -1) {
System.out.print(new String(buffer, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
(2)StringWriter:写入字符串
/**
* 示例 13:StringWriter 写入字符串(拼接文本)
*/
public class StringWriterDemo {
public static void main(String[] args) {
try (StringWriter sw = new StringWriter()) {
sw.write("姓名:张三\n");
sw.write("年龄:25\n");
sw.write("职业:Java 开发工程师");
// 获取写入的字符串
String result = sw.toString();
System.out.println("拼接后的字符串:");
System.out.println(result);
} catch (IOException e) {
e.printStackTrace();
}
}
}
💡 核心优势:
- 无需处理编码:字符串本身是 Unicode 编码,读写过程无编码转换;
- 高效便捷:适用于文本拼接、模板生成、内存中文本处理等场景;
- 线程安全:
StringWriter的方法均被synchronized修饰,支持多线程写入。
1.4.3 字符流与字节流的选择原则
| 场景 | 推荐流类型 | 原因 |
|---|---|---|
| 读写二进制文件(图片、视频、压缩包) | 字节流 | 二进制数据无编码问题,字节流直接读写 |
| 读写文本文件(TXT、配置文件、JSON) | 字符流 | 自动处理编码,避免乱码,支持按行读取 |
| 网络通信(TCP/UDP 传输文本) | 字符流 | 网络字节流转换为字符流,处理编码 |
| 网络通信(传输二进制数据) | 字节流 | 直接传输字节,无需编码转换 |
| 内存中数据处理(字节数组) | 字节流 | ByteArrayInputStream/ByteArrayOutputStream |
| 内存中数据处理(字符串) | 字符流 | StringReader/StringWriter 高效便捷 |
| 对象序列化/反序列化 | 字节流 | 仅对象流(字节流)支持序列化 |
✅ 核心原则:
- 文本数据(字符相关)→ 字符流(避免乱码,操作便捷);
- 二进制数据(非字符相关)→ 字节流(直接读写,无编码转换)。
1.5 NIO 详解:非阻塞 IO 与高并发处理
Java NIO(New IO,JDK 1.4 引入)是对传统 IO 的升级,核心优势是非阻塞 IO 和 多路复用,适用于高并发、大吞吐量的场景(如服务器、网络框架)。NIO 与传统 IO 的设计思想完全不同,需重点理解其核心组件与工作模式。
1.5.1 NIO 核心组件与核心概念
NIO 由三大核心组件构成:Channel(通道)、Buffer(缓冲区)、Selector(选择器),三者协同工作实现非阻塞 IO。
1. Channel(通道)
- 定义:Channel 是 NIO 中数据传输的通道,类似于传统 IO 中的流,但支持双向传输(既可以读也可以写),且支持非阻塞模式。
- 核心实现类:
FileChannel:文件通道,用于文件的读写(类似文件流);SocketChannel:TCP 客户端通道,用于与服务器通信;ServerSocketChannel:TCP 服务器通道,用于监听客户端连接;DatagramChannel:UDP 通道,用于无连接的数据传输;
- 核心方法:
read(Buffer buf):从通道读取数据到缓冲区;write(Buffer buf):从缓冲区写入数据到通道;configureBlocking(boolean block):设置通道为非阻塞模式(block = false);register(Selector sel, int ops):将通道注册到选择器,监听指定事件(如读、写)。
💡 与传统流的差异:
- 双向性:流是单向的(输入流/输出流),通道是双向的(可读可写);
- 非阻塞:通道支持非阻塞模式,流仅支持阻塞模式;
- 缓冲区:通道必须与缓冲区配合使用(数据先读入缓冲区,再从缓冲区写入通道)。
2. Buffer(缓冲区)
- 定义:Buffer 是 NIO 中用于存储数据的缓冲区(数组封装),所有数据的读写都必须通过缓冲区进行(通道不直接操作数据,仅操作缓冲区)。
- 核心实现类:
ByteBuffer:字节缓冲区(最常用,适用于二进制数据);CharBuffer:字符缓冲区(适用于文本数据);IntBuffer、LongBuffer等:基本类型缓冲区;
- 核心属性(Buffer 的状态由以下三个属性控制):
capacity:缓冲区容量(创建时指定,不可修改);position:当前操作位置(初始为 0,读写时自动递增);limit:缓冲区的限制(读模式下为实际数据长度,写模式下为 capacity);mark:标记位置(用于 reset() 重置 position);
- 核心方法:
put(T data):写入数据到缓冲区(写模式);get():从缓冲区读取数据(读模式);flip():切换为读模式(设置 limit = position,position = 0);rewind():重置 position 为 0(重读缓冲区数据);clear():清空缓冲区(position = 0,limit = capacity,数据未实际删除);compact():压缩缓冲区(保留未读取的数据,移动到缓冲区开头)。
💡 Buffer 读写流程:
- 写模式:创建缓冲区 →
put()写入数据 →flip()切换为读模式; - 读模式:
get()读取数据 → 读取完成后,clear()(清空)或compact()(压缩)→ 切换为写模式。
3. Selector(选择器)
- 定义:Selector 是 NIO 实现多路复用的核心组件,用于监听多个 Channel 的事件(如读就绪、写就绪、连接就绪),一个线程即可管理多个 Channel,提升并发处理能力。
- 核心事件(SelectionKey 定义的事件):
SelectionKey.OP_READ:通道可读事件;SelectionKey.OP_WRITE:通道可写事件;SelectionKey.OP_CONNECT:通道连接就绪事件(客户端 SocketChannel);SelectionKey.OP_ACCEPT:通道接受连接事件(服务器 ServerSocketChannel);
- 核心方法:
open():创建选择器;register(Channel ch, int ops):将通道注册到选择器,监听指定事件;select():阻塞等待事件就绪(返回就绪的事件数);select(long timeout):超时阻塞等待事件就绪;selectedKeys():获取所有就绪的事件(SelectionKey 集合);wakeup():唤醒阻塞在 select() 方法上的线程。
💡 多路复用原理:
- 传统 IO 中,一个线程只能处理一个连接(阻塞),高并发时需要大量线程,资源消耗大;
- NIO 中,一个线程通过 Selector 监听多个 Channel,仅当 Channel 事件就绪时才处理,线程无需阻塞等待,资源消耗低,支持高并发。
1.5.2 NIO 核心组件实战:文件读写
FileChannel 是 NIO 中用于文件读写的通道,配合 ByteBuffer 实现高效的文件操作(性能优于传统 IO 流)。
1. FileChannel 读取文件
/**
* 示例 14:NIO FileChannel 读取文件
*/
public class NIOFileReadDemo {
public static void main(String[] args) {
try (RandomAccessFile raf = new RandomAccessFile("test.txt", "r");
FileChannel channel = raf.getChannel()) {
// 1. 创建 ByteBuffer(容量 1KB)
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 2. 从通道读取数据到缓冲区
int bytesRead;
System.out.println("NIO 读取文件内容:");
while ((bytesRead = channel.read(buffer)) != -1) {
// 3. 切换为读模式(limit = position,position = 0)
buffer.flip();
// 4. 读取缓冲区数据(转换为字符串)
byte[] data = new byte[bytesRead];
buffer.get(data);
System.out.print(new String(data, StandardCharsets.UTF_8));
// 5. 清空缓冲区(切换为写模式,准备下次读取)
buffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
2. FileChannel 写入文件
/**
* 示例 15:NIO FileChannel 写入文件
*/
public class NIOFileWriteDemo {
public static void main(String[] args) {
try (RandomAccessFile raf = new RandomAccessFile("nio_output.txt", "rw");
FileChannel channel = raf.getChannel()) {
// 1. 准备数据,写入 ByteBuffer
String content = "Hello, NIO FileChannel!\n这是 NIO 写入的中文内容。";
ByteBuffer buffer = ByteBuffer.allocate(content.getBytes(StandardCharsets.UTF_8).length);
buffer.put(content.getBytes(StandardCharsets.UTF_8));
// 2. 切换为读模式(通道从缓冲区读取数据)
buffer.flip();
// 3. 从缓冲区写入数据到通道(文件)
channel.write(buffer);
System.out.println("NIO 写入文件成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. FileChannel 复制文件(高效)
/**
* 示例 16:NIO FileChannel 复制大文件(性能最优)
*/
public class NIOFileCopyDemo {
public static void main(String[] args) {
long start = System.currentTimeMillis();
try (FileChannel sourceChannel = new FileInputStream("large_file.zip").getChannel();
FileChannel targetChannel = new FileOutputStream("nio_copy_large_file.zip").getChannel()) {
// 方法 1:直接传输(推荐,底层优化,效率最高)
sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);
// 方法 2:缓冲区传输(手动控制缓冲区)
// ByteBuffer buffer = ByteBuffer.allocateDirect(8192); // 直接缓冲区(堆外内存,效率更高)
// while (sourceChannel.read(buffer) != -1) {
// buffer.flip();
// targetChannel.write(buffer);
// buffer.clear();
// }
long end = System.currentTimeMillis();
System.out.println("NIO 复制大文件完成!耗时:" + (end - start) + "ms");
} catch (IOException e) {
e.printStackTrace();
}
}
}
💡 NIO 文件操作优势:
- 高效传输:
transferTo()/transferFrom()方法直接在通道间传输数据,无需经过用户空间(零拷贝),效率远高于传统 IO; - 直接缓冲区:
ByteBuffer.allocateDirect()创建堆外内存缓冲区,避免 JVM 堆与内核空间的数据拷贝,提升性能; - 非阻塞支持:
FileChannel虽不支持非阻塞模式(文件操作天然阻塞),但其他通道(如 SocketChannel)支持,统一 NIO 编程模型。
1.5.3 NIO 非阻塞模式与 Selector 实战(TCP 服务器)
NIO 的核心价值在于非阻塞模式与 Selector 多路复用,下面通过一个简单的 TCP 服务器示例,演示 NIO 的工作流程。
1. NIO TCP 服务器核心流程
① 创建 ServerSocketChannel,绑定端口,设置为非阻塞模式;
② 创建 Selector,将 ServerSocketChannel 注册到 Selector,监听 ACCEPT 事件;
③ 循环调用 Selector 的 select() 方法,阻塞等待事件就绪;
④ 事件就绪后,遍历 SelectedKeys 集合,处理对应事件:
- ACCEPT 事件:ServerSocketChannel 接受客户端连接,创建 SocketChannel,设置为非阻塞模式,注册到 Selector,监听 READ 事件;
- READ 事件:SocketChannel 读取客户端数据,处理后响应;
⑤ 移除已处理的 SelectionKey,重复循环。
2. 实战代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
/**
* 示例 17:NIO 非阻塞 TCP 服务器(支持多客户端并发连接)
*/
public class NIOServer {
public static void main(String[] args) throws IOException {
// 1. 创建 ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080)); // 绑定 8080 端口
serverChannel.configureBlocking(false); // 设置为非阻塞模式
// 2. 创建 Selector
Selector selector = Selector.open();
// 3. 将 ServerSocketChannel 注册到 Selector,监听 ACCEPT 事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO 服务器启动,监听端口 8080...");
// 4. 循环处理事件
while (true) {
// 阻塞等待事件就绪(返回就绪的事件数)
int readyChannels = selector.select();
if (readyChannels == 0) {
continue; // 无事件就绪,继续等待
}
// 5. 获取就绪的事件集合
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
// 6. 遍历处理每个事件
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 处理 ACCEPT 事件(客户端连接)
if (key.isAcceptable()) {
handleAccept(key, selector);
}
// 处理 READ 事件(客户端发送数据)
if (key.isReadable()) {
handleRead(key);
}
// 移除已处理的事件(避免重复处理)
iterator.remove();
}
}
}
/**
* 处理客户端连接事件
*/
private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
// 接受客户端连接(非阻塞,无连接时返回 null)
SocketChannel clientChannel = serverChannel.accept();
if (clientChannel != null) {
clientChannel.configureBlocking(false); // 设置为非阻塞模式
System.out.println("客户端连接成功:" + clientChannel.getRemoteAddress());
// 注册到 Selector,监听 READ 事件,附带一个缓冲区(用于存储客户端数据)
clientChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
}
/**
* 处理客户端读事件(读取数据并响应)
*/
private static void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment(); // 获取注册时附带的缓冲区
// 读取客户端数据到缓冲区
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
// 客户端断开连接
System.out.println("客户端断开连接:" + clientChannel.getRemoteAddress());
clientChannel.close();
key.cancel();
return;
}
// 切换为读模式,读取数据
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String message = new String(data, StandardCharsets.UTF_8);
System.out.println("收到客户端消息:" + message);
// 响应客户端(发送数据)
String response = "服务器已收到消息:" + message;
ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8));
clientChannel.write(responseBuffer);
// 清空缓冲区,准备下次读取
buffer.clear();
}
}
3. 测试客户端(使用 Telnet 或代码测试)
- 打开命令行,输入
telnet localhost 8080,连接服务器; - 输入任意字符串,服务器会响应并打印消息;
- 可同时打开多个 Telnet 窗口,测试多客户端并发连接(服务器仅用一个线程处理所有客户端)。
💡 NIO 服务器优势:
- 单线程处理多客户端:通过 Selector 多路复用,一个线程即可管理所有客户端连接,无需创建大量线程;
- 非阻塞:线程无需阻塞等待客户端数据,仅在事件就绪时处理,资源消耗低;
- 高并发:支持上万甚至几十万客户端连接(受操作系统文件描述符限制),远超传统 IO 服务器。
1.5.4 NIO 与传统 IO 的核心差异
| 特性 | 传统 IO(BIO) | NIO(New IO) |
|---|---|---|
| 阻塞模式 | 阻塞(线程等待数据/连接) | 非阻塞(线程无需等待,事件驱动) |
| 并发处理 | 一个线程处理一个连接(高资源消耗) | 单线程处理多连接(多路复用) |
| 核心组件 | 流(InputStream/OutputStream) | 通道、缓冲区、选择器 |
| 数据传输方式 | 流直接读写数据 | 通道通过缓冲区传输数据 |
| 适用场景 | 低并发、简单 IO 操作 | 高并发、大吞吐量(服务器、框架) |
| 编程复杂度 | 简单(面向流) | 复杂(面向缓冲区、事件驱动) |
| 性能 | 低(线程切换、阻塞等待开销大) | 高(非阻塞、零拷贝、多路复用) |
✅ 核心结论:
- 简单场景、低并发 → 传统 IO(编程简单,无需理解 NIO 复杂模型);
- 高并发、大吞吐量 → NIO(如服务器、网络框架、中间件);
- 现代 Java 网络框架(如 Netty)均基于 NIO 实现,封装了 NIO 的复杂性,提供更易用的 API。
1.6 IO 性能优化实战技巧
无论是传统 IO 还是 NIO,性能优化都是实际开发中的重点。本节总结常用的 IO 性能优化技巧,覆盖文件读写、网络通信等场景。
1.6.1 传统 IO 性能优化
1. 使用缓冲流包装基础流
- 核心优化:缓冲流减少 IO 次数,提升效率(必须使用);
- 示例:
BufferedInputStream包装FileInputStream,BufferedReader包装FileReader; - 缓冲区大小:默认 8KB,可根据文件大小调整(如大文件使用 16KB~64KB)。
2. 批量读写,避免单字节/单字符操作
- 单字节读写效率极低(频繁触发底层 IO),优先使用
read(byte[]/char[])和write(byte[]/char[]); - 缓冲区大小建议:1KB~8KB(平衡内存占用与 IO 效率)。
3. 正确关闭流资源,避免资源泄漏
- 使用
try-with-resources语法(Java 7+),自动关闭流; - 避免在循环中创建流对象(如每次读写都创建 FileInputStream),应复用流。
4. 字符流指定编码,避免乱码与编码转换开销
- 读写文本时显式指定编码(如 UTF-8),避免系统默认编码导致的乱码;
- 避免频繁的编码转换(如字节 → 字符 → 字节),尽量在 IO 边界完成编码转换。
5. 内存流替代临时文件
- 临时数据处理(如数据转换、拼接)使用
ByteArrayInputStream/ByteArrayOutputStream或StringReader/StringWriter,避免磁盘 IO 开销。
1.6.2 NIO 性能优化
1. 使用直接缓冲区(Direct Buffer)
- 直接缓冲区位于堆外内存,避免 JVM 堆与内核空间的数据拷贝(零拷贝);
- 适用场景:大文件传输、网络通信(频繁读写的场景);
- 注意:直接缓冲区创建和销毁开销大,应复用,避免频繁创建。
2. 合理设置 Selector 超时时间
selector.select(timeout)避免无限阻塞,超时时间建议 100~1000ms,便于处理其他任务(如心跳检测);- 避免使用
selector.selectNow()(非阻塞,CPU 占用高)。
3. 复用缓冲区,避免频繁创建
- 缓冲区创建开销大,应复用(如为每个客户端连接分配一个缓冲区,而非每次读写都创建);
- 可通过
SelectionKey.attachment()为通道绑定缓冲区,实现复用。
4. 避免 Selector 空轮询
- 某些 JDK 版本存在 Selector 空轮询 bug(
select()返回 0 但有事件就绪),可通过设置超时时间或检测空轮询次数解决; - 解决方案:记录连续空轮询次数,超过阈值时重建 Selector。
5. 使用 FileChannel 的 transferTo/transferFrom 方法
- 文件复制、通道间数据传输优先使用这两个方法,底层实现零拷贝,效率远高于传统 IO 和手动缓冲区传输。
1.6.3 大文件处理专项优化
1. 大文件读取优化
- 采用“分块读取”:将大文件分为多个小块,逐块读取处理,避免一次性读取整个文件到内存导致 OOM;
- 示例:读取 1GB 文件,分 1024 块,每块 1MB,逐块处理。
2. 大文件写入优化
- 批量写入:收集一定量的数据后批量写入,减少 IO 次数;
- 异步写入:使用多线程异步写入(如生产者-消费者模式),提升写入吞吐量;
- 避免同步:关闭文件系统的同步机制(如
FileChannel.force(false)),但需注意数据安全性。
3. 大文件复制优化
- 推荐方案:NIO FileChannel + 直接缓冲区 + transferTo 方法;
- 对比:传统 IO 复制 1GB 文件约耗时 10 秒,NIO 优化后约耗时 2 秒,效率提升 5 倍。
1.7 本章总结
本章全面讲解了 Java IO 流与 NIO 的核心知识,从传统 IO 的字节流、字符流到 NIO 的非阻塞 IO,覆盖了 IO 编程的全场景,主要总结如下:
-
传统 IO 流体系:
- 核心分为字节流(处理二进制数据)和字符流(处理文本数据),基于装饰器模式和适配器模式设计;
- 字节流核心类:
InputStream/OutputStream及其子类(FileInputStream、BufferedInputStream、ObjectInputStream); - 字符流核心类:
Reader/Writer及其子类(FileReader、BufferedReader、InputStreamReader); - 选择原则:文本数据用字符流(避免乱码),二进制数据用字节流(直接读写)。
-
IO 流实战要点:
- 必须关闭流资源(优先使用
try-with-resources); - 批量读写提升效率(避免单字节/单字符操作);
- 字符流需指定编码(如 UTF-8),避免乱码;
- 缓冲流是性能优化的核心(减少 IO 次数)。
- 必须关闭流资源(优先使用
-
NIO 核心知识:
- 三大组件:Channel(通道)、Buffer(缓冲区)、Selector(选择器),协同实现非阻塞 IO;
- 核心优势:非阻塞、多路复用、零拷贝,适用于高并发场景;
- 核心实现:FileChannel(文件操作)、SocketChannel/ServerSocketChannel(网络通信);
- 编程模型:事件驱动,单线程处理多连接,资源消耗低。
-
性能优化技巧:
- 传统 IO:缓冲流、批量读写、内存流替代临时文件;
- NIO:直接缓冲区、复用缓冲区、Selector 超时设置、transferTo 零拷贝;
- 大文件处理:分块读写、异步处理、NIO 优化。
通过本章学习,希望你能熟练掌握传统 IO 流的使用场景与技巧,理解 NIO 的核心思想与非阻塞模型,能够根据业务需求选择合适的 IO 方案,应对文件读写、网络通信、高并发等复杂场景,实现高效、安全的 IO 操作。

1067

被折叠的 条评论
为什么被折叠?



