背景
本文是《Java 后端从小白到大神》修仙系列第八篇
,正式进入Java后端
世界,本篇文章主要聊Java基础
。若想详细学习请点击首篇博文,我们开始把。
文章概览
- 核心工具类(Java IO)
核心工具类
1. Java IO
1. IO 框架
graph LR
A(Java IO) --> B(流式部分)
A --> C(非流式部分)
A --> D(其他)
B --> E(字节流(Byte Streams))
B --> F(字符流(Character Streams))
E --> G(InputStream(抽象类))
E --> H(OutputStream(抽象类))
F --> I(Reader(抽象类))
F --> J(Writer(抽象类))
C --> K(File)
C --> L(RandomAccessFile)
C --> M(FileDiscriptor)
D --> N(FileSystem)
D --> O(Win32FileSystem)
D --> P(WinNTFileSystem)
2. IO 模型分类
1. BIO(Blocking I/O)
1. BIO的定义
BIO(Blocking I/O) 是Java传统的阻塞式I/O模型,采用同步阻塞机制。
- 特点:
- 线程在读写数据时会被阻塞,直到操作完成。
- 每个客户端连接需独立线程处理,适用于低并发场景。
- 核心类:
ServerSocket
(服务端)、Socket
(客户端)、InputStream
/OutputStream
(数据流)。
2. BIO的核心原理
-
阻塞模式
- 服务端调用
accept()
监听客户端连接时,线程被阻塞,直到有连接到达。
- 客户端/服务端调用
read()
或write()
时,线程被阻塞,直到数据就绪或传输完成。
-
线程模型
- 一连接一线程:每个客户端连接需分配一个独立线程处理。
- 资源消耗:线程数随连接数线性增长,高并发时易导致线程资源耗尽。
-
适用场景
- 客户端连接数较少(如内部系统)。
- 简单、低并发的网络应用(如传统HTTP服务器)。
3. BIO的核心组成
组件 |
作用 |
ServerSocket |
服务端监听指定端口,等待客户端连接(阻塞在accept() 方法)。 |
Socket |
客户端与服务端建立连接的端点,或服务端接受的客户端连接对象。 |
InputStream |
从Socket中读取数据的输入流(阻塞式读取)。 |
OutputStream |
向Socket中写入数据的输出流(阻塞式写入)。 |
4. BIO代码示例
- 服务端代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class BioServer {
public static void main(String[] args) {
// 使用 try-with-resources 确保 ServerSocket 被关闭
try (ServerSocket serverSocket = new ServerSocket(8080)) {
System.out.println("BIO服务端启动,监听端口 8080...");
while (true) {
// 2. 阻塞等待客户端连接(线程在此暂停,直到有连接),每次调用 accept 方法时,服务器会处理一个连接请求
Socket clientSocket = serverSocket.accept();
System.out.println("新客户端连接: " + clientSocket.getRemoteSocketAddress());
// 3. 为每个客户端连接创建新线程处理
new Thread(() -> {
// 4. 获取输入流(读取客户端数据)。这个方法返回一个 InputStream
// 对象,该对象允许你从套接字中读取数据,通过这个流,你可以逐个字节或批量读取数据
try (InputStream in = clientSocket.getInputStream();
// 5. 获取输出流(向客户端写数据)
OutputStream out = clientSocket.getOutputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
// 6. 阻塞读取客户端数据(线程在此暂停,直到有数据可读)
while ((bytesRead = in.read(buffer)) != -1) {
String message = new String(buffer, 0, bytesRead);
System.out.println("收到客户端消息: " + message);
// 7. 向客户端回写数据(阻塞式写入),out 流中的数据是通过程序主动写入的,而不是自动填充的
out.write(("服务端回复: " + message).getBytes());
// 8.确保数据立即发送给客户端,而不是缓存起来
out.flush();
}
} catch (IOException e) {
e.printStackTrace();
System.out.println("处理客户端连接时发生异常:" + e.getMessage());
} finally {
try {
if (!clientSocket.isClosed()) {
clientSocket.close();
System.out.println("客户端断开: " + clientSocket.getRemoteSocketAddress());
}
} catch (IOException e) {
e.printStackTrace();
System.out.println("关闭客户端连接时发生异常");
}
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
System.out.println("启动服务器时发生异常:" + e.getMessage());
}
}
}
|
- 客户端流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class BioClient {
public static void main(String[] args) throws Exception {
// 1. 连接服务端
Socket socket = new Socket("localhost", 8080);
System.out.println("已连接到服务端");
// 2. 获取输入输出流
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
Scanner scanner = new Scanner(System.in);
// 3. 发送消息线程
new Thread(() -> {
try {
while (true) {
String input = scanner.nextLine();
if ("exit".equalsIgnoreCase(input)) {
socket.close();
break;
}
out.write(input.getBytes()); // 阻塞式写入
out.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
// 4. 接收服务端响应线程
new Thread(() -> {
try {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) { // 阻塞式读取
String response = new String(buffer, 0, bytesRead);
System.out.println("服务端回复: " + response);
}
} catch (IOException e) {
System.out.println("连接已关闭");
}
}).start();
}
}
|
5.代码注释解析
-
服务端流程
serverSocket.accept()
:阻塞等待客户端连接。
- 每接收到一个连接,创建新线程处理读写操作。
in.read(buffer)
:阻塞读取客户端数据。
out.write()
:阻塞写入响应数据。
-
客户端流程
- 用户输入消息后通过
out.write()
发送到服务端。
in.read(buffer)
:阻塞等待服务端响应。
-
多线程必要性
- 服务端需为每个客户端分配独立线程,否则无法同时处理多个连接。
- 客户端使用多线程分离发送和接收操作,避免阻塞用户输入。
6. BIO与NIO对比
特性 |
BIO |
NIO |
阻塞模式 |
完全阻塞 |
非阻塞/多路复用 |
线程模型 |
一连接一线程 |
单线程处理多连接(Selector) |
数据单位 |
流(Stream) |
块(Buffer) |
适用场景 |
低并发、简单应用 |
高并发、高性能需求 |
资源消耗 |
线程数随连接数线性增长 |
线程数固定,资源利用率高 |
7. BIO的优缺点
-
优点:
- 代码简单,易于理解和调试。
- 适合连接数少、业务逻辑简单的场景(如内部管理系统)。
-
缺点:
- 线程阻塞导致资源浪费。
- 高并发时线程数激增,可能引发内存溢出或线程调度性能下降。
8. 关键注意事项
- 线程资源管理
需限制最大线程数(如使用线程池),避免高并发时线程爆炸。
- 异常处理
确保关闭Socket
和ServerSocket
,防止资源泄漏。
- 性能瓶颈
在频繁短连接的场景下,BIO的线程创建/销毁开销较大。
通过上述机制,BIO提供了一种直观的I/O编程模型,但需结合场景权衡其性能与资源消耗。
2. NIO(Non-blocking I/O)
1. NIO的定义
NIO(New I/O) 是 Java 1.4引入的高性能 I/O API,支持非阻塞和多路复用的I/O操作,适用于高并发场景。与传统IO(阻塞式、流式读写)不同,NIO基于通道(Channel)和缓冲区(Buffer),通过事件驱动模型提升效率。
2. NIO的核心原理
- 非阻塞模式
线程不会因等待I/O操作而阻塞,可处理其他任务。
- 多路复用
单个线程通过Selector监控多个通道(Channel)的就绪事件(如连接、读、写),减少线程资源消耗。
- 缓冲区导向
数据通过Buffer块传输,而非传统IO的逐字节流。
3. NIO核心组件
1. Buffer(缓冲区):
- 作用:临时存储数据,支持批量读写。
- 类型:
ByteBuffer
、CharBuffer
、IntBuffer
等。
- 关键操作:
put()
:写入数据到Buffer。
get()
:从Buffer读取数据。
flip()
:切换Buffer为读模式。
clear()
:清空Buffer并切换为写模式。
2. Channel(通道):
- 作用:双向数据传输(可读可写),需与Buffer配合。
- 常见实现:
FileChannel
:文件I/O。
SocketChannel
:TCP网络通信。
ServerSocketChannel
:监听TCP连接。
DatagramChannel
:UDP通信。
3. Selector(选择器):
- 作用:单线程监听多个Channel的就绪事件。
- 关键函数:
open()
:创建Selector。
select()
:阻塞等待至少一个Channel就绪。
selectedKeys()
:获取就绪事件集合。
wakeup()
:唤醒阻塞的Selector。
4. NIo流程图:
graph LR
A[Thread] --> B[Selector]
C[Channel<br>绑定端口/事件] --注册--> B[Selector]
D[Channel<br>绑定端口/事件] --注册--> B[Selector]
E[Channel<br>绑定端口/事件] --注册--> B[Selector]
B[Selector] --根据事件轮训获取就绪的Channel--> F[SelectorKeys]
F[SelectorKeys] --> G[NIO Server]
F[SelectorKeys] --> H[NIO Client]
G[NIO Server] --> I[accept a new socket connection]
G[NIO Server] --> J[ready for reading]
I --注册数据可读事件--> B
H --> K[Tests key's channel has either finished]
H --> L[read data from channel]
H --> M[write data to channel]
K --注册数据可写事件--> B
4. Selector事件类型
事件常量 |
说明 |
SelectionKey.OP_ACCEPT |
服务端接收新连接(ServerSocketChannel ) |
SelectionKey.OP_CONNECT |
客户端完成连接(SocketChannel ) |
SelectionKey.OP_READ |
数据可读 |
SelectionKey.OP_WRITE |
数据可写 |
5. NIO代码示例
- 服务端代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
public class NioServer {
public static void main(String[] args) throws IOException {
// 1. 创建Selector
Selector selector = Selector.open();
// 2. 创建ServerSocketChannel并绑定端口
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false); // 非阻塞模式
// 3. 将ServerSocketChannel注册到Selector,监听ACCEPT事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器启动,监听端口 8080...");
while (true) {
// 4. 阻塞等待就绪的Channel(事件到达时返回)
selector.select();
// 5. 获取所有就绪的事件
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove(); // 处理完事件后需手动移除
if (key.isAcceptable()) {
// 6. 处理新连接事件
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = server.accept();
clientChannel.configureBlocking(false);
// 注册客户端Channel到Selector,监听READ事件
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("客户端连接: " + clientChannel.getRemoteAddress());
} else if (key.isReadable()) {
// 7. 处理读事件
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = clientChannel.read(buffer);
if (bytesRead > 0) {
buffer.flip(); // 切换为读模式
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String message = new String(data);
System.out.println("收到消息: " + message);
// 回写数据给客户端
ByteBuffer response = ByteBuffer.wrap(("服务端回复: " + message).getBytes());
clientChannel.write(response);
} else if (bytesRead == -1) {
// 客户端关闭连接
System.out.println("客户端断开: " + clientChannel.getRemoteAddress());
clientChannel.close();
}
}
}
}
}
}
|
- 客户端流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
public class NioClient {
private Selector selector;
private SocketChannel clientChannel;
private Scanner scanner;
public NioClient(String host, int port) {
try {
// 打开选择器
selector = Selector.open();
// 打开客户端通道
clientChannel = SocketChannel.open();
clientChannel.configureBlocking(false);
// 连接到服务器
clientChannel.connect(new InetSocketAddress(host, port));
// 注册连接事件,客户端将 SocketChannel 注册到 Selector 上,并指定感兴趣的事件类型为
// SelectionKey.OP_CONNECT,这表示客户端希望在连接操作完成时被通知
clientChannel.register(selector, SelectionKey.OP_CONNECT);
System.out.println("客户端启动,尝试连接到服务器: " + host + ":" + port);
// 初始化Scanner
scanner = new Scanner(System.in);
} catch (IOException e) {
System.out.println("启动客户端时发生异常: " + e.getMessage());
}
}
public void run() {
Thread consoleThread = new Thread(() -> {
while (true) {
if (scanner.hasNextLine()) {
String message = scanner.nextLine();
try {
send(clientChannel, message);
} catch (IOException e) {
System.out.println("发送消息时发生异常: " + e.getMessage());
}
}
}
});
consoleThread.start();
try {
while (true) {
selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove();
// 表示 SelectionKey 对应的通道是否已经准备好完成连接操作
if (key.isConnectable()) {
finishConnection(key);
} else if (key.isReadable()) {
readData(key);
}
}
}
} catch (IOException e) {
System.out.println("运行客户端时发生异常: " + e.getMessage());
} finally {
scanner.close();
}
}
private void finishConnection(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
// 检查连接是否已经完成
if (channel.finishConnect()) {
System.out.println("连接到服务器成功");
// 将 SocketChannel 注册到 selector 上,并指定感兴趣的事件类型为 SelectionKey.OP_READ。
channel.register(selector, SelectionKey.OP_READ);
// 发送初始消息
send(channel, "Hello Server!");
} else {
System.out.println("连接到服务器失败");
}
}
private void readData(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String message = new String(data);
System.out.println("收到服务器消息: " + message);
} else if (bytesRead == -1) {
System.out.println("服务器断开连接");
channel.close();
}
}
private void send(SocketChannel channel, String message) throws IOException {
ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
channel.write(buffer);
System.out.println("发送消息到服务器: " + message);
}
public static void main(String[] args) {
NioClient client = new NioClient("localhost", 8080);
client.run();
}
}
|
6.代码注释解析
- Selector创建
使用Selector.open()
创建一个选择器,用于监控多个通道事件。
- ServerSocketChannel配置
- 绑定端口并设置为非阻塞模式(
configureBlocking(false)
)。
- 注册
OP_ACCEPT
事件,表示监听新连接。
- 事件循环
selector.select()
阻塞直到有事件就绪。
- 遍历
selectedKeys
处理所有就绪事件。
- 处理ACCEPT事件
- 当新客户端连接时,接受并注册其
SocketChannel
到Selector,监听OP_READ
事件。
- 处理READ事件
7. NIO与传统IO对比
特性 |
传统IO |
NIO |
阻塞模式 |
阻塞 |
非阻塞 |
数据单位 |
流(Stream) |
块(Buffer) |
线程模型 |
一连接一线程 |
单线程多连接(Selector) |
适用场景 |
低并发、简单连接 |
高并发、高性能需求 |
8. 关键注意事项
- Buffer状态管理
务必正确使用flip()
和clear()
切换读写模式。
- 非阻塞配置
Channel必须设置为非阻塞模式才能注册到Selector。
- 资源释放
操作结束后需关闭Channel和Selector(代码示例省略了try-with-resources
,实际需补充)。
通过上述机制,NIO显著提升了Java在高并发场景下的I/O处理能力。
3. AIO(Asynchronous I/O)
- 定义:
异步非阻塞模型,通过回调机制(CompletionHandler
)或 Future
实现异步操作,线程无需等待 IO 完成。
- 特点:由操作系统通知 IO 完成,适合高吞吐量场景,但 Java 实现较少使用(Linux 支持有限)。
- 代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
// 服务端(AIO)
AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel client, Void attachment) {
server.accept(null, this); // 继续接受下一个连接
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer, null, new CompletionHandler<Integer, Void>() {
@Override
public void completed(Integer bytesRead, Void attachment) {
System.out.println("收到数据:" + new String(buffer.array()));
}
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});
}
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});
Thread.sleep(Long.MAX_VALUE); // 防止主线程退出
|
对比总结
模型 |
阻塞方式 |
线程资源 |
适用场景 |
Java 实现 |
BIO |
同步阻塞 |
每个连接一个线程 |
低并发、简单逻辑 |
ServerSocket , Socket |
NIO |
同步非阻塞 |
单线程管理多连接 |
高并发、高吞吐 |
Selector , Channel |
AIO |
异步非阻塞 |
操作系统回调通知 |
高吞吐、复杂 IO 操作 |
AsynchronousChannel |
注意事项
- BIO:简单但性能差,需通过线程池优化(如
Executors.newFixedThreadPool
)。
- NIO:实际开发中常用 Netty/Mina 等框架简化编程。
- AIO:Java 支持有限(尤其 Linux),生产环境较少直接使用。
3. 流的方向
graph TB
A[流的方向] --> B[输入流]
A --> C[输出流]
B --> D[将<存储设备>中的内容读入到<内存>中]
C --> E[将<内存>中的内容写入到<存储设备>中]
AA[文件] --输入流--> BB[程序] --输处流--> CC[文件]
-
流动方向
数据从外部资源流向程序内部。
用于读取数据,方向是从对方(客户端/服务器)到自己。
例如:文件 → 程序、网络套接字 → 程序、内存字节数组 → 程序。
-
数据源
2. 输出流(OutputStream/Writer)
-
流动方向
数据从程序内部流向外部资源。
用于写入数据,方向是从自己到对方(客户端/服务器)。
例如:程序 → 文件、程序 → 网络套接字、程序 → 内存字节数组。
-
数据目的地
3. 关键总结
流类型 |
方向 |
数据源/目的地判断方法 |
常见实现类示例 |
输入流 |
外部资源 → 程序 |
通过构造函数参数确定(如文件路径、字节数组等) |
FileInputStream , BufferedReader , ObjectInputStream |
输出流 |
程序 → 外部资源 |
通过构造函数参数确定(如文件路径、字节数组等) |
FileOutputStream , PrintWriter , ObjectOutputStream |
4. 示例对比
1
2
3
4
5
|
// 输入流:数据从文件data.txt流入程序
InputStream in = new FileInputStream("data.txt");
// 输出流:数据从程序流入文件output.txt
OutputStream out = new FileOutputStream("output.txt");
|
5. 注意事项
-
装饰器模式的影响
即使使用装饰器(如BufferedInputStream
),底层数据源仍由最基础的流决定。
例如:
1
2
|
InputStream is = new BufferedInputStream(new FileInputStream("data.txt"));
// 实际数据源仍是文件data.txt
|
-
资源与程序的视角
- 输入流:程序是数据的消费者,外部资源是数据的提供者。
- 输出流:程序是数据的生产者,外部资源是数据的接收者。
通过构造函数或获取方式明确数据源/目的地,是理解I/O流向的核心。
4. 流的分类
在 Java IO 流框架中,字节流和处理流(装饰器流)是两种不同角色的流类。
1. 字节流(Byte Streams)
字节流是直接操作原始字节的流,负责与底层数据源(如文件、内存等)进行直接的字节读写。
核心特征:
- 继承自
InputStream
或 OutputStream
。
- 直接操作二进制数据(如图片、音频等非文本文件)。
典型实现类:
类名 |
功能描述 |
FileInputStream |
从文件读取字节 |
FileOutputStream |
向文件写入字节 |
ByteArrayInputStream |
从字节数组读取字节 |
ByteArrayOutputStream |
向字节数组写入字节 |
SocketInputStream |
从网络套接字读取字节 |
SocketOutputStream |
向网络套接字写入字节 |
代码示例:
1
2
3
4
5
6
7
|
// 使用 FileInputStream 读取文件字节
try (InputStream is = new FileInputStream("data.bin")) {
int byteData;
while ((byteData = is.read()) != -1) { // 逐字节读取
System.out.println(byteData);
}
}
|
2. 处理流(Processing Streams / Decorator Streams)
处理流是装饰器模式的体现,通过包装其他流(字节流或字符流)来增强功能(如缓冲、数据类型转换等)。
核心特征:
- 继承自
FilterInputStream
或 FilterOutputStream
(字节流装饰器基类)。
- 不能独立使用,必须包装一个已有的流。
典型实现类:
类名 |
功能描述 |
包装对象示例 |
BufferedInputStream |
提供缓冲,提升读取效率 |
FileInputStream |
BufferedOutputStream |
提供缓冲,提升写入效率 |
FileOutputStream |
DataInputStream |
读取基本数据类型(int、double等) |
FileInputStream |
DataOutputStream |
写入基本数据类型 |
FileOutputStream |
ObjectInputStream |
反序列化对象 |
FileInputStream |
ObjectOutputStream |
序列化对象 |
FileOutputStream |
PushbackInputStream |
支持回退已读取的字节 |
任何 InputStream |
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 使用 BufferedInputStream 包装 FileInputStream
try (InputStream raw = new FileInputStream("data.bin");
InputStream buffered = new BufferedInputStream(raw)) { // 装饰器模式
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = buffered.read(buffer)) != -1) { // 按块读取(高效)
// 处理 buffer[0..bytesRead-1]
}
}
// 使用 DataInputStream 读取基本数据类型
try (InputStream raw = new FileInputStream("data.bin");
DataInputStream dis = new DataInputStream(raw)) {
int num = dis.readInt(); // 读取 int
double value = dis.readDouble(); // 读取 double
}
|
3. 关键区别总结
类别 |
直接操作数据源? |
功能角色 |
依赖关系 |
字节流 |
✔️ |
基础数据读写(字节级别) |
独立使用 |
处理流 |
❌ |
增强功能(缓冲、转换等) |
必须包装其他流 |
4. 常见组合用法
1
2
3
4
5
6
|
// 组合流示例:文件 → 缓冲 → 数据类型处理
try (InputStream raw = new FileInputStream("data.bin");
InputStream buffered = new BufferedInputStream(raw); // 缓冲处理
DataInputStream dis = new DataInputStream(buffered)) { // 数据类型处理
int num = dis.readInt();
}
|
5. 为什么需要处理流?
处理流通过装饰器模式动态扩展流的功能,例如:
- 缓冲:减少直接 I/O 操作次数,提升性能。
- 数据类型支持:直接读写
int
、String
等类型,无需手动转换字节。
- 对象序列化:简化对象的持久化和传输。
这种设计使得 Java IO 流框架高度灵活,能够按需组合功能,避免继承体系的臃肿。
graph LR
A[InputStream] --> B[节点流]
A --> C[处理流]
B --> D[FileInputStream<读取文件字节>]
B --> E[ByteArrayInputStream<读取字节数组>]
B --> F[PipedInputStream<用于线程间通信>]
C --> H[FilterInputStream<装饰器模式基类>]
H --> I[BufferedInputStream<缓冲提升性能>]
H --> J[DataInputStream<读取基本数据类型(int, double)>]
H --> K[PushbackInputStream<支持回退字节>]
C --> L[ObjectInputStream<读取反序列化对象>]
代码示例:
场景:读取文件中的原始字节(如图片、视频等非文本文件)。
技巧:使用 try-with-resources
自动关闭资源,避免内存泄漏。
1
2
3
4
5
6
7
8
|
try (FileInputStream fis = new FileInputStream("image.jpg")) {
int byteData;
while ((byteData = fis.read()) != -1) { // 逐字节读取
// 处理字节数据(如复制文件)
}
} catch (IOException e) {
e.printStackTrace();
}
|
场景:从内存中的字节数组读取数据(如模拟输入流)。
技巧:无需关闭流,但显式关闭是良好习惯。
1
2
3
4
5
6
7
8
9
|
byte[] data = "Hello ByteArray".getBytes();
try (ByteArrayInputStream bais = new ByteArrayInputStream(data)) {
int byteData;
while ((byteData = bais.read()) != -1) {
System.out.print((char) byteData); // 输出: Hello ByteArray
}
} catch (IOException e) {
e.printStackTrace();
}
|
场景:线程间通过管道传递字节数据(生产者-消费者模型)。
技巧:必须与 PipedOutputStream
连接,且需处理线程同步。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
PipedInputStream pis = new PipedInputStream();
PipedOutputStream pos = new PipedOutputStream();
pis.connect(pos); // 建立管道连接
// 生产者线程写入数据
new Thread(() -> {
try {
pos.write("Data from thread".getBytes());
pos.close();
} catch (IOException e) {
e.printStackTrace();
}
}).start();
// 消费者线程读取数据
new Thread(() -> {
try {
int byteData;
while ((byteData = pis.read()) != -1) {
System.out.print((char) byteData); // 输出: Data from thread
}
pis.close();
} catch (IOException e) {
e.printStackTrace();
}
}).start();
|
场景:为其他输入流提供缓冲,减少 I/O 操作次数,提升读取效率。
技巧:默认缓冲区大小 8KB,可根据场景调整(如 new BufferedInputStream(stream, 8192)
)。
1
2
3
4
5
6
7
8
9
10
|
try (FileInputStream fis = new FileInputStream("largefile.bin");
BufferedInputStream bis = new BufferedInputStream(fis)) { // 包装文件流
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) { // 按块读取
// 处理缓冲数据(如文件复制)
}
} catch (IOException e) {
e.printStackTrace();
}
|
场景:读取二进制文件中的基本数据类型(如 int、double)。
技巧:需确保读取顺序与写入顺序一致(通常搭配 DataOutputStream
使用)。
1
2
3
4
5
6
7
8
|
try (FileInputStream fis = new FileInputStream("data.bin");
DataInputStream dis = new DataInputStream(fis)) {
int num = dis.readInt(); // 读取 int
double value = dis.readDouble(); // 读取 double
System.out.println(num + ", " + value); // 例如: 100, 3.14
} catch (IOException e) {
e.printStackTrace();
}
|
场景:解析数据时回退已读取的字节(如处理自定义分隔符)。
技巧:通过 unread()
将字节推回流中,供下次读取。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
String data = "123#456";
try (ByteArrayInputStream bais = new ByteArrayInputStream(data.getBytes());
PushbackInputStream pis = new PushbackInputStream(bais)) {
int byteData;
while ((byteData = pis.read()) != -1) {
if (byteData == '#') {
System.out.println(); // 遇到#换行
pis.unread(' '); // 将空格推回流中,后续读取
} else {
System.out.print((char) byteData); // 输出: 123 456
}
}
} catch (IOException e) {
e.printStackTrace();
}
|
场景:反序列化对象(从文件或网络恢复对象状态)。
技巧:对象需实现 Serializable
接口,序列化与反序列化版本号需一致。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class User implements Serializable {
private static final long serialVersionUID = 1L;
String name;
public User(String name) { this.name = name; }
}
// 从文件反序列化对象
try (FileInputStream fis = new FileInputStream("user.bin");
ObjectInputStream ois = new ObjectInputStream(fis)) {
User user = (User) ois.readObject();
System.out.println(user.name); // 输出: Alice
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
|
总结:
类 |
核心场景 |
关键技巧 |
FileInputStream |
读取文件原始字节 |
使用 try-with-resources 关闭流 |
ByteArrayInputStream |
内存字节数据模拟流 |
适用于测试或内存数据处理 |
PipedInputStream |
线程间管道通信 |
必须连接 PipedOutputStream |
BufferedInputStream |
提升读取效率 |
默认 8KB 缓冲,可调整大小 |
DataInputStream |
读取基本数据类型 |
需与 DataOutputStream 配对使用 |
PushbackInputStream |
回退字节以重新解析 |
unread() 方法灵活处理数据分隔 |
ObjectInputStream |
反序列化对象 |
确保对象实现 Serializable 接口 |
通过合理选择流类型并组合使用,可以高效处理不同 I/O 场景。
总结
Java 核心工具涵盖了数据处理、并发、模块化等关键领域。通过合理使用这些工具,可以显著提升代码质量和开发效率。