重识 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);
    }
}
相关推荐
J2虾虾4 分钟前
Spring AI Alibaba文档
java·人工智能·spring
YikNjy11 分钟前
break和continue
java·开发语言·算法
SomeOtherTime12 分钟前
Geojson相关(AI回答)
java·前端·python
日月云棠24 分钟前
10 Integer —— 最常用的整数包装类深度解析
java·后端
秋928 分钟前
java项目中cpu飙升排查及解决方法
java·开发语言
野生技术架构师29 分钟前
牛客网2026最新大厂Java高频面试题精选(附标准答案)
java·开发语言
PH = 732 分钟前
JAVA的SPI机制
java·开发语言
一 乐33 分钟前
高校实习信息发布网站|基于Spring Boot的高校实习信息发布网站的设计与实现(源码+数据库+文档)
java·数据库·spring boot·后端·论文·毕设·高校实习信息发布网站
weelinking35 分钟前
【产品】11_实现后端接口——数据在背后如何流动
java·人工智能·python·sql·oracle·json·ai编程
摇滚侠42 分钟前
东方通替换tomcat,实战经验
java