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流可以按照以下维度分类:
-
按数据单位:
- 字节流(8位字节):InputStream/OutputStream
- 字符流(16位Unicode字符):Reader/Writer
-
按流向:
- 输入流:从数据源读取数据
- 输出流:向目的地写入数据
-
按功能:
- 节点流:直接与数据源连接
- 处理流:对节点流进行包装,提供额外功能
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 +'}';}
}
注意事项:
- 类必须实现
Serializable
接口 - 使用
transient
关键字可以阻止字段被序列化 - 建议显式声明
serialVersionUID
以确保版本兼容性 - 序列化不保存静态变量状态
四、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的区别:
- Channel是双向的,Stream是单向的
- Channel总是与Buffer交互
- Channel支持异步IO
Selector详解
Selector允许单线程处理多个Channel,实现多路复用IO。
核心方法:
open()
: 创建Selectorselect()
: 阻塞直到有就绪的ChannelselectNow()
: 非阻塞检查就绪ChannelselectedKeys()
: 返回就绪的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 选择建议
-
使用传统IO的场景:
- 简单文件操作
- 文本处理
- 低并发网络应用
- 需要简单易用的API
-
使用NIO的场景:
- 高并发网络服务器
- 需要非阻塞IO
- 大文件处理
- 需要内存映射文件
- 需要更精细的IO控制
-
混合使用:
在实际开发中,可以结合两者的优势。例如:- 使用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最佳实践
-
始终关闭资源:
使用try-with-resources确保资源被正确关闭 -
合理使用缓冲:
- 对于文件IO,使用BufferedInputStream/BufferedOutputStream
- 对于NIO,使用适当大小的Buffer
-
选择正确的流类型:
- 文本数据使用字符流
- 二进制数据使用字节流
-
处理大文件:
- 使用NIO的FileChannel
- 考虑内存映射文件
- 分块处理,避免内存不足
-
异常处理:
- 区分可恢复和不可恢复错误
- 提供有意义的错误信息
- 考虑重试机制
-
性能监控:
- 监控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
解决方案:
- 使用流式处理,避免一次性加载整个文件
- 使用NIO的FileChannel和MappedByteBuffer
- 增加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进一步增强了文件系统操作的能力。
关键点回顾:
- 理解流的概念和分类
- 掌握字节流和字符流的区别与使用场景
- 熟练使用缓冲流提高IO性能
- 了解NIO的核心组件:Channel、Buffer和Selector
- 掌握Java 8+对IO/NIO的增强功能
- 根据具体场景选择合适的IO方案
Java 的 IO 与 NIO 流就像数据快递员,IO 慢悠悠,NIO 风驰电掣,用错了,数据就像迷路的小孩啦!
点赞的明天瘦10斤,不点的…胖在我心里(对手指)。