重识 Java IO、NIO 与 OkIO

IO 到底是什么?

IO 就是 Input/Output------输入输出,要清楚概念,关键在于要知道所谓的输入输出是以程序内存(代码中的变量、对象)为主体的。

程序内存就是内部,程序内存之外的则是外部,比如本地文件网络连接、其他进程等。

所以:

  • 输入指的是把数据从外部(如文件)读到内存中。
  • 输出指的是把数据从内存写到外部(如文件)。

传统 IO(java.io

传统 IO 操作都是阻塞的,它的设计核心是流(Stream)。

形象点来说,流就像一条管道,数据会流动于管道中,我们通过管道来存取数据。

字节流

字节流有着 InputStreamOutputStream,它们内部流动的是字节,我们通常用于处理原始的二进制数据,比如图片、音频。

java 复制代码
/**
 * 字节流示例
 */
private static void IOStreamDemo() {
    // 往文件写入数据
    File file = new File("./Text");
    OutputStream outputStream = null;
    try {
        outputStream = new FileOutputStream(file);
        outputStream.write("hello world!\r\n你好啊,世界!".getBytes(StandardCharsets.UTF_8));
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (outputStream != null) {
            try {
                outputStream.close(); // 关闭流
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    // 从文件读取数据
    try (InputStream inputStream = new FileInputStream(file)) {
        byte[] buffer = new byte[1024];
        int len; // 读取的字节数
        while ((len = inputStream.read(buffer)) != -1) {
            String dataChunk = new String(buffer, 0, len, StandardCharsets.UTF_8);
            System.out.print(dataChunk);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

当文件被打开后,会在内存中保存文件的相关信息(如文件描述符或句柄)。我们需要手动关闭文件,具体是在 finally 块中调用流的 close() 函数。如果不关闭,会导致资源泄露。当打开的文件过多时,程序将无法再打开新文件,最终导致崩溃。

上面的代码展示了两种关闭流的写法:第一种是标准写法。第二种写法利用了 Java7 新增的 try-with-resources 特性,它能够自动关闭所用到的资源。

字符流

字符流有着 ReaderWriter,它们内部流动的是字符,我们专门用它们来处理文本数据。

字节只是计算机物理存储的最小单位,有 8 个比特位。而字符是人类语言的抽象概念,大小由编码决定。

java 复制代码
/**
 * 字符流示例
 */
private static void ReaderWriterDemo() {
    // 往文件写入数据
    File file = new File("./Text.txt");
    try (OutputStream outputStream = new FileOutputStream(file);
         Writer writer = new OutputStreamWriter(outputStream)) {
        writer.write("hello world!");
    } catch (IOException e) {
        e.printStackTrace();
    }

    // 从文件读取数据
    try (InputStream inputStream = new FileInputStream(file);
         Reader reader = new InputStreamReader(inputStream)) {
        char[] buffer = new char[1024];
        int len;

        while ((len = reader.read(buffer)) != -1) {
            for (int i = 0; i < len; i++) {
                char ch = buffer[i];
                System.out.print(ch);
            }
        }

    } catch (IOException e) {
        e.printStackTrace();
    }
}

缓冲流

字节流是连接到文件的管道;字符流包着字节流,会将流过的字节按照指定编码(默认是 UTF-8)转换为字符;

虽然我们在字节流和字符流的示例中,手动创建了字节数组来分块读取,但这和 BufferedStream 缓冲流的缓冲机制是不同的。

缓冲流包着字符流,它内部自带了一个缓冲区(默认大小为 8192),它能自动预读数据尽可能填满缓冲区,从而减少与外部(如磁盘)IO 的次数,极大提升性能。同时,它还提供了 readLine 等便捷函数。

java 复制代码
/**
 * 缓冲流示例
 */
private static void BufferedStreamDemo() {
    // 写入数据到文件
    File file = new File("./Text.txt");
    try (OutputStream outputStream = new FileOutputStream(file);
         OutputStreamWriter writer = new OutputStreamWriter(outputStream);
         BufferedWriter bufferedWriter = new BufferedWriter(writer);) {
        bufferedWriter.write("hello io!");
        bufferedWriter.flush(); // 刷新缓冲区
    } catch (IOException e) {
        e.printStackTrace();
    }

    // 从文件中读取数据
    try (InputStream inputStream = new FileInputStream(file);
         InputStreamReader reader = new InputStreamReader(inputStream);
         BufferedReader bufferedReader = new BufferedReader(reader);
    ) {
        String line;
        while ((line = bufferedReader.readLine()) != null) {
            System.out.println(line);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

使用缓冲输出流时,数据会先写入到内存中,并没有写入到文件。调用 flush() 是为了让缓冲区中的数据能够及立即写入文件。当缓冲区满了或者输出流被关闭时,缓冲区会自动刷新,也就是自动调用一次 flush()

文件复制

要完成文件复制,我们可以调用 android.os.FileUtils.copy() 或者 kotlin.io.File.copyTo() 扩展函数。

如果要自己完成,也不难,只需复制一个文件的所有字节到另一个文件即可。

java 复制代码
public class Main {
    public static void main(String[] args) {
        try (InputStream inputStream = new FileInputStream("Text.txt");
             OutputStream outputStream = new FileOutputStream("NewText.txt")) {
            long progress = myFileCopy(inputStream, outputStream);
            System.out.println("File Copied, Total Size: " + progress + " bytes");
        } catch (IOException e) {
            System.err.println("Error: An IO exception occurred during file copying");
            e.printStackTrace();
        }
    }

    public static long myFileCopy(InputStream src, OutputStream dest) throws IOException {
        if (src == null) {
            throw new NullPointerException("Src stream is null");
        }
        if (dest == null) {
            throw new NullPointerException("Dest stream is null");
        }

        byte[] buffer = new byte[8192];
        long progress = 0;
        int len;
        while ((len = src.read(buffer)) != -1) {
            dest.write(buffer, 0, len);
            progress += len;
        }
        return progress;
    }
}

Socket

在与网络进行交互时,使用的也是 IO 流,比如说:

客户端:

java 复制代码
// SocketClientDemo.java
void main() {
    SocketClientDemo();
}

private static final int PORT = 8080;
private static final String HOST = "127.0.0.1";

public static void SocketClientDemo() {
    IO.println("Client: Connecting to " + HOST + ":" + PORT + "...");
    try (Socket socket = new Socket(HOST, PORT);
         BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
         BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
         BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in))) {

        IO.println("Client: Connected. Please enter a message (enter 'exit'):");

        String userInput;
        while ((userInput = consoleReader.readLine()) != null) {
            // 将用户输入发送到服务器
            writer.write(userInput);
            writer.newLine(); // 添加换行符
            writer.flush();

            // 退出
            if ("exit".equals(userInput)) {
                break;
            }

            // 读取服务器的回显
            String line = reader.readLine();
            IO.println(line);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }

    IO.println("Client: The connection is closed.");
}

服务端:

java 复制代码
// SocketServerDemo.java
void main() {
    SocketServerDemo();
}

private static final int PORT = 8080;

public static void SocketServerDemo() {
    IO.println("Server: Starting, listening on port " + PORT + "...");

    try (ServerSocket serverSocket = new ServerSocket(PORT)) {
        try (Socket socket = serverSocket.accept();
             BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
             BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()))
        ) {
            IO.println("Server: Client connected: " + socket.getRemoteSocketAddress());

            String clientMessage;
            while ((clientMessage = reader.readLine()) != null) {
                // 退出
                if ("exit".equalsIgnoreCase(clientMessage)) {
                    break;
                }

                // 回显消息
                String echoMessage = "Echo: " + clientMessage;

                // 将消息发回客户端
                writer.write(echoMessage);
                writer.newLine();
                writer.flush();
            }
        }
        IO.println("Server: The client connection is closed.");
    } catch (IOException e) {
        e.printStackTrace();
    }

    IO.println("Server: The server is down.");
}

在客户端控制台输入消息,服务端会回显对应消息:

vbnet 复制代码
Client: Connecting to 127.0.0.1:8080...
Client: Connected. Please enter a message (enter 'exit'):
你好!
Echo: 你好!
今天天气不错呢?
Echo: 今天天气不错呢?

NIO (java.nio)

NIO(New IO)和传统 IO 不同的是:

  1. 它使用的是 Channel,这个通道是双向的(可读可写)。
  2. Buffer 缓冲区被强制使用。
  3. 具有 Selector 选择器,可实现非阻塞网络 IO 操作。

我们先来了解一下它的 Buffer,缓冲区有三个指针:position, limit, capacity。分别表示了:当前的读写位置,读写的上限(默认等于容量),缓冲区的容量。

所以一个简单的读数据操作,需要这样:

java 复制代码
/**
 * NIO示例
 */
private static void NIODemo() {
    // 从文件中读取数据
    try (RandomAccessFile file = new RandomAccessFile("Text.txt", "r");) {
        FileChannel channel = file.getChannel();
        // 分配字节缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while (channel.read(buffer) != -1) { // 读取数据
            // 修改读取的范围,这两行代码等价于 buffer.flip();
            buffer.limit(buffer.position());
            buffer.position(0);
            // 解码打印
            System.out.print(StandardCharsets.UTF_8.decode(buffer));
            // 清空缓冲区,以便下次使用
            buffer.clear();
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

不仅非常麻烦,还要手动管理指针位置。

非阻塞式

其实 NIO 并不是提供给客户端使用的。它的非阻塞式网络 IO,配合上 Selector,能够让单个线程监视成百上千个网络连接事件。这在服务端非常有用,避免了为每一个连接都开启一个线程。

下面这段代码只需粗略看看即可,我们无需了解这么多,你也可以将 NIO 给彻底忘了。

java 复制代码
private static final int PORT = 8080;
private static final int BUFFER_SIZE = 1024;

/**
 * 使用 NIO 和 Selector 实现的 Socket 服务器
 */
public static void SocketServerNioDemo() {
    IO.println("NIO Server: Starting, listening on port " + PORT + "...");

    try (// 打开 Selector
         Selector selector = Selector.open();
         // 打开 ServerSocketChannel
         ServerSocketChannel serverChannel = ServerSocketChannel.open()
    ) {
        // 绑定端口并设置为非阻塞
        serverChannel.bind(new InetSocketAddress(PORT));
        serverChannel.configureBlocking(false);

        // 将 ServerChannel 注册到 Selector,监听连接事件
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);

        while (true) {
            // 阻塞等待事件发生
            selector.select();


            // 遍历所有已就绪的事件
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();

                iterator.remove();

                try {
                    // 根据事件类型分别处理
                    if (key.isAcceptable()) {
                        // 有新的客户端连接
                        handleAccept(key, selector);
                    }

                    if (key.isReadable()) {
                        // 有客户端发送数据
                        handleRead(key, buffer);
                    }
                } catch (IOException e) {
                    // 客户端异常断开
                    System.err.println("Server: Client connection error: " + e.getMessage());
                    closeClientConnection(key);
                }
            }
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

/**
 * 处理新的客户端连接
 */
private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
    ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();

    // 非阻塞接受连接
    SocketChannel clientChannel = serverChannel.accept();
    clientChannel.configureBlocking(false); // 必须设置客户端 Channel 也为非阻塞

    // 将新的客户端 Channel 注册到 Selector,监听 "READ" 事件
    clientChannel.register(selector, SelectionKey.OP_READ);
    IO.println("Server: Client connected: " + clientChannel.getRemoteAddress());
}

/**
 * 处理来自客户端的数据读取
 */
private static void handleRead(SelectionKey key, ByteBuffer buffer) throws IOException {
    SocketChannel clientChannel = (SocketChannel) key.channel();

    // 清空缓冲区,准备写入
    buffer.clear();
    int bytesRead = clientChannel.read(buffer);

    if (bytesRead == -1) {
        // 客户端正常关闭了连接
        IO.println("Server: Client disconnected cleanly.");
        closeClientConnection(key);
        return;
    }

    if (bytesRead > 0) {
        // 切换到读模式
        buffer.flip();
        // 解码
        String clientMessage = StandardCharsets.UTF_8.decode(buffer).toString().trim();

        IO.println("Server: Received: "" + clientMessage + """);

        if ("exit".equalsIgnoreCase(clientMessage)) {
            // 客户端请求退出
            closeClientConnection(key);
            return;
        }

        // 回显消息
        String echoMessage = "Echo: " + clientMessage;

        // 加上换行符
        ByteBuffer echoBuffer = StandardCharsets.UTF_8.encode(echoMessage + "\n");

        // 将消息写回客户端
        while (echoBuffer.hasRemaining()) {
            clientChannel.write(echoBuffer);
        }
    }
}

/**
 * 关闭客户端连接
 */
private static void closeClientConnection(SelectionKey key) throws IOException {
    SocketChannel clientChannel = (SocketChannel) key.channel();
    IO.println("Server: Closing connection for: " + clientChannel.getRemoteAddress());
    // 从 Selector 中取消注册
    key.cancel();
    // 关闭 Channel
    clientChannel.close();
}

OkIO

OkIO 只是传统 IO 的封装,它提供了两个核心接口:

  • Source:数据的来源,对应 InputStream
  • Sink:数据的聚集地,对应 OutputStream

OkIO 也支持 Buffer,它既是 Source 又是 Sink

首先引入依赖:

kotlin 复制代码
implementation("com.squareup.okio:okio:3.16.2")

然后来简单读取文件:

java 复制代码
public static void OkIODemo() {
    File file = new File("file.txt");
    if (!file.exists()) {
        try {
            boolean fileCreated = file.createNewFile();
            if (!fileCreated) {
                throw new FileNotFoundException("File creation failed");
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    try (Source source = Okio.source(file);
         Buffer buffer = new Buffer()) {

        // 只要 Source 还有数据,就往 buffer 里读
        while (source.read(buffer, 8192) != -1) {

            // 处理 buffer 中所有完整的行
            String line;
            while ((line = buffer.readUtf8Line()) != null) {
                System.out.println(line);
            }
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}
相关推荐
light_in_hand2 小时前
内存区域划分——垃圾回收
java·jvm·算法
金銀銅鐵2 小时前
[Java] JDK 9 新变化之 Convenience Factory Methods for Collections
java·后端
用户7406696136252 小时前
入门并理解Java模块化系统(JPMS)
java
金銀銅鐵2 小时前
[Java] 用 Swing 生成一个最大公约数计算器
java·后端
啦啦9117142 小时前
Niagara Launcher 全新Android桌面启动器!给手机换个门面!
android·智能手机
游戏开发爱好者82 小时前
iOS 上架要求全解析,App Store 审核标准、开发者准备事项与开心上架(Appuploader)跨平台免 Mac 实战指南
android·macos·ios·小程序·uni-app·iphone·webview
小安同学iter3 小时前
SQL50+Hot100系列(11.7)
java·算法·leetcode·hot100·sql50
xrkhy3 小时前
canal1.1.8+mysql8.0+jdk17+redis的使用
android·redis·adb
yivifu3 小时前
JavaScript Selection API详解
java·前端·javascript