IO 与 NIO

体系概览

java.io(BIO,阻塞,面向流)
├── 字节流
│   ├── InputStream
│   │   ├── FileInputStream
│   │   ├── BufferedInputStream
│   │   ├── ByteArrayInputStream
│   │   └── ObjectInputStream
│   └── OutputStream
│       ├── FileOutputStream
│       ├── BufferedOutputStream
│       ├── ByteArrayOutputStream
│       └── ObjectOutputStream
└── 字符流
    ├── Reader
    │   ├── FileReader
    │   ├── BufferedReader
    │   └── InputStreamReader(字节→字符转换)
    └── Writer
        ├── FileWriter
        ├── BufferedWriter
        └── OutputStreamWriter(字符→字节转换)

java.nio(NIO,非阻塞,面向缓冲区)
├── Channel(双向通道)
├── Buffer(缓冲区)
├── Selector(多路复用)
└── Path / Files(现代文件操作 API)

字节流

文件读写

// 读文件
try (InputStream in = new BufferedInputStream(
        new FileInputStream("input.txt"))) {
    byte[] buf = new byte[4096];
    int len;
    while ((len = in.read(buf)) != -1) {
        process(buf, 0, len);
    }
}
 
// 写文件(追加模式第二个参数传 true)
try (OutputStream out = new BufferedOutputStream(
        new FileOutputStream("output.txt"))) {
    out.write("hello".getBytes(StandardCharsets.UTF_8));
    out.flush();
}
 
// 文件复制
try (InputStream in  = new FileInputStream("src.txt");
     OutputStream out = new FileOutputStream("dst.txt")) {
    in.transferTo(out);  // Java 9+,简洁高效
}

ByteArrayOutputStream(内存流)

ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write("hello".getBytes());
baos.write(" world".getBytes());
byte[] result = baos.toByteArray();
String str = baos.toString(StandardCharsets.UTF_8);

字符流

// 读文本文件(逐行)
try (BufferedReader reader = new BufferedReader(
        new FileReader("input.txt", StandardCharsets.UTF_8))) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}
 
// 写文本文件
try (BufferedWriter writer = new BufferedWriter(
        new FileWriter("output.txt", StandardCharsets.UTF_8))) {
    writer.write("第一行");
    writer.newLine();
    writer.write("第二行");
}
 
// 字节流 → 字符流(指定编码,推荐)
try (BufferedReader reader = new BufferedReader(
        new InputStreamReader(
            new FileInputStream("input.txt"), StandardCharsets.UTF_8))) {
    reader.lines().forEach(System.out::println);
}

PrintWriter / PrintStream

// PrintWriter:方便的文本输出
try (PrintWriter pw = new PrintWriter(
        new BufferedWriter(new FileWriter("out.txt")))) {
    pw.println("hello");
    pw.printf("name=%s, age=%d%n", "Alice", 30);
}

NIO:Path 与 Files(推荐优先使用)

Java 7 引入的现代文件操作 API,比传统 IO 更简洁。

Path

Path path = Path.of("src/main/resources/data.txt");  // Java 11+
Path path2 = Paths.get("src", "main", "resources", "data.txt");
 
path.getFileName();          // data.txt
path.getParent();            // src/main/resources
path.getRoot();              // / 或 C:\
path.toAbsolutePath();       // 绝对路径
path.normalize();            // 消除 . 和 ..
path.resolve("other.txt");   // 拼接路径
path.relativize(other);      // 相对路径
path.toFile();               // 转为 File 对象

Files 工具类

Path path = Path.of("data.txt");
 
// 读写(小文件)
String content = Files.readString(path, StandardCharsets.UTF_8);    // Java 11+
byte[] bytes   = Files.readAllBytes(path);
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
 
Files.writeString(path, "内容", StandardCharsets.UTF_8);            // Java 11+
Files.write(path, bytes);
Files.write(path, lines, StandardCharsets.UTF_8,
    StandardOpenOption.APPEND);                                       // 追加
 
// 流式读取(大文件,逐行处理,自动关闭)
try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
    lines.filter(l -> l.contains("ERROR"))
         .forEach(System.out::println);
}
 
// 复制 / 移动 / 删除
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
Files.move(src, dst, StandardCopyOption.ATOMIC_MOVE);
Files.delete(path);
Files.deleteIfExists(path);
 
// 创建
Files.createFile(path);
Files.createDirectory(path);
Files.createDirectories(path);   // 递归创建
Path tmp = Files.createTempFile("prefix", ".txt");
 
