背景

本文是《Java 后端从小白到大神》修仙系列第八篇,正式进入Java后端世界,本篇文章主要聊Java基础。若想详细学习请点击首篇博文,我们开始把。

文章概览

  1. 核心工具类(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的核心原理
  1. 阻塞模式

    • 服务端调用accept()监听客户端连接时,线程被阻塞,直到有连接到达。
    • 客户端/服务端调用read()write()时,线程被阻塞,直到数据就绪或传输完成。
  2. 线程模型

    • 一连接一线程:每个客户端连接需分配一个独立线程处理。
    • 资源消耗:线程数随连接数线性增长,高并发时易导致线程资源耗尽。
  3. 适用场景

    • 客户端连接数较少(如内部系统)。
    • 简单、低并发的网络应用(如传统HTTP服务器)。
3. BIO的核心组成
组件 作用
ServerSocket 服务端监听指定端口,等待客户端连接(阻塞在accept()方法)。
Socket 客户端与服务端建立连接的端点,或服务端接受的客户端连接对象。
InputStream 从Socket中读取数据的输入流(阻塞式读取)。
OutputStream 向Socket中写入数据的输出流(阻塞式写入)。
4. BIO代码示例
  1. 服务端代码
 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. 客户端流程
 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.代码注释解析
  1. 服务端流程

    • serverSocket.accept():阻塞等待客户端连接。
    • 每接收到一个连接,创建新线程处理读写操作。
    • in.read(buffer):阻塞读取客户端数据。
    • out.write():阻塞写入响应数据。
  2. 客户端流程

    • 用户输入消息后通过out.write()发送到服务端。
    • in.read(buffer):阻塞等待服务端响应。
  3. 多线程必要性

    • 服务端需为每个客户端分配独立线程,否则无法同时处理多个连接。
    • 客户端使用多线程分离发送和接收操作,避免阻塞用户输入。
6. BIO与NIO对比
特性 BIO NIO
阻塞模式 完全阻塞 非阻塞/多路复用
线程模型 一连接一线程 单线程处理多连接(Selector)
数据单位 流(Stream) 块(Buffer)
适用场景 低并发、简单应用 高并发、高性能需求
资源消耗 线程数随连接数线性增长 线程数固定,资源利用率高
7. BIO的优缺点
  • 优点

    • 代码简单,易于理解和调试。
    • 适合连接数少、业务逻辑简单的场景(如内部管理系统)。
  • 缺点

    • 线程阻塞导致资源浪费。
    • 高并发时线程数激增,可能引发内存溢出或线程调度性能下降。
8. 关键注意事项
  1. 线程资源管理
    需限制最大线程数(如使用线程池),避免高并发时线程爆炸。
  2. 异常处理
    确保关闭SocketServerSocket,防止资源泄漏。
  3. 性能瓶颈
    在频繁短连接的场景下,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的核心原理
  1. 非阻塞模式
    线程不会因等待I/O操作而阻塞,可处理其他任务。
  2. 多路复用
    单个线程通过Selector监控多个通道(Channel)的就绪事件(如连接、读、写),减少线程资源消耗。
  3. 缓冲区导向
    数据通过Buffer块传输,而非传统IO的逐字节流。
3. NIO核心组件

1. Buffer(缓冲区)

  • 作用:临时存储数据,支持批量读写。
  • 类型ByteBufferCharBufferIntBuffer等。
  • 关键操作
    • 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. 服务端代码
 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. 客户端流程
  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.代码注释解析
  1. Selector创建
    使用Selector.open()创建一个选择器,用于监控多个通道事件。
  2. ServerSocketChannel配置
    • 绑定端口并设置为非阻塞模式(configureBlocking(false))。
    • 注册OP_ACCEPT事件,表示监听新连接。
  3. 事件循环
    • selector.select()阻塞直到有事件就绪。
    • 遍历selectedKeys处理所有就绪事件。
  4. 处理ACCEPT事件
    • 当新客户端连接时,接受并注册其SocketChannel到Selector,监听OP_READ事件。
  5. 处理READ事件
    • 读取客户端数据到Buffer,处理后回写响应。
7. NIO与传统IO对比
特性 传统IO NIO
阻塞模式 阻塞 非阻塞
数据单位 流(Stream) 块(Buffer)
线程模型 一连接一线程 单线程多连接(Selector)
适用场景 低并发、简单连接 高并发、高性能需求
8. 关键注意事项
  1. Buffer状态管理
    务必正确使用flip()clear()切换读写模式。
  2. 非阻塞配置
    Channel必须设置为非阻塞模式才能注册到Selector。
  3. 资源释放
    操作结束后需关闭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
注意事项
  1. BIO:简单但性能差,需通过线程池优化(如 Executors.newFixedThreadPool)。
  2. NIO:实际开发中常用 Netty/Mina 等框架简化编程。
  3. AIO:Java 支持有限(尤其 Linux),生产环境较少直接使用。

3. 流的方向

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

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

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 场景。

总结

Java 核心工具涵盖了数据处理、并发、模块化等关键领域。通过合理使用这些工具,可以显著提升代码质量和开发效率。