IO(Input/Output)流是Java中用于处理输入输出的核心机制,它像水流一样将数据从源头(如文件、网络连接等)传输到目的地。Java IO流主要分为字节流和字符流两大类。
通俗理解:想象IO流就像水管,数据就是水流。字节流是原始的水流(二进制数据),字符流则是经过处理的可直接饮用的水(文本数据)。
特性 | IO (传统IO) | NIO (New IO) |
---|---|---|
数据流动方式 | 流式(Stream) | 块式(Channel和Buffer) |
缓冲机制 | 大多数操作无缓冲 | 总是使用Buffer |
阻塞/非阻塞 | 阻塞式(Blocking) | 可选择非阻塞(Non-blocking) |
选择器(Selector) | 无 | 有 |
适用场景 | 连接数较少,数据传输量大的情况 | 连接数多,每个连接数据传输量小的情况 |
通俗理解:传统IO就像单线程的餐厅服务员,一次只能服务一桌客人;NIO则像多线程服务员,可以同时照看多桌客人,哪桌有需求就去服务哪桌。
Java IO流可以按照以下维度分类:
按数据单位:
按流向:
按功能:
InputStream (抽象类)
├─ FileInputStream
├─ FilterInputStream
│ ├─ BufferedInputStream
│ ├─ DataInputStream
│ └─ PushbackInputStream
├─ ObjectInputStream
├─ PipedInputStream
├─ ByteArrayInputStream
└─ SequenceInputStream
OutputStream (抽象类)
├─ FileOutputStream
├─ FilterOutputStream
│ ├─ BufferedOutputStream
│ ├─ DataOutputStream
│ └─ PrintStream
├─ ObjectOutputStream
├─ PipedOutputStream
└─ ByteArrayOutputStream
Reader (抽象类)
├─ BufferedReader
├─ InputStreamReader
│ └─ FileReader
├─ StringReader
├─ PipedReader
└─ CharArrayReader
Writer (抽象类)
├─ BufferedWriter
├─ OutputStreamWriter
│ └─ FileWriter
├─ StringWriter
├─ PipedWriter
└─ CharArrayWriter
方法签名 | 描述 | 返回值说明 |
---|---|---|
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的位置 | 无 |
方法签名 | 描述 | 返回值说明 |
---|---|---|
void write(int b) |
写入一个字节 | 无 |
void write(byte[] b) |
写入整个字节数组 | 无 |
void write(byte[] b, int off, int len) |
写入字节数组的一部分 | 无 |
void flush() |
强制刷新输出缓冲区 | 无 |
void close() |
关闭流并释放资源 | 无 |
方法签名 | 描述 | 返回值说明 |
---|---|---|
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的位置 | 无 |
方法签名 | 描述 | 返回值说明 |
---|---|---|
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() |
关闭流 | 无 |
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();
}
}
}
缓冲流可以显著提高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
Java对象序列化可以将对象转换为字节流,便于存储或传输。
import java.io.*;
import java.util.Date;
public class SerializationExample {
public static void main(String[] args) {
// 要序列化的对象
User user = new User("张三", "[email protected]", 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;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", email='" + email + '\'' +
", birthDate=" + birthDate +
", age=" + age +
'}';
}
}
注意事项:
Serializable
接口transient
关键字可以阻止字段被序列化serialVersionUID
以确保版本兼容性NIO有三个核心组件:Channel、Buffer和Selector。
Buffer是NIO中的数据容器,主要实现类包括:
Buffer核心属性:
Buffer常用方法:
方法 | 描述 |
---|---|
allocate(int capacity) |
分配新的缓冲区 |
put() /get() |
写入/读取数据 |
flip() |
切换为读模式,limit=position, position=0 |
clear() |
清空缓冲区,position=0, limit=capacity |
compact() |
压缩缓冲区,保留未读数据 |
rewind() |
重绕缓冲区,position=0 |
mark() |
标记当前位置 |
reset() |
重置到mark位置 |
Channel是NIO中的双向数据传输通道,主要实现包括:
Channel与Stream的区别:
Selector允许单线程处理多个Channel,实现多路复用IO。
核心方法:
open()
: 创建Selectorselect()
: 阻塞直到有就绪的ChannelselectNow()
: 非阻塞检查就绪ChannelselectedKeys()
: 返回就绪的SelectionKey集合wakeup()
: 唤醒阻塞的select()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();
}
}
}
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 {
// 创建Selector
Selector 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为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)) { // 最大深度3
paths.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);
}
}
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();
}
}
}
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性能 | 推荐选择 |
---|---|---|---|
大文件顺序读写 | 高 | 更高 | NIO |
小文件随机访问 | 中等 | 高 | NIO |
高并发网络服务(1000+) | 低 | 高 | NIO |
低并发网络服务 | 中等 | 中等 | 均可 |
简单文本处理 | 高 | 中等 | IO |
使用传统IO的场景:
使用NIO的场景:
混合使用:
在实际开发中,可以结合两者的优势。例如:
内存映射文件(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; // 100MB
try (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));
}
}
}
适用场景:
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("释放文件锁");
}
}
}
}
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();
}
}
}
始终关闭资源:
使用try-with-resources确保资源被正确关闭
合理使用缓冲:
选择正确的流类型:
处理大文件:
异常处理:
性能监控:
问题:读取文本文件时出现乱码
解决方案:
// 指定正确的字符编码
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("file.txt"), "UTF-8"))) {
// 读取文件
}
问题:在Windows上无法删除或修改刚使用过的文件
解决方案:
try (FileInputStream fis = new FileInputStream(file)) {
// 使用文件
} // 自动关闭流,释放文件锁
// 或者强制释放资源
System.gc(); // 有时可以帮助释放未正确关闭的资源
问题:读取大文件时出现OutOfMemoryError
解决方案:
问题:跨平台文件路径不一致
解决方案:
// 使用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进一步增强了文件系统操作的能力。
关键点回顾:
Java 的 IO 与 NIO 流就像数据快递员,IO 慢悠悠,NIO 风驰电掣,用错了,数据就像迷路的小孩啦!
点赞的明天瘦10斤,不点的…胖在我心里(对手指)。