当前位置: 首页 > news >正文

Java IO流与NIO终极指南:从基础到高级应用

一、IO流与NIO概述

1.1 什么是IO流

IO(Input/Output)流是Java中用于处理输入输出的核心机制,它像水流一样将数据从源头(如文件、网络连接等)传输到目的地。Java IO流主要分为字节流和字符流两大类。

通俗理解:想象IO流就像水管,数据就是水流。字节流是原始的水流(二进制数据),字符流则是经过处理的可直接饮用的水(文本数据)。

1.2 IO与NIO的区别

特性IO (传统IO)NIO (New IO)
数据流动方式流式(Stream)块式(Channel和Buffer)
缓冲机制大多数操作无缓冲总是使用Buffer
阻塞/非阻塞阻塞式(Blocking)可选择非阻塞(Non-blocking)
选择器(Selector)
适用场景连接数较少,数据传输量大的情况连接数多,每个连接数据传输量小的情况

通俗理解:传统IO就像单线程的餐厅服务员,一次只能服务一桌客人;NIO则像多线程服务员,可以同时照看多桌客人,哪桌有需求就去服务哪桌。

二、Java IO流体系

2.1 IO流分类

Java IO流可以按照以下维度分类:

  1. 按数据单位

    • 字节流(8位字节):InputStream/OutputStream
    • 字符流(16位Unicode字符):Reader/Writer
  2. 按流向

    • 输入流:从数据源读取数据
    • 输出流:向目的地写入数据
  3. 按功能

    • 节点流:直接与数据源连接
    • 处理流:对节点流进行包装,提供额外功能

2.2 核心类继承体系

字节流体系
InputStream (抽象类)
├─ FileInputStream
├─ FilterInputStream
│  ├─ BufferedInputStream
│  ├─ DataInputStream
│  └─ PushbackInputStream
├─ ObjectInputStream
├─ PipedInputStream
├─ ByteArrayInputStream
└─ SequenceInputStreamOutputStream (抽象类)
├─ FileOutputStream
├─ FilterOutputStream
│  ├─ BufferedOutputStream
│  ├─ DataOutputStream
│  └─ PrintStream
├─ ObjectOutputStream
├─ PipedOutputStream
└─ ByteArrayOutputStream
字符流体系
Reader (抽象类)
├─ BufferedReader
├─ InputStreamReader
│  └─ FileReader
├─ StringReader
├─ PipedReader
└─ CharArrayReaderWriter (抽象类)
├─ BufferedWriter
├─ OutputStreamWriter
│  └─ FileWriter
├─ StringWriter
├─ PipedWriter
└─ CharArrayWriter

2.3 常用IO流方法详解

InputStream核心方法
方法签名描述返回值说明
int read()读取一个字节返回读取的字节(0-255),如果到达末尾返回-1
int read(byte[] b)读取最多b.length个字节到数组返回实际读取的字节数,末尾返回-1
int read(byte[] b, int off, int len)读取最多len个字节到数组的off位置同上
long skip(long n)跳过并丢弃n个字节返回实际跳过的字节数
int available()返回可读取的估计字节数通常用于检查是否可无阻塞读取
void close()关闭流并释放资源
boolean markSupported()是否支持mark/reset返回布尔值
void mark(int readlimit)标记当前位置
void reset()重置到最后一次mark的位置
OutputStream核心方法
方法签名描述返回值说明
void write(int b)写入一个字节
void write(byte[] b)写入整个字节数组
void write(byte[] b, int off, int len)写入字节数组的一部分
void flush()强制刷新输出缓冲区
void close()关闭流并释放资源
Reader核心方法
方法签名描述返回值说明
int read()读取一个字符返回读取的字符(0-65535),末尾返回-1
int read(char[] cbuf)读取字符到数组返回实际读取的字符数,末尾返回-1
int read(char[] cbuf, int off, int len)读取字符到数组的一部分同上
long skip(long n)跳过n个字符返回实际跳过的字符数
boolean ready()是否可读取返回布尔值
void close()关闭流
boolean markSupported()是否支持mark返回布尔值
void mark(int readAheadLimit)标记当前位置
void reset()重置到最后一次mark的位置
Writer核心方法
方法签名描述返回值说明
void write(int c)写入一个字符
void write(char[] cbuf)写入字符数组
void write(char[] cbuf, int off, int len)写入字符数组的一部分
void write(String str)写入字符串
void write(String str, int off, int len)写入字符串的一部分
Writer append(char c)追加一个字符返回Writer本身
Writer append(CharSequence csq)追加字符序列返回Writer本身
Writer append(CharSequence csq, int start, int end)追加字符序列的一部分返回Writer本身
void flush()刷新缓冲区
void close()关闭流