// 判断
Files.exists(path);
Files.notExists(path);
Files.isRegularFile(path);
Files.isDirectory(path);
Files.isReadable(path);
Files.size(path);                // 文件大小(字节)
 
// 遍历目录
try (Stream<Path> entries = Files.list(path)) {          // 当前目录
    entries.filter(Files::isRegularFile).forEach(System.out::println);
}
 
try (Stream<Path> all = Files.walk(path, 3)) {           // 递归,最大深度 3
    all.filter(p -> p.toString().endsWith(".java"))
       .forEach(System.out::println);
}
 
// 监听属性
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
attrs.creationTime();
attrs.lastModifiedTime();
attrs.size();

NIO:Channel 与 Buffer

适合需要精细控制或高性能的场景。

FileChannel

// 读文件
try (FileChannel channel = FileChannel.open(
        Path.of("data.txt"), StandardOpenOption.READ)) {
    ByteBuffer buf = ByteBuffer.allocate(4096);
    while (channel.read(buf) != -1) {
        buf.flip();                        // 切换为读模式
        while (buf.hasRemaining()) {
            System.out.print((char) buf.get());
        }
        buf.clear();                       // 切换为写模式
    }
}
 
// 写文件
try (FileChannel channel = FileChannel.open(
        Path.of("out.txt"),
        StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
    ByteBuffer buf = ByteBuffer.wrap("hello".getBytes());
    channel.write(buf);
}
 
// 零拷贝传输(高性能文件复制)
try (FileChannel src = FileChannel.open(Path.of("src.txt"), StandardOpenOption.READ);
     FileChannel dst = FileChannel.open(Path.of("dst.txt"),
         StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
    src.transferTo(0, src.size(), dst);  // 内核层直接传输,不经用户空间
}

ByteBuffer 操作

ByteBuffer buf = ByteBuffer.allocate(1024);      // 堆内存
ByteBuffer direct = ByteBuffer.allocateDirect(1024); // 直接内存(减少一次拷贝)
 
// 写数据(position 移动)
buf.put("hello".getBytes());
buf.putInt(42);
 
// 切换读模式:limit = position, position = 0
buf.flip();
 
// 读数据
byte[] data = new byte[buf.remaining()];
buf.get(data);
 
// 重置:position = 0,limit 不变(可重复读)
buf.rewind();
 
// 清空:position = 0,limit = capacity(准备重新写)
buf.clear();
 
// 压缩:未读数据移到开头,position = 剩余字节数(保留未读继续写)
buf.compact();
 
// 标记
buf.mark();    // 记录当前 position
buf.reset();   // 恢复到 mark 位置

内存映射文件(MappedByteBuffer)

大文件读写,将文件直接映射到内存,性能极高:

try (FileChannel channel = FileChannel.open(
        Path.of("large.dat"), StandardOpenOption.READ, StandardOpenOption.WRITE)) {
    MappedByteBuffer mbb = channel.map(
        FileChannel.MapMode.READ_WRITE, 0, channel.size());
    mbb.put(0, (byte) 42);              // 直接修改,自动同步到文件
}

关于 transferToMappedByteBufferDirectByteBuffer 背后的内核拷贝原理(sendfile、mmap、DMA gather),见 零拷贝


WatchService(目录监听)

WatchService watcher = FileSystems.getDefault().newWatchService();
 
Path dir = Path.of("/tmp/watch");
dir.register(watcher,
    StandardWatchEventKinds.ENTRY_CREATE,
    StandardWatchEventKinds.ENTRY_MODIFY,
    StandardWatchEventKinds.ENTRY_DELETE);
 
while (true) {
    WatchKey key = watcher.take();  // 阻塞等待事件
    for (WatchEvent<?> event : key.pollEvents()) {
        Path changed = (Path) event.context();
        System.out.println(event.kind() + ": " + changed);
    }
    key.reset();  // 必须重置,否则不再接收事件
}

IO 模型

BIO / NIO / AIO 对比

模型阻塞线程模型适用场景
BIO(同步阻塞)read/write 阻塞直到完成一连接一线程连接数少、开发简单
NIO(同步非阻塞)立即返回,由 Selector 通知就绪单线程管理多连接高并发、长连接
AIO(异步非阻塞)操作完成后回调通知无需轮询,OS 驱动回调超高并发、Linux 支持有限

NIO 的非阻塞本质:将 SocketChannel 设为非阻塞后,read() 没有数据时立即返回 0 而不挂起线程,再由 Selector 集中监听多个 Channel 的就绪事件。

select / poll / epoll

Linux 内核提供三种多路复用系统调用,Java NIO 在不同平台会选用最优实现:

selectpollepoll
数据结构fd_set(位图)pollfd 数组红黑树 + 就绪链表
最大连接数1024(FD_SETSIZE)无限制无限制
事件通知遍历全部 fd遍历全部 fd只遍历就绪 fd(O(1))
内核拷贝每次调用拷贝 fd 集合每次调用拷贝 pollfd注册一次,无重复拷贝
触发方式水平触发水平触发水平触发 / 边缘触发

Java NIO 在 Linux 上自动使用 epoll(JDK 1.5+ EPollSelectorProvider),macOS 使用 kqueue,Windows 使用 IOCP。


NIO 网络编程

SocketChannel(非阻塞客户端)

SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);                         // 切换为非阻塞
channel.connect(new InetSocketAddress("127.0.0.1", 8080));
 
// 非阻塞 connect 可能未立即完成,需轮询
while (!channel.finishConnect()) {
    // 可做其他事情
}
 
ByteBuffer buf = ByteBuffer.wrap("hello".getBytes());
channel.write(buf);
 
ByteBuffer resp = ByteBuffer.allocate(1024);
int n = channel.read(resp);                               // 无数据时返回 0,不阻塞
if (n > 0) {
    resp.flip();
    System.out.println(StandardCharsets.UTF_8.decode(resp));
}
channel.close();

Selector(多路复用核心)

// 创建 Selector
Selector selector = Selector.open();
 
// 注册感兴趣的事件(返回 SelectionKey)
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
key.attach(someContext);               // 可附加业务对象
 
// 事件循环
while (true) {
    int readyCount = selector.select(1000);   // 阻塞至多 1 秒,有事件立即返回
    if (readyCount == 0) continue;
 
    Iterator<SelectionKey> it = selector.selectedKeys().iterator();
    while (it.hasNext()) {
        SelectionKey sk = it.next();
        it.remove();                           // 必须手动移除,否则重复处理
 
        if (sk.isAcceptable()) {
            ServerSocketChannel server = (ServerSocketChannel) sk.channel();
            SocketChannel client = server.accept();
            client.configureBlocking(false);
            client.register(selector, SelectionKey.OP_READ);
        } else if (sk.isReadable()) {
            SocketChannel client = (SocketChannel) sk.channel();
            ByteBuffer buf = ByteBuffer.allocate(1024);
            int n = client.read(buf);
            if (n == -1) {
                sk.cancel();
                client.close();
            } else {
                buf.flip();
                client.write(buf);             // echo 回去
            }
        }
    }
}

SelectionKey 四种事件:

常量含义
OP_ACCEPT16ServerSocketChannel 有新连接
OP_CONNECT8SocketChannel 连接完成
OP_READ1Channel 有数据可读
OP_WRITE4Channel 可写(缓冲区未满)

OP_WRITE 通常不要一直注册,只在写缓冲区满时注册、写完后立即取消,否则 select() 会持续返回。

完整 NIO Echo Server

public class NioEchoServer {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
 
        ServerSocketChannel server = ServerSocketChannel.open();
        server.bind(new InetSocketAddress(8080));
        server.configureBlocking(false);
        server.register(selector, SelectionKey.OP_ACCEPT);
 
        System.out.println("Echo server started on :8080");
 
        while (true) {
            selector.select();
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey key = it.next();
                it.remove();
 
                try {
                    if (key.isAcceptable()) {
                        SocketChannel client = server.accept();
                        client.configureBlocking(false);
                        client.register(selector, SelectionKey.OP_READ);
                    } else if (key.isReadable()) {
                        SocketChannel client = (SocketChannel) key.channel();
                        ByteBuffer buf = ByteBuffer.allocate(1024);
                        int n = client.read(buf);
                        if (n == -1) {
                            key.cancel();
                            client.close();
                        } else {
                            buf.flip();
                            client.write(buf);
                        }
                    }
                } catch (IOException e) {
                    key.cancel();
                    key.channel().close();
                }
            }
        }
    }
}

IO vs NIO 选型

场景推荐
简单文件读写(小文件)Files.readString / Files.writeString
逐行处理文本Files.lines
大文件顺序读写BufferedInputStream / FileChannel
大文件随机访问FileChannel + ByteBuffer
超大文件(GB 级)MappedByteBuffer
网络通信高并发SocketChannel + Selector
目录变化监听WatchService

编码注意

// 始终显式指定字符集,不依赖系统默认编码
Files.readString(path, StandardCharsets.UTF_8);
new InputStreamReader(in, StandardCharsets.UTF_8);
new String(bytes, StandardCharsets.UTF_8);
"hello".getBytes(StandardCharsets.UTF_8);

相关链接