背景

本文是《Java 后端从小白到大神》修仙系列第九篇,正式进入Java后端世界,本篇文章主要聊Java基础中的IO(输入输出)。IO是Java中非常重要的一部分,它负责处理程序与外部资源之间的数据传输。若想详细学习请点击首篇博文,我们开始吧。

文章概览

  1. Java IO 框架概述
  2. IO 模型分类
    • BIO(Blocking I/O)
    • NIO(Non-blocking I/O)
    • AIO(Asynchronous I/O)
  3. 流的方向与分类
  4. 字节流与字符流
  5. 输入流与输出流
  6. 节点流与处理流
  7. 常用IO类详解
  8. IO 最佳实践

1. Java IO 框架概述

Java 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(FileDescriptor)

    D --> N(FileSystem)
    D --> O(Win32FileSystem)
    D --> P(WinNTFileSystem)

核心概念

  • 流(Stream):数据传输的抽象,分为输入流和输出流
  • 通道(Channel):双向数据传输的通道,用于NIO
  • 缓冲区(Buffer):临时存储数据的容器,用于NIO
  • 选择器(Selector):用于NIO的多路复用机制

2. IO 模型分类

2.1 BIO(Blocking I/O)

定义:BIO(Blocking I/O)是Java传统的阻塞式I/O模型,采用同步阻塞机制。

核心特点

  • 阻塞模式:线程在读写数据时会被阻塞,直到操作完成
  • 线程模型:每个客户端连接需独立线程处理,适用于低并发场景
  • 数据单位:基于流(Stream)的逐字节传输

核心类

  • ServerSocket:服务端监听指定端口,等待客户端连接
  • Socket:客户端与服务端建立连接的端点
  • InputStream/OutputStream:用于数据传输的输入输出流

工作原理

  1. 服务端调用accept()方法阻塞等待客户端连接
  2. 每接收到一个连接,创建新线程处理读写操作
  3. 线程调用read()write()方法时阻塞,直到数据就绪或传输完成

代码示例

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
61
62
63
64
65
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) {
                // 阻塞等待客户端连接(线程在此暂停,直到有连接)
                // 阻塞等待客户端连接(每accept()一次,就拿到一个新的clientSocket)
                // 阻塞点 1:主线程卡在这,直到有客户端连接
                Socket clientSocket = serverSocket.accept();
                System.out.println("新客户端连接: " + clientSocket.getRemoteSocketAddress());

                // 为每个客户端连接创建新线程处理
                // 每 accept 一次,就 new Thread 一次,start 一次 → 每个客户端对应一个独立线程
                new Thread(() -> {
                    try (InputStream in = clientSocket.getInputStream();
                         OutputStream out = clientSocket.getOutputStream()) {

                        byte[] buffer = new byte[1024];
                        int bytesRead;

                        // 阻塞读取客户端数据(线程在此暂停,直到有数据可读)
                        // in.read(buffer):调用输入流的 read() 方法,这个方法会做两件事:
                        // 从客户端发送过来的数据流里,读取数据填充到 buffer 数组里(最多填 1024 字节,因为数组大小是 1024);
                        // 返回实际读到的字节数(比如客户端发了 5 个字节,就返回 5;如果客户端断开,返回 - 1)。
                        // 阻塞点 2:in.read(buffer)
                        while ((bytesRead = in.read(buffer)) != -1) {
                            String message = new String(buffer, 0, bytesRead);
                            System.out.println("收到客户端消息: " + message);
                            // 向客户端回写数据(阻塞式写入)
                            // 阻塞点 3:out.write(...)/out.flush()
                            out.write(("服务端回复: " + message).getBytes());
                            // 确保数据立即发送给客户端,而不是缓存起来
                            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());
        }
    }
}

客户端代码

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
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);

        // 在 Java 中,线程分为两类:用户线程(User Thread) 和 守护线程(Daemon Thread)
        // 主线程(main 线程)是 JVM 启动时创建的用户线程(所有程序的入口线程默认都是用户线程)
        // 用户线程:也叫「非守护线程」,是程序的核心业务线程(比如代码里的发消息、收消息线程)。JVM 会一直运行,直到所有用户线程都执行完毕才会退出;
        // 守护线程:是「辅助线程」(比如垃圾回收 GC 线程、定时器线程),它的存在是为用户线程服务的。只要最后一个用户线程结束,不管守护线程是否运行,JVM 都会直接退出,守护线程会被强制终止。
        // 通过 new Thread(...) 创建的「发消息线程」「收消息线程」,父线程都是主线程,所以默认继承了「用户线程」的属性;
        // 因此这两个子线程是用户线程,JVM 必须等它们都结束,才会退出

        // 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();
    }
}

优缺点

  • 优点:代码简单,易于理解和调试;适合连接数少、业务逻辑简单的场景
  • 缺点:线程阻塞导致资源浪费;高并发时线程数激增,可能引发内存溢出或线程调度性能下降

适用场景:低并发、简单的网络应用,如内部管理系统、小型工具等

2.2 NIO(Non-blocking I/O)

定义:NIO(New I/O)是 Java 1.4 引入的高性能 I/O API,支持非阻塞多路复用的I/O操作,适用于高并发场景。

核心特点

  • 非阻塞模式:线程不会因等待I/O操作而阻塞,可处理其他任务
  • 多路复用:单个线程通过Selector监控多个通道的就绪事件
  • 缓冲区导向:数据通过Buffer块传输,而非传统IO的逐字节流
  • 通道:双向数据传输(可读可写)

核心组件

  1. Buffer:临时存储数据的容器,支持批量读写

    • 类型:ByteBufferCharBufferIntBuffer
    • 关键操作:put()(写入)、get()(读取)、flip()(切换为读模式)、clear()(清空并切换为写模式)
  2. Channel:双向数据传输通道,需与Buffer配合

    • 常见实现:FileChannel(文件I/O)、SocketChannel(TCP网络通信)、ServerSocketChannel(监听TCP连接)、DatagramChannel(UDP通信)
  3. Selector:单线程监听多个Channel的就绪事件

    • 关键方法:open()(创建)、select()(阻塞等待就绪事件)、selectedKeys()(获取就绪事件集合)、wakeup()(唤醒阻塞的Selector)

Selector事件类型