三、IO流实战示例

3.1 文件读写基础示例

字节流文件复制
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;public class FileCopyExample {public static void main(String[] args) {// 定义源文件和目标文件路径String sourceFile = "source.jpg";String targetFile = "target.jpg";// 使用try-with-resources确保流自动关闭try (FileInputStream fis = new FileInputStream(sourceFile);FileOutputStream fos = new FileOutputStream(targetFile)) {// 创建缓冲区(通常8KB是比较理想的缓冲区大小)byte[] buffer = new byte[8192];int bytesRead;// 读取源文件并写入目标文件while ((bytesRead = fis.read(buffer)) != -1) {fos.write(buffer, 0, bytesRead);}System.out.println("文件复制完成!");} catch (IOException e) {e.printStackTrace();}}
}
字符流文本文件读写
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;public class TextFileExample {public static void main(String[] args) {String inputFile = "input.txt";String outputFile = "output.txt";try (BufferedReader reader = new BufferedReader(new FileReader(inputFile));BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile))) {String line;int lineNumber = 1;// 逐行读取并处理while ((line = reader.readLine()) != null) {// 添加行号并写入新文件writer.write(lineNumber + ": " + line);writer.newLine(); // 换行lineNumber++;}System.out.println("文本处理完成!");} catch (IOException e) {e.printStackTrace();}}
}

3.2 缓冲流性能对比

缓冲流可以显著提高IO性能,下面通过一个例子展示差异:

import java.io.*;public class BufferedStreamBenchmark {private static final String FILE_PATH = "large_file.dat";private static final int FILE_SIZE_MB = 100; // 100MB测试文件public static void main(String[] args) throws IOException {// 创建测试文件createTestFile(FILE_PATH, FILE_SIZE_MB);// 测试无缓冲读取long startTime = System.currentTimeMillis();readWithoutBuffer(FILE_PATH);long duration = System.currentTimeMillis() - startTime;System.out.println("无缓冲读取耗时: " + duration + "ms");// 测试缓冲读取startTime = System.currentTimeMillis();readWithBuffer(FILE_PATH);duration = System.currentTimeMillis() - startTime;System.out.println("缓冲读取耗时: " + duration + "ms");// 删除测试文件new File(FILE_PATH).delete();}private static void createTestFile(String path, int sizeMB) throws IOException {try (FileOutputStream fos = new FileOutputStream(path);BufferedOutputStream bos = new BufferedOutputStream(fos)) {byte[] data = new byte[1024]; // 1KB数据块for (int i = 0; i < 1024 * sizeMB; i++) { // 写入sizeMB MB数据bos.write(data);}}}private static void readWithoutBuffer(String path) throws IOException {try (FileInputStream fis = new FileInputStream(path)) {while (fis.read() != -1) { // 逐字节读取// 什么都不做,只读取}}}private static void readWithBuffer(String path) throws IOException {try (FileInputStream fis = new FileInputStream(path);BufferedInputStream bis = new BufferedInputStream(fis)) {while (bis.read() != -1) { // 使用缓冲流读取// 什么都不做,只读取}}}
}

典型输出结果

无缓冲读取耗时: 12345ms
缓冲读取耗时: 234ms

3.3 对象序列化与反序列化

Java对象序列化可以将对象转换为字节流,便于存储或传输。

import java.io.*;
import java.util.Date;public class SerializationExample {public static void main(String[] args) {// 要序列化的对象User user = new User("张三", "zhangsan@example.com", new Date(), 28);// 序列化到文件try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"))) {oos.writeObject(user);System.out.println("对象序列化完成");} catch (IOException e) {e.printStackTrace();}// 从文件反序列化try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.dat"))) {User deserializedUser = (User) ois.readObject();System.out.println("反序列化得到的对象: " + deserializedUser);} catch (IOException | ClassNotFoundException e) {e.printStackTrace();}}
}// 可序列化的User类
class User implements Serializable {private static final long serialVersionUID = 1L; // 序列化版本号private String name;private String email;private Date birthDate;private transient int age; // transient关键字标记的字段不会被序列化public User(String name, String email, Date birthDate, int age) {this.name = name;this.email = email;this.birthDate = birthDate;this.age = age;}@Overridepublic String toString() {return "User{" +"name='" + name + '\'' +", email='" + email + '\'' +", birthDate=" + birthDate +", age=" + age +'}';}
}

注意事项

  1. 类必须实现Serializable接口
  2. 使用transient关键字可以阻止字段被序列化
  3. 建议显式声明serialVersionUID以确保版本兼容性
  4. 序列化不保存静态变量状态

四、NIO深入解析

4.1 NIO核心组件

NIO有三个核心组件:Channel、Buffer和Selector。

Buffer详解

Buffer是NIO中的数据容器,主要实现类包括:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

Buffer核心属性

  • capacity: 缓冲区容量,创建时设定且不可改变
  • position: 当前读写位置
  • limit: 可读写的上限
  • mark: 标记位置,用于reset

Buffer常用方法

方法描述
allocate(int capacity)分配新的缓冲区
put()/get()写入/读取数据
flip()切换为读模式,limit=position, position=0
clear()清空缓冲区,position=0, limit=capacity
compact()压缩缓冲区,保留未读数据
rewind()重绕缓冲区,position=0
mark()标记当前位置
reset()重置到mark位置
Channel详解

Channel是NIO中的双向数据传输通道,主要实现包括:

  • FileChannel: 文件IO
  • SocketChannel: TCP网络IO
  • ServerSocketChannel: TCP服务端监听
  • DatagramChannel: UDP网络IO

Channel与Stream的区别

  1. Channel是双向的,Stream是单向的
  2. Channel总是与Buffer交互
  3. Channel支持异步IO
Selector详解

Selector允许单线程处理多个Channel,实现多路复用IO。

核心方法

  • open(): 创建Selector
  • select(): 阻塞直到有就绪的Channel
  • selectNow(): 非阻塞检查就绪Channel
  • selectedKeys(): 返回就绪的SelectionKey集合
  • wakeup(): 唤醒阻塞的select()

4.2 NIO实战示例

文件复制示例
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;public class NIOFileCopy {public static void main(String[] args) {String sourceFile = "source.mp4";String targetFile = "target.mp4";try (RandomAccessFile source = new RandomAccessFile(sourceFile, "r");RandomAccessFile target = new RandomAccessFile(targetFile, "rw");FileChannel sourceChannel = source.getChannel();FileChannel targetChannel = target.getChannel()) {// 创建直接缓冲区(性能更好,但创建成本高)ByteBuffer buffer = ByteBuffer.allocateDirect(8192);while (sourceChannel.read(buffer) != -1) {buffer.flip(); // 切换为读模式targetChannel.write(buffer);buffer.clear(); // 清空缓冲区,准备下一次读取}// 确保所有数据写入磁盘targetChannel.force(true);System.out.println("文件复制完成!");} catch (IOException e) {e.printStackTrace();}}
}
非阻塞Socket示例
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;public class NonBlockingServer {public static void main(String[] args) throws IOException {// 创建SelectorSelector selector = Selector.open();// 创建ServerSocketChannel并配置为非阻塞ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.bind(new InetSocketAddress(8080));// 注册到Selector,监听ACCEPT事件serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("服务器启动,监听8080端口...");while (true) {// 阻塞直到有事件发生selector.select();// 获取就绪的SelectionKey集合Set<SelectionKey> selectedKeys = selector.selectedKeys();Iterator<SelectionKey> keyIterator = selectedKeys.iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();if (key.isAcceptable()) {// 处理新连接handleAccept(serverSocketChannel, selector);} else if (key.isReadable()) {// 处理读事件handleRead(key);}keyIterator.remove(); // 处理完后移除}}}private static void handleAccept(ServerSocketChannel serverChannel, Selector selector) throws IOException {SocketChannel clientChannel = serverChannel.accept();clientChannel.configureBlocking(false);clientChannel.register(selector, SelectionKey.OP_READ);System.out.println("客户端连接: " + clientChannel.getRemoteAddress());}private static void handleRead(SelectionKey key) throws IOException {SocketChannel channel = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);int bytesRead = channel.read(buffer);if (bytesRead == -1) {// 连接关闭channel.close();System.out.println("客户端断开连接");return;}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());channel.write(response);}
}

五、Java 8+中的IO/NIO增强

5.1 Files类新增方法

Java 8为java.nio.file.Files类添加了许多实用方法:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;public class FilesNewMethods {public static void main(String[] args) throws IOException {Path path = Paths.get(".");// 1. 使用lines()方法逐行读取文件try (Stream<String> lines = Files.lines(Paths.get("data.txt"))) {lines.filter(line -> line.contains("error")).forEach(System.out::println);}// 2. 使用list()方法列出目录内容try (Stream<Path> paths = Files.list(path)) {paths.forEach(System.out::println);}// 3. 使用walk()方法递归遍历目录try (Stream<Path> paths = Files.walk(path, 3)) { // 最大深度3paths.filter(Files::isRegularFile).forEach(System.out::println);}// 4. 使用find()方法搜索文件try (Stream<Path> paths = Files.find(path, 3, (p, attrs) -> attrs.isRegularFile() && p.toString().endsWith(".java"))) {paths.forEach(System.out::println);}// 5. 使用readAllBytes()和write()简化文件读写byte[] data = Files.readAllBytes(Paths.get("source.txt"));Files.write(Paths.get("target.txt"), data);}
}

5.2 BufferedReader新增lines()方法

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.stream.Stream;public class BufferedReaderLines {public static void main(String[] args) {try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"));Stream<String> lines = reader.lines()) {long count = lines.filter(line -> !line.isEmpty()).count();System.out.println("非空行数: " + count);} catch (IOException e) {e.printStackTrace();}}
}

5.3 Java 9的InputStream增强

Java 9为InputStream添加了实用的transferTo方法:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;public class InputStreamTransfer {public static void main(String[] args) throws IOException {try (FileInputStream fis = new FileInputStream("source.txt");FileOutputStream fos = new FileOutputStream("target.txt")) {// Java 9新增方法,直接将输入流传输到输出流fis.transferTo(fos);System.out.println("文件传输完成!");}}
}

六、IO与NIO性能对比与选择

6.1 性能对比表格

场景IO性能NIO性能推荐选择
大文件顺序读写更高NIO
小文件随机访问中等NIO
高并发网络服务(1000+)NIO
低并发网络服务中等中等均可
简单文本处理中等IO

6.2 选择建议

  1. 使用传统IO的场景

    • 简单文件操作
    • 文本处理
    • 低并发网络应用
    • 需要简单易用的API
  2. 使用NIO的场景

    • 高并发网络服务器
    • 需要非阻塞IO
    • 大文件处理
    • 需要内存映射文件
    • 需要更精细的IO控制
  3. 混合使用
    在实际开发中,可以结合两者的优势。例如:

    • 使用NIO处理网络连接
    • 使用传统IO处理业务逻辑
    • 使用NIO的文件通道进行大文件传输
    • 使用传统IO的API进行简单文件操作

七、高级主题与最佳实践

7.1 内存映射文件

内存映射文件(Memory-Mapped Files)是NIO提供的一种高效文件访问方式,它将文件直接映射到内存中:

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;public class MemoryMappedFileExample {public static void main(String[] args) throws Exception {// 文件路径和大小String filePath = "large_file.dat";long fileSize = 1024 * 1024 * 100; // 100MBtry (RandomAccessFile file = new RandomAccessFile(filePath, "rw");FileChannel channel = file.getChannel()) {// 将文件映射到内存MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, // 读写模式0,                            // 起始位置fileSize                       // 映射区域大小);// 写入数据for (int i = 0; i < fileSize; i++) {buffer.put((byte) (i % 128));}// 读取数据buffer.flip();byte[] data = new byte[100];buffer.get(data, 0, data.length);System.out.println("前100字节数据: " + new String(data));}}
}

适用场景

  • 超大文件随机访问
  • 进程间通信
  • 高频更新的文件

7.2 文件锁

NIO提供了文件锁机制,防止多个进程同时修改同一文件:

import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;public class FileLockExample {public static void main(String[] args) throws Exception {String filePath = "shared.txt";try (RandomAccessFile file = new RandomAccessFile(filePath, "rw");FileChannel channel = file.getChannel()) {// 获取排他锁FileLock lock = channel.lock();try {System.out.println("获得文件锁,开始操作文件...");// 执行文件操作Thread.sleep(5000); // 模拟长时间操作} finally {lock.release();System.out.println("释放文件锁");}}}
}

7.3 异步IO (AIO)

Java 7引入了AsynchronousFileChannel支持异步IO:

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;public class AsyncFileIOExample {public static void main(String[] args) {Path path = Paths.get("large_file.dat");try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {ByteBuffer buffer = ByteBuffer.allocate(1024);long position = 0;// 异步读取Future<Integer> operation = channel.read(buffer, position);while (!operation.isDone()) {System.out.println("执行其他任务...");Thread.sleep(500);}int bytesRead = operation.get();buffer.flip();byte[] data = new byte[buffer.limit()];buffer.get(data);System.out.println("读取数据: " + new String(data));} catch (Exception e) {e.printStackTrace();}}
}

7.4 IO最佳实践

  1. 始终关闭资源
    使用try-with-resources确保资源被正确关闭

  2. 合理使用缓冲

    • 对于文件IO,使用BufferedInputStream/BufferedOutputStream
    • 对于NIO,使用适当大小的Buffer
  3. 选择正确的流类型

    • 文本数据使用字符流
    • 二进制数据使用字节流
  4. 处理大文件

    • 使用NIO的FileChannel
    • 考虑内存映射文件
    • 分块处理,避免内存不足
  5. 异常处理

    • 区分可恢复和不可恢复错误
    • 提供有意义的错误信息
    • 考虑重试机制
  6. 性能监控

    • 监控IO操作耗时
    • 识别瓶颈并进行优化

八、常见问题与解决方案

8.1 文件编码问题

问题:读取文本文件时出现乱码

解决方案

// 指定正确的字符编码
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("file.txt"), "UTF-8"))) {// 读取文件
}

8.2 文件锁定问题

问题:在Windows上无法删除或修改刚使用过的文件

解决方案

try (FileInputStream fis = new FileInputStream(file)) {// 使用文件
} // 自动关闭流,释放文件锁// 或者强制释放资源
System.gc(); // 有时可以帮助释放未正确关闭的资源

8.3 内存不足问题

问题:读取大文件时出现OutOfMemoryError

解决方案

  1. 使用流式处理,避免一次性加载整个文件
  2. 使用NIO的FileChannel和MappedByteBuffer
  3. 增加JVM堆内存:-Xmx参数

8.4 文件路径问题

问题:跨平台文件路径不一致

解决方案

// 使用Paths.get()或File.separator
Path path = Paths.get("data", "files", "example.txt");
// 或者
String path = "data" + File.separator + "files" + File.separator + "example.txt";

九、总结

Java IO和NIO提供了丰富的API来处理各种输入输出需求。传统IO简单易用,适合大多数常规场景;NIO则提供了更高的性能和灵活性,特别适合高并发和大数据量处理。从Java 7开始引入的NIO 2.0进一步增强了文件系统操作的能力。

关键点回顾

  1. 理解流的概念和分类
  2. 掌握字节流和字符流的区别与使用场景
  3. 熟练使用缓冲流提高IO性能
  4. 了解NIO的核心组件:Channel、Buffer和Selector
  5. 掌握Java 8+对IO/NIO的增强功能
  6. 根据具体场景选择合适的IO方案

Java 的 IO 与 NIO 流就像数据快递员,IO 慢悠悠,NIO 风驰电掣,用错了,数据就像迷路的小孩啦!

点赞的明天瘦10斤,不点的…胖在我心里(对手指)。

在这里插入图片描述

相关文章:

  • JAVA入门-JAVA数据类型
  • 永磁同步电机控制算法--线性ADRC转速环控制器(一阶、二阶)
  • Keysight万用表使用指南及基于Python采集数据生成Excel文件
  • Python os.path.join()路径拼接异常
  • 如何解决matlab/octave画图legend图例颜色一样的问题?
  • 零基础做自动驾驶集成测试(仿真)
  • C# 高效操作excel文件
  • Elasticsearch--自带“搜索引擎“的数据库
  • 软考中级-软件设计师 操作系统(手写笔记)
  • 基础术语说明
  • 负载均衡技术全景指南:架构、算法与发展趋势
  • Qwen3 模型架构和能力概览
  • Compose笔记(二十)--TextField
  • MCP协议:自然语言与结构化数据的双向桥梁 ——基于JSON-RPC 2.0的标准化实践
  • 遗传算法(Genetic Algorithm,GA)
  • 健康管理系统操作界面解析:从建档到干预方案生成的极简逻辑
  • Vulkan 学习(16)---- 使用 VertexBuffer
  • Windows系统安装Docker(Win10系统升级,然后安装)
  • 区块链:跨链协的技术突破与产业重构
  • ASP.NET MVC​ 入门与提高指南六
  • 中国空间站多项太空实验已取得成果,未来将陆续开展千余项研究
  • 顺利撤离空间站,神十九乘组踏上回家之旅
  • 我国首部《人工智能气象应用服务办法》今天发布
  • 人到中年为何腰围变粗?科学家发现腹部脂肪增加的细胞元凶
  • 餐饮店直播顾客用餐,律师:公共场所并非无隐私,需对方同意
  • 扎克伯格怕“错过风口”?Meta AI数字伴侣被允许与未成年人讨论不当话题