事件常量 说明
SelectionKey.OP_ACCEPT 服务端接收新连接(ServerSocketChannel
SelectionKey.OP_CONNECT 客户端完成连接(SocketChannel
SelectionKey.OP_READ 数据可读
SelectionKey.OP_WRITE 数据可写

NIO工作流程

graph LR
    %% 阶段1:初始化核心组件
    步骤1[步骤1: 创建主线程Thread] --> 步骤2[步骤2: 创建Selector事件监听器]
    步骤3[步骤3: 创建服务端通道ServerSocketChannel 绑定8080端口] -- 步骤4[注册ACCEPT连接事件到Selector] --> 步骤2
    步骤5[步骤5: 创建客户端通道SocketChannel] -- 步骤6[注册READ读/WRITE写事件到Selector] --> 步骤2

    %% 阶段2:Selector轮询就绪事件
    步骤2 -- 步骤7[调用select方法 检测所有注册的通道] --> 步骤8[步骤8: 获取就绪的SelectorKeys事件键]

    %% 阶段3:处理不同类型的就绪事件
    步骤8 --> 步骤9{步骤9: 判断事件类型}
    步骤9 --> |ACCEPT连接事件| 步骤10[步骤10: 服务端接受新连接 生成新SocketChannel]
    步骤10 -- 步骤11[为新通道注册READ读事件] --> 步骤2
    步骤9 --> |READ读事件| 步骤12[步骤12: 从通道读取客户端数据]
    步骤9 --> |WRITE写事件| 步骤13[步骤13: 向通道写入响应数据]
    
    %% 处理完事件后重新注册
    步骤12 -- 步骤14[注册WRITE写事件 准备回写数据] --> 步骤2
    步骤13 -- 步骤15[注册READ读事件 等待新数据] --> 步骤2

完整的类比流程(对应流程图序号)

  1. 餐厅开业:经理(Thread)到岗;
  2. 经理拿上「需求记录本」(Selector);
  3. 餐厅摆好餐桌(ServerSocketChannel),绑定好“营业中”的牌子(绑定端口);
  4. 经理把“门口迎宾”的需求(ACCEPT事件)记录在本子上(注册);
  5. (可选)有熟客提前预约餐桌(SocketChannel);
  6. 经理把这些餐桌的“点餐/结账”需求(READ/WRITE)也记在本子上;
  7. 经理站在大堂,循环巡视(select()轮询)所有餐桌,看谁举手;
  8. 发现有3号桌举手(就绪SelectorKey),一看需求是“要点餐”(READ事件);
  9. 经理判断需求类型:是“进门找座”(ACCEPT)、“点餐”(READ)还是“上菜”(WRITE);
  10. 如果是“进门找座”(ACCEPT):经理带顾客到空桌(accept()建立连接);
  11. 把新餐桌的“点餐”需求(READ事件)记到本子上;
  12. 如果是“点餐”(READ):经理让服务员去餐桌拿点餐单(读取数据);
  13. 如果是“上菜”(WRITE):经理让服务员把菜送到餐桌(写入数据);
  14. 点完餐后,经理把“上菜”需求(WRITE)记到本子上(注册);
  15. 上完菜后,经理把“加菜/结账”需求(READ)重新记到本子上。

服务端代码示例

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
 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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.Selector;
import java.nio.channels.SelectionKey;
import java.util.Set;
import java.util.Iterator;

public class NioServer {
    // 复用缓冲区,避免频繁创建(优化点1)
    private static final ByteBuffer READ_BUFFER = ByteBuffer.allocate(1024);

    public static void main(String[] args) {
        Selector selector = null;
        ServerSocketChannel serverSocketChannel = null;

        try {
            // ========== 阶段1:初始化(对应流程1-6) ==========
            // 1. 创建主线程(main方法本身就是主线程,无需手动创建)
            // 2. 创建Selector(事件监听器)
            selector = Selector.open();

            // 3. 创建ServerSocketChannel(服务端通道),绑定8080端口
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.socket().bind(new InetSocketAddress(8080));
            serverSocketChannel.configureBlocking(false); // 必须设置非阻塞

            // 4. 注册ACCEPT事件到Selector(服务端核心事件)
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            System.out.println("NIO服务器启动,监听端口 8080...");

            while (true) {
                // ========== 阶段2:事件轮询(对应流程7-8) ==========
                // 7. 轮询select():阻塞检测所有注册的Channel,有就绪事件才返回
                int readyChannels = selector.select();
                if (readyChannels == 0) {
                    continue; // 无就绪事件,跳过
                }

                // 8. 获取就绪的SelectorKeys(每个Key对应一个就绪的Channel+事件)
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

                // ========== 阶段3:处理就绪事件(对应流程9-15) ==========
                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();
                    keyIterator.remove(); // 必须移除,避免重复处理(优化点2)

                    // 9. 判断事件类型
                    if (key.isValid() && key.isAcceptable()) {
                        // ========== 处理ACCEPT事件(新客户端连接) ==========
                        // 10. Server处理:accept()建立连接,得到新的SocketChannel
                        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
                        SocketChannel clientChannel = serverChannel.accept();
                        if (clientChannel == null) {
                            continue; // 防御性编程(优化点3)
                        }
                        clientChannel.configureBlocking(false);
                        System.out.println("新客户端连接: " + clientChannel.getRemoteAddress());

                        // 11. 为新Channel注册READ事件(监听客户端发数据)
                        clientChannel.register(selector, SelectionKey.OP_READ);

                    } else if (key.isValid() && key.isReadable()) {
                        // ========== 处理READ事件(通道有数据可读) ==========
                        SocketChannel clientChannel = (SocketChannel) key.channel();
                        READ_BUFFER.clear(); // 重置缓冲区(优化点4)
                        int bytesRead = 0;

                        try {
                            // 12. Client处理:从Channel读取数据
                            bytesRead = clientChannel.read(READ_BUFFER);
                        } catch (IOException e) {
                            // 客户端异常断开(优化点5:捕获IO异常)
                            System.out.println("客户端异常断开: " + clientChannel.getRemoteAddress());
                            key.cancel();
                            clientChannel.close();
                            continue;
                        }

                        if (bytesRead > 0) {
                            READ_BUFFER.flip(); // 切换为读模式
                            byte[] data = new byte[READ_BUFFER.remaining()];
                            READ_BUFFER.get(data);
                            String message = new String(data, "UTF-8"); // 指定编码(优化点6)
                            System.out.println("收到客户端消息: " + message);

                            // 13. Client处理:向Channel写入数据(回写响应)
                            ByteBuffer responseBuffer = ByteBuffer.wrap(("服务端回复: " + message).getBytes("UTF-8"));
                            while (responseBuffer.hasRemaining()) {
                                clientChannel.write(responseBuffer); // 确保数据写完(优化点7)
                            }

                            // 14. 可选:注册WRITE事件(如需主动推送数据时用)
                            // clientChannel.register(selector, SelectionKey.OP_WRITE);

                        } else if (bytesRead == -1) {
                            // 客户端正常断开
                            System.out.println("客户端正常断开: " + clientChannel.getRemoteAddress());
                            key.cancel(); // 取消注册(优化点8)
                            clientChannel.close();

                        }
                    } else if (key.isValid() && key.isWritable()) {
                        // ========== 处理WRITE事件(通道可写数据) ==========
                        // 15. 可选:注册READ事件(写完数据后,重新监听读事件)
                        SocketChannel clientChannel = (SocketChannel) key.channel();
                        // 此处可处理主动推送数据逻辑
                        key.interestOps(SelectionKey.OP_READ); // 切换回读事件(优化点9)
                    }
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("服务器启动/运行异常: " + e.getMessage());
        } finally {
            // 资源释放(优化点10)
            try {
                if (serverSocketChannel != null && serverSocketChannel.isOpen()) {
                    serverSocketChannel.close();
                }
                if (selector != null && selector.isOpen()) {
                    selector.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

客户端代码示例

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
 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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.channels.Selector;
import java.nio.channels.SelectionKey;
import java.util.Set;
import java.util.Iterator;
import java.util.Scanner;

/**
 * NIO 客户端核心类
 * 核心逻辑:单Selector监听连接/读事件 + 独立线程处理控制台输入
 */
public class NioClient {
    // 核心组件
    private Selector selector;          // 事件监听器(Selector)
    private SocketChannel clientChannel;// 客户端通道(SocketChannel)
    private Scanner scanner;            // 控制台输入扫描器
    private volatile boolean isRunning = true; // 客户端运行状态标识
    private static final ByteBuffer READ_BUFFER = ByteBuffer.allocate(1024); // 复用读缓冲区
    private static final String CHARSET = "UTF-8"; // 统一编码格式

    /**
     * 构造方法:初始化客户端核心组件
     * @param host 服务端IP
     * @param port 服务端端口
     */
    public NioClient(String host, int port) {
        try {
            // 1. 创建Selector(事件监听器)
            selector = Selector.open();
            
            // 2. 创建客户端通道(SocketChannel),设置非阻塞模式
            clientChannel = SocketChannel.open();
            clientChannel.configureBlocking(false);
            
            // 3. 发起连接(非阻塞,不会立即完成)
            clientChannel.connect(new InetSocketAddress(host, port));
            
            // 4. 注册CONNECT事件到Selector(监听连接是否完成)
            clientChannel.register(selector, SelectionKey.OP_CONNECT);
            System.out.println("客户端启动,尝试连接到服务器: " + host + ":" + port);

            // 5. 初始化控制台输入扫描器
            scanner = new Scanner(System.in);

        } catch (IOException e) {
            System.err.println("客户端初始化失败: " + e.getMessage());
            // 初始化失败时终止程序
            shutdown();
        }
    }

    /**
     * 客户端核心运行方法
     * 逻辑:启动控制台输入线程 + 轮询Selector处理事件
     */
    public void run() {
        // 6. 启动独立线程处理控制台输入(避免阻塞Selector轮询)
        startConsoleInputThread();

        try {
            // 7. 循环轮询Selector,处理就绪事件
            while (isRunning) {
                // 阻塞等待就绪事件(无事件时线程休眠)
                int readyChannels = selector.select();
                if (readyChannels == 0) {
                    continue; // 无就绪事件,跳过本次循环
                }

                // 8. 获取所有就绪的事件Key
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

                // 9. 遍历处理每个就绪事件
                while (keyIterator.hasNext() && isRunning) {
                    SelectionKey key = keyIterator.next();
                    keyIterator.remove(); // 必须移除,避免重复处理

                    // 防御性检查:确保Key有效
                    if (!key.isValid()) {
                        continue;
                    }

                    // 10. 处理CONNECT事件(连接完成/失败)
                    if (key.isConnectable()) {
                        finishConnection(key);
                    }
                    // 11. 处理READ事件(服务端回写数据)
                    else if (key.isReadable()) {
                        readServerData(key);
                    }
                }
            }
        } catch (IOException e) {
            System.err.println("客户端运行异常: " + e.getMessage());
        } finally {
            // 12. 程序退出时释放资源
            shutdown();
        }
    }

    /**
     * 启动控制台输入线程
     * 功能:读取用户输入,发送到服务端
     */
    private void startConsoleInputThread() {
        Thread consoleThread = new Thread(() -> {
            System.out.println("请输入消息(输入exit退出):");
            while (isRunning && scanner.hasNextLine()) {
                String input = scanner.nextLine().trim();
                
                // 处理退出指令
                if ("exit".equalsIgnoreCase(input)) {
                    System.out.println("客户端准备退出...");
                    shutdown();
                    break;
                }
                
                // 空消息不发送
                if (input.isEmpty()) {
                    System.out.println("消息不能为空,请重新输入!");
                    continue;
                }

                try {
                    // 13. 发送消息到服务端
                    sendMessageToServer(input);
                } catch (IOException e) {
                    System.err.println("发送消息失败: " + e.getMessage());
                }
            }
        }, "ConsoleInputThread"); // 给线程命名,便于调试
        consoleThread.setDaemon(true); // 设置为守护线程,主程序退出时自动终止
        consoleThread.start();
    }

    /**
     * 完成连接建立
     * @param key 就绪的SelectionKey
     */
    private void finishConnection(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        
        // 检查连接是否真正完成(非阻塞连接的核心步骤)
        if (channel.finishConnect()) {
            System.out.println("连接到服务器成功!");
            // 14. 连接完成后,注册READ事件(监听服务端回写数据)
            key.interestOps(SelectionKey.OP_READ); // 替换事件类型,无需重新注册
            // 发送初始问候消息
            sendMessageToServer("Hello Server!");
        } else {
            System.err.println("连接到服务器失败!");
            shutdown(); // 连接失败,终止客户端
        }
    }

    /**
     * 读取服务端回写的数据
     * @param key 就绪的SelectionKey
     */
    private void readServerData(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        READ_BUFFER.clear(); // 重置缓冲区(清空旧数据)
        
        int bytesRead = channel.read(READ_BUFFER);
        if (bytesRead > 0) {
            READ_BUFFER.flip(); // 切换为读模式(从写→读)
            byte[] data = new byte[READ_BUFFER.remaining()];
            READ_BUFFER.get(data);
            String message = new String(data, CHARSET); // 指定编码,避免乱码
            System.out.println("\n收到服务器消息: " + message);
            System.out.println("请输入消息(输入exit退出):"); // 提示用户继续输入
        } else if (bytesRead == -1) {
            // 服务端断开连接
            System.err.println("服务器已断开连接!");
            shutdown();
        }
    }

    /**
     * 发送消息到服务端
     * @param message 要发送的消息
     */
    private void sendMessageToServer(String message) throws IOException {
        if (clientChannel == null || !clientChannel.isOpen()) {
            throw new IOException("客户端通道未打开,无法发送消息");
        }

        ByteBuffer writeBuffer = ByteBuffer.wrap(message.getBytes(CHARSET));
        // 循环写入,确保数据全部发送(NIO write可能只写部分数据)
        while (writeBuffer.hasRemaining()) {
            clientChannel.write(writeBuffer);
        }
        System.out.println("发送消息到服务器: " + message);
    }

    /**
     * 优雅关闭客户端,释放所有资源
     */
    private void shutdown() {
        isRunning = false; // 终止主循环
        
        // 关闭Selector(会自动取消所有注册的Key)
        if (selector != null && selector.isOpen()) {
            try {
                selector.close();
            } catch (IOException e) {
                System.err.println("关闭Selector失败: " + e.getMessage());
            }
        }
        
        // 关闭客户端通道
        if (clientChannel != null && clientChannel.isOpen()) {
            try {
                clientChannel.close();
            } catch (IOException e) {
                System.err.println("关闭客户端通道失败: " + e.getMessage());
            }
        }
        
        // 关闭控制台输入扫描器
        if (scanner != null) {
            scanner.close();
        }
        
        System.out.println("客户端已优雅关闭!");
    }

    /**
     * 主方法:启动客户端
     */
    public static void main(String[] args) {
        try {
            NioClient client = new NioClient("localhost", 8080);
            client.run();
        } catch (Exception e) {
            System.err.println("客户端启动失败: " + e.getMessage());
            System.exit(1); // 非0退出码标识异常
        }
    }
}

优缺点

  • 优点:非阻塞模式提高了线程利用率;单线程可处理多个连接,适合高并发场景;缓冲区导向提高了I/O效率
  • 缺点:代码复杂度较高;需要手动管理Buffer状态;对编程技巧要求较高

适用场景:高并发、高性能的网络应用,如Web服务器、聊天服务器等

2.3 AIO(Asynchronous I/O)

定义:AIO(Asynchronous I/O)是 Java 7 引入的异步非阻塞I/O模型,通过回调机制或 Future 实现异步操作。

核心特点

  • 异步非阻塞:线程无需等待I/O完成,由操作系统通知I/O完成
  • 回调机制:通过CompletionHandler处理I/O完成事件
  • Future模式:通过Future获取I/O操作结果

工作原理

  1. 发起I/O操作,立即返回
  2. 操作系统在后台处理I/O操作
  3. I/O完成后,通过回调或Future通知应用程序

用生活例子讲透:谁在回调?谁调用谁?
我们用「点外卖」的场景类比AIO中的回调逻辑,对应代码里的角色:

角色 生活场景对应 代码中对应角色
你(程序员) 点外卖的用户 编写 CompletionHandler 回调方法的你
别人(回调方) 外卖骑手 JDK/AIO底层的异步线程池(操作系统通知后)
回调方法 你留给骑手的“敲门动作” completed()/failed() 方法
触发回调的时机 骑手送到餐,按约定敲门 操作系统完成IO操作(连接建立/数据读取完成)

生活场景完整流程(对应代码逻辑):

  1. 你发起请求 + 留下回调规则

    • 你打开外卖APP下单(对应代码:server.accept(null, 回调对象));
    • 你告诉骑手:“送到后敲我家门(调用completed()),送不到就打电话(调用failed())”(对应代码:你定义completed()/failed()方法);
    • 你说完就去看电视了,不用站在门口等(对应代码:主线程发起异步操作后,继续执行,不阻塞)。
  2. 别人执行任务 + 触发回调

    • 骑手取餐、送餐(对应:操作系统处理AIO的连接/读/写操作);
    • 骑手送到了,按约定敲你家门(对应:JDK底层线程池调用你定义的completed()方法);
    • 如果骑手迷路送不到,按约定给你打电话(对应:JDK底层线程池调用你定义的failed()方法)。

代码示例

AIO服务端代码
  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
108
109
110
111
112
113
114
115
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;

/**
 * AIO(异步非阻塞IO)服务端核心类
 * 核心特点:所有IO操作(接受连接/读/写)都是异步的,由操作系统完成后回调通知,主线程无主动阻塞
 */
public class AioServer {
    public static void main(String[] args) throws IOException {
        // ========== 步骤1:创建并初始化异步服务端通道 ==========
        // 1. 打开异步服务器套接字通道(AIO核心组件,替代BIO的ServerSocket)
        //    区别于NIO的ServerSocketChannel:AIO通道的所有操作都是异步的
        AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open();
        
        // 2. 绑定监听端口8080(和BIO/NIO的端口绑定逻辑一致)
        server.bind(new InetSocketAddress(8080));
        
        System.out.println("AIO服务器启动,监听端口 8080...");
        
        // ========== 步骤2:异步接受客户端连接(AIO核心回调逻辑) ==========
        // server.accept():异步接受连接,不会阻塞主线程!
        // 参数1:attachment(附件)- 可以传递任意数据给回调方法,这里用null表示不需要传递
        // 参数2:CompletionHandler(完成处理器)- IO操作完成后触发的回调接口
        //        泛型说明:<AsynchronousSocketChannel, Void> 
        //        - 第一个泛型:IO操作成功后返回的结果类型(这里是客户端通道)
        //        - 第二个泛型:附件的类型(这里是Void,因为附件传了null)
        server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
            // 回调方法1:IO操作成功完成时触发(这里是“接受连接成功”)
            @Override
            public void completed(AsynchronousSocketChannel client, Void attachment) {
                // 【关键】立即再次调用accept(),继续接受下一个客户端连接
                // AIO的accept()是一次性的,处理完一个连接后必须重新调用,否则不会监听新连接
                server.accept(null, this); // this指当前CompletionHandler对象,复用回调逻辑
                
                // 打印客户端连接信息(client是异步返回的客户端通道,替代BIO的Socket)
                System.out.println("客户端连接: " + client);
                
                // ========== 步骤3:异步读取客户端发送的数据 ==========
                // 3.1 创建1024字节的缓冲区(AIO/NIO都基于缓冲区传输数据,替代BIO的流)
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                
                // 3.2 异步读取客户端数据:client.read()不会阻塞当前线程!
                // 参数说明:
                // - buffer:存储读取数据的缓冲区
                // - attachment:附件,这里传null
                // - CompletionHandler<Integer, Void>:读取完成后的回调
                //   第一个泛型Integer:读取成功后返回“读取的字节数”
                client.read(buffer, null, new CompletionHandler<Integer, Void>() {
                    // 回调方法1:读取数据成功完成时触发
                    @Override
                    public void completed(Integer bytesRead, Void attachment) {
                        // bytesRead > 0 表示读取到了有效数据
                        if (bytesRead > 0) {
                            // 切换缓冲区为读模式(NIO/AIO通用操作,把指针移到数据起始位置)
                            buffer.flip();
                            // 创建和缓冲区剩余数据长度一致的字节数组(避免浪费空间)
                            byte[] data = new byte[buffer.remaining()];
                            // 从缓冲区读取数据到字节数组
                            buffer.get(data);
                            // 将字节数组转为字符串(这里默认用系统编码,生产环境建议指定UTF-8)
                            String message = new String(data);
                            System.out.println("收到数据:" + message);
                            
                            // ========== 步骤4:异步回写数据给客户端 ==========
                            // 4.1 封装响应数据到缓冲区(服务端回复+客户端消息)
                            ByteBuffer response = ByteBuffer.wrap(("服务端回复: " + message).getBytes());
                            
                            // 4.2 异步写入数据到客户端:client.write()不会阻塞当前线程!
                            client.write(response, null, new CompletionHandler<Integer, Void>() {
                                // 回调方法1:写入数据成功完成时触发
                                @Override
                                public void completed(Integer bytesWritten, Void attachment) {
                                    // bytesWritten是成功写入的字节数
                                    System.out.println("回复发送完成,写入字节数:" + bytesWritten);
                                }
                                
                                // 回调方法2:写入数据失败时触发(比如客户端断开连接)
                                @Override
                                public void failed(Throwable exc, Void attachment) {
                                    System.out.println("回复发送失败: " + exc.getMessage());
                                }
                            });
                        }
                        // 注意:bytesRead == -1 表示客户端断开连接,这里可以补充关闭通道的逻辑
                    }
                    
                    // 回调方法2:读取数据失败时触发(比如客户端异常断开)
                    @Override
                    public void failed(Throwable exc, Void attachment) {
                        System.out.println("读取数据失败: " + exc.getMessage());
                    }
                });
            }
            
            // 回调方法2:接受连接失败时触发(比如端口被占用、权限不足)
            @Override
            public void failed(Throwable exc, Void attachment) {
                System.out.println("接受连接失败: " + exc.getMessage());
            }
        });
        
        // ========== 步骤5:防止主线程退出 ==========
        // AIO的所有IO操作都由操作系统的异步线程池处理,主线程执行完上面的代码后会立即退出
        // Thread.sleep(Long.MAX_VALUE) 让主线程无限休眠,保证服务端一直运行
        try {
            Thread.sleep(Long.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
AIO客户端代码
  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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.Scanner;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

/**
 * AIO 客户端(异步非阻塞IO)
 * 核心逻辑:异步连接服务端 + 异步读取服务端响应 + 独立线程处理控制台输入
 */
public class AioClient {
    // 核心组件
    private AsynchronousSocketChannel clientChannel; // 异步客户端通道
    private Scanner scanner;                         // 控制台输入扫描器
    private static final String CHARSET = "UTF-8";   // 统一编码格式
    private static final int BUFFER_SIZE = 1024;     // 缓冲区大小

    /**
     * 初始化客户端并连接服务端
     * @param host 服务端IP
     * @param port 服务端端口
     */
    public AioClient(String host, int port) {
        try {
            // 1. 创建异步客户端通道
            clientChannel = AsynchronousSocketChannel.open();
            // 2. 异步连接服务端(Future方式获取连接结果)
            Future<Void> connectFuture = clientChannel.connect(new InetSocketAddress(host, port));
            connectFuture.get(); // 阻塞等待连接完成(也可改用CompletionHandler异步处理)
            
            System.out.println("AIO客户端连接服务端成功(" + host + ":" + port + ")");
            System.out.println("请输入消息(输入exit退出):");

            // 3. 初始化控制台输入扫描器
            scanner = new Scanner(System.in);

            // 4. 启动异步读取服务端响应的线程(持续监听服务端回写)
            startAsyncRead();

            // 5. 启动控制台输入线程,处理用户输入
            startConsoleInputThread();

        } catch (IOException e) {
            System.err.println("客户端初始化失败: " + e.getMessage());
            shutdown();
        } catch (InterruptedException | ExecutionException e) {
            System.err.println("连接服务端失败: " + e.getMessage());
            shutdown();
        }
    }

    /**
     * 启动异步读取服务端响应(核心AIO逻辑)
     * 采用CompletionHandler回调方式处理异步读结果
     */
    private void startAsyncRead() {
        ByteBuffer readBuffer = ByteBuffer.allocate(BUFFER_SIZE);
        // 异步读取:参数1-缓冲区,参数2-附件(传递给回调),参数3-回调处理器
        clientChannel.read(readBuffer, readBuffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer bytesRead, ByteBuffer buffer) {
                if (bytesRead > 0) {
                    // 读取到数据:切换缓冲区为读模式,解析数据
                    buffer.flip();
                    byte[] data = new byte[buffer.remaining()];
                    buffer.get(data);
                    try {
                        String response = new String(data, CHARSET);
                        System.out.println("\n收到服务端回复: " + response);
                        System.out.println("请输入消息(输入exit退出):");
                    } catch (Exception e) {
                        System.err.println("解析服务端消息失败: " + e.getMessage());
                    }
                    // 读取完成后,重新注册异步读(持续监听服务端响应)
                    startAsyncRead();
                } else if (bytesRead == -1) {
                    // 服务端断开连接
                    System.err.println("服务端已断开连接!");
                    shutdown();
                }
            }

            @Override
            public void failed(Throwable exc, ByteBuffer buffer) {
                System.err.println("读取服务端数据失败: " + exc.getMessage());
                shutdown();
            }
        });
    }

    /**
     * 启动控制台输入线程,处理用户输入并异步发送到服务端
     */
    private void startConsoleInputThread() {
        new Thread(() -> {
            while (clientChannel != null && clientChannel.isOpen() && scanner.hasNextLine()) {
                String input = scanner.nextLine().trim();
                
                // 处理退出指令
                if ("exit".equalsIgnoreCase(input)) {
                    System.out.println("客户端准备退出...");
                    shutdown();
                    break;
                }
                
                // 空消息不发送
                if (input.isEmpty()) {
                    System.out.println("消息不能为空,请重新输入!");
                    continue;
                }

                // 异步发送消息到服务端
                sendMessage(input);
            }
        }, "ConsoleInputThread").start();
    }

    /**
     * 异步发送消息到服务端
     * @param message 要发送的消息
     */
    private void sendMessage(String message) {
        try {
            ByteBuffer writeBuffer = ByteBuffer.wrap(message.getBytes(CHARSET));
            // 异步写入:回调处理发送结果
            clientChannel.write(writeBuffer, null, new CompletionHandler<Integer, Void>() {
                @Override
                public void completed(Integer bytesWritten, Void attachment) {
                    System.out.println("消息发送成功,发送字节数: " + bytesWritten);
                }

                @Override
                public void failed(Throwable exc, Void attachment) {
                    System.err.println("消息发送失败: " + exc.getMessage());
                }
            });
        } catch (Exception e) {
            System.err.println("构建发送缓冲区失败: " + e.getMessage());
        }
    }

    /**
     * 优雅关闭客户端,释放所有资源
     */
    private void shutdown() {
        // 关闭控制台扫描器
        if (scanner != null) {
            scanner.close();
        }
        
        // 关闭异步通道
        if (clientChannel != null && clientChannel.isOpen()) {
            try {
                clientChannel.close();
                System.out.println("客户端通道已关闭");
            } catch (IOException e) {
                System.err.println("关闭客户端通道失败: " + e.getMessage());
            }
        }
        
        // 终止JVM(主线程无需阻塞)
        System.exit(0);
    }

    /**
     * 主方法:启动AIO客户端
     */
    public static void main(String[] args) {
        new AioClient("localhost", 8080);
        
        // 防止主线程退出(AIO依赖底层线程池,主线程退出会导致客户端终止)
        try {
            Thread.sleep(Long.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

用「快递驿站」的现实场景,拆解类的设计逻辑

我们把 FakeAIOFramework 对应成「小区快递驿站」,用最贴近生活的分工,讲清楚 “类→方法→线程→回调” 的设计思路:

第一步:先定义现实中的“角色”(对应代码中的“类”)

现实角色 代码中的类/组件 核心职责
快递驿站(驿站老板) FakeAIOFramework 接收用户的快递配送请求、安排快递员(线程)干活、按用户要求完成后续动作
快递员 方法内创建的 Thread 线程 执行具体的“配送快递”任务(耗时操作)
小区用户 调用驿站的人(main方法) 发起配送请求、定义“快递送到后要做的事”(回调规则)
用户的“收货规则” CompletionHandler 接口/对象 用户告诉驿站:“送到放门口(completed)/送不到打电话(failed)”

第二步:现实场景完整流程(对应代码执行逻辑)

我们一步步还原“用户找驿站寄快递”的过程,对应代码的设计思路:

  1. 先定义“用户的收货规则”(对应 CompletionHandler 接口)

现实中:用户需要先告诉驿站“快递送到后该怎么做”,这个“规则”是固定的(要么成功、要么失败)。 代码中:用接口定义回调规则(只有方法签名,没有具体实现),就像驿站给用户一张“收货确认单”,只留“成功/失败”两个填写栏。

1
2
3
4
5
6
7
// 对应现实:驿站的“收货确认单”(只定规则,不填内容)
interface ReceiveRule {
    // 快递送到了该做什么
    void onSuccess(String msg);
    // 快递送不到该做什么
    void onFailed(String msg);
}
  1. 设计“快递驿站”类(对应 FakeAIOFramework

现实中:驿站的核心职责是“接请求→派快递员→按用户规则执行”,对应代码里“类→方法→线程→回调”的设计:

 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
// 对应现实:小区快递驿站类
class CourierStation {
    // 驿站的核心方法:接收“配送请求”
    // 参数:用户的收货规则(ReceiveRule)+ 快递信息
    public void sendCourier(ReceiveRule rule, String expressInfo) {
        // 驿站老板不会自己送快递(主线程不做耗时操作),安排快递员(创建线程)去送
        new Thread(() -> {
            // 快递员执行“配送”耗时操作(对应AIO的IO操作)
            System.out.println("快递员开始配送:" + expressInfo);
            try {
                // 模拟配送耗时(比如3分钟)
                Thread.sleep(3000);
                
                // 配送成功:按用户的规则执行(调用onSuccess)
                rule.onSuccess(expressInfo + " 已送到,放家门口了");
            } catch (Exception e) {
                // 配送失败:按用户的规则执行(调用onFailed)
                rule.onFailed(expressInfo + " 配送失败,联系不上收件人");
            }
        }).start();
        
        // 驿站老板安排完快递员,就去接待下一个用户(主线程不阻塞)
        System.out.println("驿站已接单,快递员正在配送中,你可以先去忙了");
    }
}
  1. 用户使用驿站(对应 main 方法)

现实中:用户找驿站寄快递,告诉驿站“收货规则”,然后该干嘛干嘛,不用等快递员。

 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
public class TestCourier {
    public static void main(String[] args) {
        // 1. 先创建驿站对象(老板)
        CourierStation station = new CourierStation();
        
        // 2. 用户定义自己的“收货规则”(填写驿站的确认单)
        ReceiveRule myRule = new ReceiveRule() {
            @Override
            public void onSuccess(String msg) {
                System.out.println("用户收到通知:" + msg);
            }
            
            @Override
            public void onFailed(String msg) {
                System.out.println("用户收到通知:" + msg);
            }
        };
        
        // 3. 用户发起配送请求(告诉驿站“寄快递”,并把规则传过去)
        station.sendCourier(myRule, "京东快递-手机包裹");
        
        // 4. 用户发起请求后,继续做自己的事(比如做饭),不用等快递员
        System.out.println("用户:我先去做饭,快递到了按我的规则来就行");
        
        // 防止主线程退出,等快递配送完成
        try {Thread.sleep(4000);} catch (Exception e) {}
    }
}

运行结果(对应现实流程)

驿站已接单,快递员正在配送中,你可以先去忙了
用户:我先去做饭,快递到了按我的规则来就行
快递员开始配送:京东快递-手机包裹
用户收到通知:京东快递-手机包裹 已送到,放家门口了

优缺点

  • 优点:完全异步,线程利用率最高;适合高吞吐量场景
  • 缺点:Java实现较少使用(Linux支持有限);代码复杂度高;调试困难

适用场景:高吞吐、IO密集型应用,如文件服务器、视频流服务器等

2.4 IO模型对比总结

模型 阻塞方式 线程资源 数据单位 适用场景 Java实现
BIO 同步阻塞 每个连接一个线程 流(Stream) 低并发、简单逻辑 ServerSocket, Socket
NIO 同步非阻塞 单线程管理多连接 块(Buffer) 高并发、高性能 Selector, Channel
AIO 异步非阻塞 操作系统回调通知 块(Buffer) 高吞吐、IO密集 AsynchronousChannel

用生活例子再理解:NIO 非阻塞 vs BIO 阻塞 vs AIO 异步非阻塞

场景 BIO 阻塞(一个服务员盯一桌) NIO 非阻塞(一个经理盯所有桌) AIO 异步非阻塞(智能餐厅系统)
等待连接 服务员站在门口,不等到客人来就不做任何事(accept() 阻塞)。 经理偶尔看一眼门口,没人就去巡厅(accept() 返回 null,线程处理其他事),有人就安排座位。 餐厅装了智能门禁,客人进门自动触发通知(异步 accept() 回调),经理不用盯门口,收到通知再安排座位。
等待点餐 服务员站在餐桌旁,客人不点餐就一直等(read() 阻塞),啥也干不了。 经理巡厅时看餐桌:客人举手(有数据)才上前服务(处理 read()),没举手就去看其他桌(线程处理其他通道)。 每张餐桌装了智能点餐屏,客人点完餐自动推送订单到经理终端(异步 read() 回调),经理不用巡厅,收到订单再安排出餐。
上菜(回写) 服务员拿着菜站在餐桌旁,等客人接菜(write() 阻塞),菜没送完不走。 经理确认餐桌有空(通道可写),才让服务员上菜(write() 非阻塞,没送完就先处理其他桌)。 智能传菜机器人自动送餐,送完后给经理发完成通知(异步 write() 回调),经理不用管送餐过程,等通知即可。
阻塞/等待点 每个服务员都卡在“等客人”/“等点餐”/“等接菜”,全员闲置。 经理只在“办公室(Selector)”等“有人举手的通知”(select() 阻塞),通知到了才去处理,没通知就休息(不浪费CPU)。 经理完全不等待:所有流程(客人进门/点餐/收菜)都由智能设备自动触发回调,经理只处理“已完成的事件”,全程无主动等待/阻塞。
资源占用 100 桌需要 100 个服务员(线程),哪怕 99 桌没人,服务员也得站着。 100 桌只需要 1 个经理(线程),经理同时盯所有桌的需求。 100 桌只需要 1 个经理(线程),且经理不用主动盯,等智能系统推送任务即可。
核心特点 同步阻塞,线程与连接一一绑定,资源浪费严重。 同步非阻塞,单线程多路复用,仅在 Selector 处可控阻塞,资源利用率大幅提升。 异步非阻塞,全程无主动阻塞,IO 操作由操作系统完成后回调通知,资源利用率最高。

选择建议

  • 低并发场景(< 1000连接):BIO 简单易用
  • 高并发场景(1000-10000连接):NIO 性能优势明显
  • 极高并发场景(> 10000连接):AIO 或 NIO框架(如Netty)

实际开发中的选择

  • 简单应用:直接使用BIO
  • 高性能应用:使用NIO框架如Netty,简化编程
  • 特殊场景:根据具体需求选择合适的IO模型

3. 流的方向

所有 Java IO 的命名,全部站在「你的程序」视角:

graph TB
    A[流的方向] --> B[输入流]
    A --> C[输出流]
    B --> D[将<存储设备>中的内容读入到<内存>中]
    C --> E[将<内存>中的内容写入到<存储设备>中]

    AA[文件] --输入流--> BB[程序] --输处流--> CC[文件]

InputStream(输入流)= 数据 进入 程序 = 读
OutputStream(输出流)= 数据 离开 程序 = 写

最生活化的例子(秒懂)
你 = 程序
水 = 数据

🍵 InputStream = 喝水

  • 水从杯子 → 进入你的嘴里
  • 入 = 读

🥤 OutputStream = 吐水 / 倒水

  • 水从你嘴里 → 倒出去
  • 出 = 写

总结

In → 进 → 读(数据进程序)
out → 出 → 写(数据出程序)

xxxInputStream = 从 xxx 读
xxxOutputStream = 向 xxx 写

1. 输入流(InputStream/Reader)

  1. 流动方向
    数据从外部资源流向程序内部
    用于读取数据,方向是从对方(客户端/服务器)到自己。
    例如:文件 → 程序、网络套接字 → 程序、内存字节数组 → 程序。

  2. 数据源

    • 输入流的数据源取决于具体实现类:

      • FileInputStream → 数据源是文件
      • ByteArrayInputStream → 数据源是字节数组
      • Socket.getInputStream() → 数据源是网络连接的另一端
      • System.in(标准输入流)→ 数据源是控制台输入
    • 如何判断数据源
      观察流的构造函数或获取方式。例如:

      1
      
      InputStream is = new FileInputStream("data.txt"); // 数据源是文件data.txt
      

2. 输出流(OutputStream/Writer)

  1. 流动方向
    数据从程序内部流向外部资源
    用于写入数据,方向是从自己到对方(客户端/服务器)。
    例如:程序 → 文件、程序 → 网络套接字、程序 → 内存字节数组。

  2. 数据目的地

    • 输出流的目的地取决于具体实现类:

      • FileOutputStream → 目的地是文件
      • ByteArrayOutputStream → 目的地是字节数组
      • Socket.getOutputStream() → 目的地是网络连接的另一端
      • System.out(标准输出流)→ 目的地是控制台
    • 如何判断目的地
      观察流的构造函数或获取方式。例如:

      1
      
      OutputStream os = new FileOutputStream("output.txt"); // 目的地是文件output.txt
      
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. 注意事项
  1. 装饰器模式的影响
    即使使用装饰器(如BufferedInputStream),底层数据源仍由最基础的流决定。
    例如:

    1
    2
    
    InputStream is = new BufferedInputStream(new FileInputStream("data.txt")); 
    // 实际数据源仍是文件data.txt
    
  2. 资源与程序的视角

    • 输入流:程序是数据的消费者,外部资源是数据的提供者
    • 输出流:程序是数据的生产者,外部资源是数据的接收者

通过构造函数或获取方式明确数据源/目的地,是理解I/O流向的核心。

4. 流的分类

在 Java IO 流框架中,字节流处理流(装饰器流)是两种不同角色的流类。

1. 字节流(Byte Streams)

字节流是直接操作原始字节的流,负责与底层数据源(如文件、内存等)进行直接的字节读写。
核心特征

  • 继承自 InputStreamOutputStream
  • 直接操作二进制数据(如图片、音频等非文本文件)。

典型实现类

类名 功能描述
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)

处理流是装饰器模式的体现,通过包装其他流(字节流或字符流)来增强功能(如缓冲、数据类型转换等)。
核心特征

  • 继承自 FilterInputStreamFilterOutputStream(字节流装饰器基类)。
  • 不能独立使用,必须包装一个已有的流。

典型实现类

类名 功能描述 包装对象示例
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 操作次数,提升性能。
  • 数据类型支持:直接读写 intString 等类型,无需手动转换字节。
  • 对象序列化:简化对象的持久化和传输。

这种设计使得 Java IO 流框架高度灵活,能够按需组合功能,避免继承体系的臃肿。

6. InputStream

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<读取反序列化对象>]

代码示例

1. FileInputStream

场景:读取文件中的原始字节(如图片、视频等非文本文件)。
技巧:使用 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();
}

2. ByteArrayInputStream

场景:从内存中的字节数组读取数据(如模拟输入流)。
技巧:无需关闭流,但显式关闭是良好习惯。

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();
}

3. PipedInputStream

场景:线程间通过管道传递字节数据(生产者-消费者模型)。
技巧:必须与 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();

4. BufferedInputStream

场景:为其他输入流提供缓冲,减少 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();
}

5. DataInputStream

场景:读取二进制文件中的基本数据类型(如 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();
}

6. PushbackInputStream

场景:解析数据时回退已读取的字节(如处理自定义分隔符)。
技巧:通过 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();
}

7. ObjectInputStream

场景:反序列化对象(从文件或网络恢复对象状态)。
技巧:对象需实现 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 场景。

7. OutputStream

graph LR
    A[OutputStream] --> B[节点流]
    A --> C[处理流]
    B --> D[FileOutputStream<向文件写入字节>]
    B --> E[ByteArrayOutputStream<向字节数组写入字节>]
    B --> F[PipedOutputStream<用于线程间通信>]

    C --> H[FilterOutputStream<装饰器模式基类>]
    H --> I[BufferedOutputStream<缓冲提升性能>]
    H --> J[DataOutputStream<写入基本数据类型(int, double)>]
    C --> L[ObjectOutputStream<写入序列化对象>]

代码示例

1. FileOutputStream

场景:向文件写入原始字节(如保存图片、视频等非文本文件)。
技巧:使用 try-with-resources 自动关闭资源;可设置 append 参数决定是否追加内容。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
try (FileOutputStream fos = new FileOutputStream("output.bin")) {
    byte[] data = "Hello FileOutputStream".getBytes();
    fos.write(data); // 写入字节数组
    System.out.println("数据写入成功");
} catch (IOException e) {
    e.printStackTrace();
}

// 追加模式
try (FileOutputStream fos = new FileOutputStream("output.bin", true)) {
    byte[] data = "\nAppended data".getBytes();
    fos.write(data); // 追加写入
    System.out.println("数据追加成功");
} catch (IOException e) {
    e.printStackTrace();
}

2. ByteArrayOutputStream

场景:在内存中收集字节数据(如动态生成数据)。
技巧:通过 toByteArray() 获取最终字节数组;无需关闭流。

1
2
3
4
5
6
7
8
9
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
    baos.write("Hello ".getBytes());
    baos.write("ByteArray".getBytes());
    
    byte[] result = baos.toByteArray();
    System.out.println(new String(result)); // 输出: Hello ByteArray
} catch (IOException e) {
    e.printStackTrace();
}

3. PipedOutputStream

场景:线程间通过管道传递字节数据(与 PipedInputStream 配合)。
技巧:必须与 PipedInputStream 连接,且需处理线程同步。

 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
PipedInputStream pis = new PipedInputStream();
// 生产者线程:pos.write(...)→ 往管道里写数据
// 数据自动流向连接好的 pis
// 消费者线程:pis.read()→ 从管道读数据
PipedOutputStream pos = new PipedOutputStream(pis); // 直接连接,pos(写) → 连接 → pis(读)

// 生产者线程写入数据
new Thread(() -> {
    try {
        pos.write("Data to consumer".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 to consumer
        }
        pis.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}).start();

4. BufferedOutputStream

场景:为其他输出流提供缓冲,减少 I/O 操作次数,提升写入效率。
技巧:默认缓冲区大小 8KB,可根据场景调整;记得调用 flush() 确保数据写入。

1
2
3
4
5
6
7
8
9
try (FileOutputStream fos = new FileOutputStream("output.bin");
     BufferedOutputStream bos = new BufferedOutputStream(fos)) { // 包装文件流
    bos.write("Buffered ".getBytes());
    bos.write("Output".getBytes());
    bos.flush(); // 确保数据写入文件
    System.out.println("数据写入成功");
} catch (IOException e) {
    e.printStackTrace();
}

5. DataOutputStream

场景:向二进制文件写入基本数据类型(如 int、double)。
技巧:需确保写入顺序与后续读取顺序一致(通常搭配 DataInputStream 使用)。

1
2
3
4
5
6
7
8
9
try (FileOutputStream fos = new FileOutputStream("data.bin");
     DataOutputStream dos = new DataOutputStream(fos)) {
    dos.writeInt(100);          // 写入 int
    dos.writeDouble(3.14);      // 写入 double
    dos.writeUTF("Hello");      // 写入 UTF 字符串
    System.out.println("数据类型写入成功");
} catch (IOException e) {
    e.printStackTrace();
}

6. ObjectOutputStream

场景:序列化对象(将对象状态保存到文件或网络)。
技巧:对象需实现 Serializable 接口,序列化与反序列化版本号需一致。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class User implements Serializable {
    private static final long serialVersionUID = 1L;
    String name;
    public User(String name) { this.name = name; }
}

// 序列化对象到文件
try (FileOutputStream fos = new FileOutputStream("user.bin");
     ObjectOutputStream oos = new ObjectOutputStream(fos)) {
    User user = new User("Alice");
    oos.writeObject(user);
    System.out.println("对象序列化成功");
} catch (IOException e) {
    e.printStackTrace();
}

总结

核心场景 关键技巧
FileOutputStream 写入文件原始字节 设置 append 参数控制追加模式
ByteArrayOutputStream 内存字节数据收集 使用 toByteArray() 获取结果
PipedOutputStream 线程间管道通信 必须连接 PipedInputStream
BufferedOutputStream 提升写入效率 记得调用 flush() 确保数据写入
DataOutputStream 写入基本数据类型 需与 DataInputStream 配对使用
ObjectOutputStream 序列化对象 确保对象实现 Serializable 接口

8. 字符流(Reader/Writer)

定义:字符流是以字符为单位的 I/O 流,专为处理文本数据设计,自动处理字符编码问题。

核心特点

  • 基于字符(2字节 Unicode)操作
  • 自动处理字符编码转换
  • 适用于文本文件的读写

核心类层次

graph LR
    A[Reader] --> B[节点流]
    A --> C[处理流]
    B --> D[FileReader<读取文件字符>]
    B --> E[CharArrayReader<读取字符数组>]
    B --> F[StringReader<读取字符串>]
    B --> G[PipedReader<线程间通信>]
    C --> H[BufferedReader<缓冲提升性能>]
    C --> J[PushbackReader<支持回退字符>]
    A --> K[桥接流]
    K --> L[InputStreamReader<字节流转字符流>]

8.1 Reader 系列

1. FileReader

场景:读取文本文件的字符数据。
技巧:默认使用平台编码,如需指定编码应使用 InputStreamReader

1
2
3
4
5
6
7
8
try (FileReader fr = new FileReader("text.txt")) {
    int charData;
    while ((charData = fr.read()) != -1) {
        System.out.print((char) charData); // 逐字符读取
    }
} catch (IOException e) {
    e.printStackTrace();
}
2. 节点流:CharArrayReader(读字符数组)

场景:从内存中的 char[] 读取字符(内存流)

1
2
3
4
5
6
7
8
char[] charArray = {'H', 'e', 'l', 'l', 'o'};

try (CharArrayReader reader = new CharArrayReader(charArray)) {
    int data;
    while ((data = reader.read()) != -1) {
        System.out.print((char) data); // 输出 Hello
    }
}
3. 节点流:StringReader(读字符串)

场景:直接从字符串读取字符(内存流)

1
2
3
4
5
6
7
8
String str = "我是字符串,Reader 直接读我!";

try (StringReader reader = new StringReader(str)) {
    int c;
    while ((c = reader.read()) != -1) {
        System.out.print((char) c);
    }
}
4. 节点流:PipedReader(线程间通信)

场景:和 PipedWriter 配对,用于两个线程之间管道通信

 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
PipedReader pr = new PipedReader();
PipedWriter pw = new PipedWriter();
pr.connect(pw); // 必须连接,因为 PipedReader.connect() 内部直接转发给了 PipedWriter.connect(),所以两者谁连谁都可以

// 线程1:写入
new Thread(() -> {
    try {
        pw.write("来自管道的消息");
        pw.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}).start();

// 线程2:读取
new Thread(() -> {
    try {
        int c;
        while ((c = pr.read()) != -1) {
            System.out.print((char) c);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}).start();
5. 处理流:BufferedReader(缓冲字符流)

场景:包装字符流,提高效率,支持 readLine()
技巧:使用 readLine() 按行读取文本,方便处理文本文件。

1
2
3
4
5
6
7
8
9
try (FileReader fr = new FileReader("text.txt");
     BufferedReader br = new BufferedReader(fr)) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}
6. 处理流:PushbackReader(字符回退流)

场景:读取一个字符后,把它“推回”流中,用于解析语法、词法分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
char[] arr = {'A', 'B', 'C'};
CharArrayReader reader = new CharArrayReader(arr);

try (PushbackReader pbr = new PushbackReader(reader)) {
    int c1 = pbr.read(); // 读 A
    System.out.println((char) c1);

    pbr.unread(c1); // 把 A 推回流里

    int c2 = pbr.read(); // 再次读到 A
    System.out.println((char) c2);
}
7. 桥接流:InputStreamReader(字节 → 字符)

场景:字节流转字符流,可指定编码(UTF-8),解决乱码神器
技巧:处理网络或文件输入时,明确指定编码可避免乱码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
try (FileInputStream fis = new FileInputStream("text.txt");
     InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
     BufferedReader br = new BufferedReader(isr)) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}

8.2 Writer 系列

graph LR
    AA[Writer] --> BB[节点流]
    AA --> CC[处理流]
    BB --> DD[FileWriter<写入文件字符>]
    BB --> EE[CharArrayWriter<写入字符数组>]
    BB --> FF[StringWriter<写入字符串>]
    BB --> GG[PipedWriter<线程间通信>]
    CC --> HH[BufferedWriter<缓冲提升性能>]
    CC --> JJ[PrintWriter<格式化输出>]
    AA --> KK[桥接流]   
    KK --> LL[OutputStreamWriter<字符流转字节流>]
1. FileWriter

场景:向文本文件写入字符数据。
技巧:默认使用平台编码,如需指定编码应使用 OutputStreamWriter

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
try (FileWriter fw = new FileWriter("output.txt")) {
    fw.write("Hello FileWriter");
    fw.write("\nSecond line");
    System.out.println("文本写入成功");
} catch (IOException e) {
    e.printStackTrace();
}

// 追加模式
try (FileWriter fw = new FileWriter("output.txt", true)) {
    fw.write("\nAppended line");
    System.out.println("文本追加成功");
} catch (IOException e) {
    e.printStackTrace();
}
2. CharArrayWriter

场景:将字符写入内存中的字符数组,适合临时缓存字符数据。
技巧:最后通过 toCharArray() 获取结果,无需IO操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
try (CharArrayWriter caw = new CharArrayWriter()) {
    caw.write("Hello");
    caw.write(' ');
    caw.write("CharArrayWriter");

    // 从内存流中获取字符数组
    char[] charArray = caw.toCharArray();
    System.out.println(new String(charArray));
} catch (IOException e) {
    e.printStackTrace();
}
3. StringWriter

场景:将字符写入内存字符串缓冲区,最后获取完整字符串。
技巧:纯内存操作,无IO异常,适合拼接字符串。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
try (StringWriter sw = new StringWriter()) {
    sw.write("我");
    sw.write("是");
    sw.write("StringWriter");

    // 获取最终字符串
    String result = sw.toString();
    System.out.println(result);
} catch (IOException e) {
    e.printStackTrace();
}
4. PipedWriter

场景:与 PipedReader 配对,用于线程之间管道通信
技巧:必须先连接,再启动线程。

 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
PipedReader pr = new PipedReader();
PipedWriter pw = new PipedWriter();
pr.connect(pw); // 连接管道

// 线程1:写入
new Thread(() -> {
    try {
        pw.write("来自线程一的管道消息");
        pw.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}).start();

// 线程2:读取
new Thread(() -> {
    try {
        int c;
        while ((c = pr.read()) != -1) {
            System.out.print((char) c);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}).start();
5. BufferedWriter

场景:为字符输出流提供缓冲,减少 I/O 操作次数,提升写入效率。
技巧:使用 newLine() 写入平台无关的换行符。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
try (FileWriter fw = new FileWriter("output.txt");
     BufferedWriter bw = new BufferedWriter(fw)) {
    bw.write("Buffered");
    bw.newLine(); // 平台无关的换行
    bw.write("Writer");
    bw.flush(); // 确保数据写入
    System.out.println("文本写入成功");
} catch (IOException e) {
    e.printStackTrace();
}
6. PrintWriter

场景:提供格式化打印功能,如 print、println、printf,非常常用。
技巧:不会抛出IO异常,可自动刷新,兼容所有字符流。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 直接包装文件
try (PrintWriter pw = new PrintWriter("print.txt")) {
    pw.println("Hello PrintWriter");
    pw.print("整数:");
    pw.println(100);
    pw.printf("浮点数:%.2f", 3.14159);
} catch (IOException e) {
    e.printStackTrace();
}

// 包装 BufferedWriter
try (FileWriter fw = new FileWriter("print2.txt");
     BufferedWriter bw = new BufferedWriter(fw);
     PrintWriter pw = new PrintWriter(bw)) {
    pw.println("使用缓冲的PrintWriter");
    pw.println("非常常用!");
}
7. OutputStreamWriter

场景:将字符流转换为字节流,支持指定字符编码。
技巧:处理网络或文件输出时,明确指定编码可避免乱码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
try (FileOutputStream fos = new FileOutputStream("output.txt");
     OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8"); // 指定编码
     BufferedWriter bw = new BufferedWriter(osw)) {
    bw.write("UTF-8 encoded text");
    bw.newLine();
    bw.write("中文测试");
    System.out.println("编码文本写入成功");
} catch (IOException e) {
    e.printStackTrace();
}

字节流 vs 字符流

特性 字节流 字符流
操作单位 字节(8位) 字符(16位 Unicode)
核心类 InputStream/OutputStream Reader/Writer
适用场景 二进制文件(图片、视频等) 文本文件(.txt、.java 等)
编码处理 需手动处理编码 自动处理编码转换
缓冲效率 BufferedInputStream BufferedReader
特殊功能 readLine() 按行读取

选择建议

  • 处理二进制数据(如图片、视频):使用字节流
  • 处理文本数据(如配置文件、日志):使用字符流
  • 需指定编码时:使用 InputStreamReader/OutputStreamWriter
  • 需要缓冲提高性能时:使用 Buffered 系列流

9. 节点流与处理流总结

节点流:直接连接数据源或目的地的流,是 I/O 操作的基础。
处理流:通过装饰器模式包装其他流,增强功能或提高性能。

核心区别

特性 节点流 处理流
数据源 直接连接物理资源 包装其他流
功能 基础读写操作 增强功能(缓冲、转换等)
依赖关系 独立使用 必须包装其他流
典型实现 FileInputStream BufferedInputStream
使用场景 底层 I/O 操作 提升性能或功能扩展

组合使用示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 最佳实践:文件 → 缓冲 → 数据类型处理(字节流)
try (FileInputStream fis = new FileInputStream("data.bin");
     BufferedInputStream bis = new BufferedInputStream(fis);
     DataInputStream dis = new DataInputStream(bis)) {
    // 读取数据
}

// 最佳实践:文件 → 编码转换 → 缓冲(字符流)
try (FileInputStream fis = new FileInputStream("text.txt");
     InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
     BufferedReader br = new BufferedReader(isr)) {
    // 读取文本
}

// 最佳实践:文件 → 编码转换 → 缓冲 → 格式化输出(字符流)
try (FileOutputStream fos = new FileOutputStream("output.txt");
     OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
     BufferedWriter bw = new BufferedWriter(osw);
     PrintWriter pw = new PrintWriter(bw)) {
    // 写入文本
}

10. IO 最佳实践

10.1 资源管理

使用 try-with-resources:自动关闭实现了 AutoCloseable 接口的资源,避免内存泄漏。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 推荐:try-with-resources 自动关闭资源
try (FileInputStream fis = new FileInputStream("file.txt")) {
    // 操作流
} catch (IOException e) {
    e.printStackTrace();
}

// 不推荐:手动关闭资源
try {
    FileInputStream fis = new FileInputStream("file.txt");
    // 操作流
    fis.close(); // 可能因异常而未执行
} catch (IOException e) {
    e.printStackTrace();
}

10.2 性能优化

使用缓冲流:减少 I/O 操作次数,提升读写效率。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 推荐:使用缓冲流
try (FileInputStream fis = new FileInputStream("largefile.txt");
     BufferedInputStream bis = new BufferedInputStream(fis)) {
    // 按块读取
    byte[] buffer = new byte[8192]; // 8KB 缓冲区
    int bytesRead;
    while ((bytesRead = bis.read(buffer)) != -1) {
        // 处理数据
    }
}

// 不推荐:无缓冲逐字节读取
try (FileInputStream fis = new FileInputStream("largefile.txt")) {
    int byteData;
    while ((byteData = fis.read()) != -1) {
        // 处理数据(效率低)
    }
}

合理设置缓冲区大小:根据数据量和系统内存调整缓冲区大小。

  • 小文件:默认 8KB 缓冲区即可
  • 大文件:可适当增大缓冲区(如 16KB、32KB)
  • 内存受限:减小缓冲区大小

10.3 编码处理

明确指定字符编码:避免平台编码差异导致的乱码问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 推荐:明确指定编码
try (FileInputStream fis = new FileInputStream("text.txt");
     InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8);
     BufferedReader br = new BufferedReader(isr)) {
    // 读取文本
}

// 不推荐:依赖平台默认编码
try (FileReader fr = new FileReader("text.txt")) {
    // 读取文本(可能因平台编码不同而乱码)
}

使用 StandardCharsets:避免硬编码编码名称,提高代码可维护性。

10.4 异常处理

合理处理 I/O 异常:根据实际场景选择合适的异常处理策略。

1
2
3
4
5
6
7
8
9
try (FileInputStream fis = new FileInputStream("file.txt")) {
    // 操作流
} catch (FileNotFoundException e) {
    // 文件不存在的特殊处理
    System.err.println("文件未找到: " + e.getMessage());
} catch (IOException e) {
    // 其他 I/O 异常处理
    e.printStackTrace();
}

10.5 NIO 与 NIO.2

NIO.2概念:JDK 推出的升级版 NIO,包含两大核心能力:

1)全新文件系统API(Path、Files、Paths)——用于简化文件操作、文件监控、遍历、复制等。
2)AIO 异步IO(AsynchronousServerSocketChannel / AsynchronousFileChannel)——真正的异步非阻塞IO。

NIO.2 是一个 “大套装”,AIO 只是 NIO.2 里面的 “一个功能模块”!

NIO.2(整体)
├─> 文件系统API(Path、Files、Paths)→ 同步、简单、常用
└─> AIO 异步IO(AsynchronousChannel)→ 真正异步、不常用

NIO 适用场景:Java 1.4 推出的 同步非阻塞 IO(New IO / Non-blocking IO),核心API:ServerSocketChannel、SocketChannel、Selector、FileChannel。高并发网络应用,如服务器、聊天室等。
NIO.2 适用场景:文件操作、文件监控、异步网络、异步文件IO。

NIO.2 文件操作示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 使用 NIO.2 读取文件
Path path = Paths.get("file.txt");
try {
    byte[] data = Files.readAllBytes(path);
    System.out.println(new String(data, StandardCharsets.UTF_8));
} catch (IOException e) {
    e.printStackTrace();
}

// 使用 NIO.2 写入文件
try {
    String content = "Hello NIO.2";
    Files.write(path, content.getBytes(StandardCharsets.UTF_8));
    System.out.println("文件写入成功");
} catch (IOException e) {
    e.printStackTrace();
}

10.6 常见陷阱与解决方案

陷阱 解决方案
资源未关闭 使用 try-with-resources
缓冲区未刷新 调用 flush() 或使用自动刷新的流
编码不一致 明确指定编码,使用 StandardCharsets
大文件内存溢出 分块读取,使用缓冲流
网络 I/O 阻塞 使用 NIO 或 NIO 框架(如 Netty)
文件路径处理错误 使用 PathsFiles 类(NIO.2)
异常处理不当 分层捕获,根据场景处理

11. 综合示例:文件复制

字节流实现:适用于所有文件类型(推荐)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void copyFile(String sourcePath, String targetPath) {
    Path source = Paths.get(sourcePath);
    Path target = Paths.get(targetPath);
    
    try (BufferedInputStream bis = new BufferedInputStream(Files.newInputStream(source));
         BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(target))) {
        
        byte[] buffer = new byte[8192]; // 8KB 缓冲区
        int bytesRead;
        while ((bytesRead = bis.read(buffer)) != -1) {
            bos.write(buffer, 0, bytesRead);
        }
        
        bos.flush();
        System.out.println("文件复制成功: " + sourcePath + " -> " + targetPath);
        
    } catch (IOException e) {
        System.err.println("文件复制失败: " + e.getMessage());
        e.printStackTrace();
    }
}

// 调用示例
copyFile("source.jpg", "target.jpg");

字符流实现:仅适用于文本文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void copyTextFile(String sourcePath, String targetPath) {
    try (BufferedReader br = new BufferedReader(
             new InputStreamReader(Files.newInputStream(Paths.get(sourcePath)), StandardCharsets.UTF_8));
         BufferedWriter bw = new BufferedWriter(
             new OutputStreamWriter(Files.newOutputStream(Paths.get(targetPath)), StandardCharsets.UTF_8))) {
        
        String line;
        while ((line = br.readLine()) != null) {
            bw.write(line);
            bw.newLine(); // 保持换行
        }
        
        bw.flush();
        System.out.println("文本文件复制成功");
        
    } catch (IOException e) {
        System.err.println("文本文件复制失败: " + e.getMessage());
        e.printStackTrace();
    }
}

// 调用示例
copyTextFile("source.txt", "target.txt");

NIO.2 实现:更简洁的文件复制

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public static void copyFileNio(String sourcePath, String targetPath) {
    Path source = Paths.get(sourcePath);
    Path target = Paths.get(targetPath);
    
    try {
        Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
        System.out.println("NIO 文件复制成功");
    } catch (IOException e) {
        System.err.println("NIO 文件复制失败: " + e.getMessage());
        e.printStackTrace();
    }
}

// 调用示例
copyFileNio("source.jpg", "target.jpg");

总结

Java IO 是 Java 编程中不可或缺的一部分,从传统的 BIO 到现代的 NIO 和 AIO,Java 提供了丰富的 I/O 工具和 API。本文系统介绍了:

  1. IO 模型:BIO(同步阻塞)、NIO(同步非阻塞)、AIO(异步非阻塞)的特点和适用场景
  2. 流的分类:字节流与字符流、输入流与输出流、节点流与处理流
  3. 核心类InputStream/OutputStreamReader/Writer 及其常用实现
  4. NIO 组件:Buffer、Channel、Selector 的工作原理和使用方法
  5. 最佳实践:资源管理、性能优化、编码处理、异常处理等

通过合理选择和组合使用这些 I/O 工具,可以高效处理各种数据传输场景,提升应用程序的性能和可靠性。

关键要点

  • 根据数据类型选择合适的流(字节流/字符流)
  • 始终使用 try-with-resources 管理资源
  • 对于大文件或频繁 I/O 操作,使用缓冲流提升性能
  • 明确指定字符编码,避免乱码问题
  • 高并发场景下考虑使用 NIO 或 NIO 框架
  • 利用 NIO.2 提供的现代文件操作 API

掌握 Java IO 的核心概念和最佳实践,将为你的 Java 后端开发奠定坚实的基础。