IO 流
Java 的 I/O(Input/Output)流是用于处理数据输入和输出的一组抽象机制。Java 将数据的读写抽象为"流"(Stream),无论是读写文件、网络通信还是内存操作,都可以通过统一的流式 API 完成。
IO 流体系概述
Java IO 流体系按照以下两个维度进行分类:
按数据流向:
- 输入流(Input Stream):从数据源(文件、网络等)读取数据到程序
- 输出流(Output Stream):将数据从程序写入到目标(文件、网络等)
按处理单位:
- 字节流(Byte Stream):以字节(8 bit)为单位读写,适用于所有类型文件(图片、音频、视频等)
- 字符流(Character Stream):以字符(16 bit)为单位读写,适用于文本文件
两者结合得到四类基础抽象类:
| 流向 \ 单位 | 字节流 | 字符流 |
|---|---|---|
| 输入流 | InputStream | Reader |
| 输出流 | OutputStream | Writer |
字符流底层仍然基于字节流,但加入了字符编码/解码的能力,特别适合处理文本数据。
流体系类图
InputStream (抽象)
├── FileInputStream
├── BufferedInputStream
├── ObjectInputStream
└── ByteArrayInputStream
OutputStream (抽象)
├── FileOutputStream
├── BufferedOutputStream
├── ObjectOutputStream
└── ByteArrayOutputStream
Reader (抽象)
├── FileReader
├── BufferedReader
├── InputStreamReader
└── CharArrayReader
Writer (抽象)
├── FileWriter
├── BufferedWriter
├── OutputStreamWriter
└── CharArrayWriter字节流:InputStream / OutputStream
字节流是 IO 流的基础,以 8 位字节为单位进行数据读写。最常用的实现类是 FileInputStream 和 FileOutputStream。
FileInputStream
用于从文件中读取字节数据。
import java.io.FileInputStream;
import java.io.IOException;
public class FileInputStreamExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("example.txt")) {
int data;
// read() 返回下一个字节(0~255),到达末尾返回 -1
while ((data = fis.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}FileOutputStream
用于向文件中写入字节数据。
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputStreamExample {
public static void main(String[] args) {
String content = "Hello, Java IO!";
try (FileOutputStream fos = new FileOutputStream("output.txt")) {
// 将字符串转为字节数组后写入
fos.write(content.getBytes());
// 追加写入
// fos.write(content.getBytes(), 0, content.length());
} catch (IOException e) {
e.printStackTrace();
}
}
}完整示例:复制文件
使用字节流实现文件的精确复制,适用于任意类型的文件(图片、视频、文本等)。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileCopyExample {
public static void main(String[] args) {
String source = "source.jpg";
String target = "target.jpg";
// try-with-resources 自动关闭资源
try (FileInputStream fis = new FileInputStream(source);
FileOutputStream fos = new FileOutputStream(target)) {
byte[] buffer = new byte[8192]; // 8KB 缓冲区
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
System.out.println("文件复制完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
}为什么用缓冲区? 每次调用
read()只读取一个字节会频繁触发系统 I/O 操作,性能极差。使用字节数组缓冲区可以大幅减少系统调用次数,提升读写效率。
字符流:Reader / Writer
字符流以字符(char)为单位读写数据,内部自动处理字符编码和解码,专为文本文件设计。
FileReader
import java.io.FileReader;
import java.io.IOException;
public class FileReaderExample {
public static void main(String[] args) {
try (FileReader fr = new FileReader("story.txt")) {
int data;
while ((data = fr.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}FileWriter
import java.io.FileWriter;
import java.io.IOException;
public class FileWriterExample {
public static void main(String[] args) {
try (FileWriter fw = new FileWriter("story.txt")) {
fw.write("从前有座山,山里有个庙。");
fw.write(System.lineSeparator()); // 换行符
fw.write("庙里有个老和尚在讲故事。");
// 追加模式:new FileWriter("story.txt", true)
} catch (IOException e) {
e.printStackTrace();
}
}
}完整示例:复制文本文件
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class TextFileCopyExample {
public static void main(String[] args) {
try (FileReader fr = new FileReader("source.txt");
FileWriter fw = new FileWriter("target.txt")) {
char[] buffer = new char[4096]; // 4K 字符缓冲区
int charsRead;
while ((charsRead = fr.read(buffer)) != -1) {
fw.write(buffer, 0, charsRead);
}
System.out.println("文本文件复制完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
}转换流:InputStreamReader / OutputStreamWriter
转换流充当字节流和字符流之间的桥梁。InputStreamReader 将字节输入流解码为字符输入流,OutputStreamWriter 将字符输出流编码为字节输出流。
核心应用场景: 解决文件编码不一致导致的乱码问题。
InputStreamReader 示例
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.IOException;
public class InputStreamReaderExample {
public static void main(String[] args) {
// 以 UTF-8 编码读取 GBK 编码的文件——可以指定正确的编码
try (InputStreamReader isr =
new InputStreamReader(new FileInputStream("gbk.txt"), "GBK")) {
int data;
while ((data = isr.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}OutputStreamWriter 示例
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.IOException;
public class OutputStreamWriterExample {
public static void main(String[] args) {
try (OutputStreamWriter osw =
new OutputStreamWriter(new FileOutputStream("output_gbk.txt"), "GBK")) {
osw.write("这是一段中文内容,以 GBK 编码保存。");
} catch (IOException e) {
e.printStackTrace();
}
}
}完整示例:转换编码
将一个 UTF-8 编码的文本文件转换为 GBK 编码输出。
import java.io.*;
public class EncodingConvertExample {
public static void main(String[] args) {
String source = "utf8_file.txt";
String target = "gbk_file.txt";
try (InputStreamReader reader =
new InputStreamReader(new FileInputStream(source), "UTF-8");
OutputStreamWriter writer =
new OutputStreamWriter(new FileOutputStream(target), "GBK")) {
char[] buffer = new char[4096];
int len;
while ((len = reader.read(buffer)) != -1) {
writer.write(buffer, 0, len);
}
System.out.println("编码转换完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
}缓冲流:BufferedInputStream / BufferedOutputStream / BufferedReader / BufferedWriter
缓冲流在底层流的基础上添加了内部缓冲区,大幅减少实际 I/O 操作次数,从而显著提升读写性能。
为什么缓冲区能提升性能?
- 不带缓冲区:每次
read()/write()都触发一次系统调用(如磁盘 I/O) - 带缓冲区:数据先写入内存缓冲区,缓冲区满了才一次性执行系统调用
性能对比:带缓冲区 vs 不带缓冲区
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
public class PerformanceComparison {
public static void main(String[] args) throws IOException {
String sourceFile = "large_file.dat";
// 1. 不使用缓冲流——单字节读写
long start1 = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream(sourceFile);
FileOutputStream fos = new FileOutputStream("copy_no_buffer.dat")) {
int data;
while ((data = fis.read()) != -1) {
fos.write(data);
}
}
long end1 = System.currentTimeMillis();
System.out.println("无缓冲(单字节)耗时: " + (end1 - start1) + " ms");
// 2. 手动使用字节数组缓冲区
long start2 = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream(sourceFile);
FileOutputStream fos = new FileOutputStream("copy_array_buffer.dat")) {
byte[] buffer = new byte[8192];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
}
long end2 = System.currentTimeMillis();
System.out.println("字节数组缓冲区耗时: " + (end2 - start2) + " ms");
// 3. 使用 BufferedInputStream / BufferedOutputStream
long start3 = System.currentTimeMillis();
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(sourceFile));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy_buffered.dat"))) {
int data;
while ((data = bis.read()) != -1) {
bos.write(data);
}
}
long end3 = System.currentTimeMillis();
System.out.println("缓冲流耗时: " + (end3 - start3) + " ms");
// 清理临时文件
Files.deleteIfExists(Paths.get("copy_no_buffer.dat"));
Files.deleteIfExists(Paths.get("copy_array_buffer.dat"));
Files.deleteIfExists(Paths.get("copy_buffered.dat"));
}
}运行结果参考: 对于大文件,无缓冲方式可能耗时数秒甚至更久,而缓冲流方式和字节数组缓冲区方式通常只需几十毫秒。
BufferedReader 和 BufferedWriter
BufferedReader 提供了 readLine() 方法,可以按行读取文本,极大地简化了文本处理。
import java.io.*;
public class BufferedReaderExample {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("article.txt"))) {
String line;
int lineNum = 0;
while ((line = br.readLine()) != null) {
lineNum++;
System.out.printf("第 %d 行: %s%n", lineNum, line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}BufferedWriter 提供了 newLine() 方法,可以写入平台相关的换行符。
import java.io.*;
public class BufferedWriterExample {
public static void main(String[] args) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter("poem.txt"))) {
bw.write("床前明月光");
bw.newLine();
bw.write("疑是地上霜");
bw.newLine();
bw.write("举头望明月");
bw.newLine();
bw.write("低头思故乡");
} catch (IOException e) {
e.printStackTrace();
}
}
}对象流:ObjectInputStream / ObjectOutputStream
对象流用于直接将 Java 对象写入文件或从文件中读取对象,这一过程称为序列化(Serialization) 和反序列化(Deserialization)。
Serializable 接口
要使一个类的对象能够被序列化,该类必须实现 Serializable 接口。这是一个标记接口(不含任何方法),仅用于标识该类允许序列化。
import java.io.Serializable;
public class Student implements Serializable {
private String name;
private int age;
private String studentId;
public Student(String name, int age, String studentId) {
this.name = name;
this.age = age;
this.studentId = studentId;
}
@Override
public String toString() {
return "Student{name='" + name + "', age=" + age +
", studentId='" + studentId + "'}";
}
// getter 和 setter 略
}序列化与反序列化示例
import java.io.*;
public class ObjectStreamExample {
public static void main(String[] args) {
Student student = new Student("张三", 20, "2024001");
// 序列化:将对象写入文件
try (ObjectOutputStream oos =
new ObjectOutputStream(new FileOutputStream("student.dat"))) {
oos.writeObject(student);
System.out.println("序列化成功:" + student);
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化:从文件读取对象
try (ObjectInputStream ois =
new ObjectInputStream(new FileInputStream("student.dat"))) {
Student deserialized = (Student) ois.readObject();
System.out.println("反序列化成功:" + deserialized);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}transient 关键字
用 transient 修饰的字段不会被序列化。适用于敏感信息(如密码)或不需要持久化的字段。
import java.io.Serializable;
public class User implements Serializable {
private String username;
private transient String password; // 不会被序列化
private transient int loginCount; // 序列化后恢复为默认值 0
public User(String username, String password, int loginCount) {
this.username = username;
this.password = password;
this.loginCount = loginCount;
}
@Override
public String toString() {
return "User{username='" + username + "', password='" +
password + "', loginCount=" + loginCount + "}";
}
}import java.io.*;
public class TransientExample {
public static void main(String[] args) {
User user = new User("alice", "secret123", 5);
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.dat"))) {
oos.writeObject(user);
User loaded = (User) ois.readObject();
System.out.println("原始对象: " + user);
System.out.println("反序列化后: " + loaded);
// password 为 null,loginCount 为 0
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}serialVersionUID
serialVersionUID 是序列化版本号,用于验证序列化对象和反序列化时的类定义是否兼容。
import java.io.Serializable;
public class Employee implements Serializable {
// 显式声明 serialVersionUID,确保版本一致性
private static final long serialVersionUID = 1L;
private String name;
private String department;
// 假设后续版本增加了 email 字段
// private String email;
public Employee(String name, String department) {
this.name = name;
this.department = department;
}
}为什么需要 serialVersionUID? 如果没有显式声明,JVM 会根据类结构自动生成一个。当类结构发生变化(如新增字段)时,自动生成的 UID 会改变,导致反序列化抛出
InvalidClassException。显式声明可以控制兼容性行为。
序列化机制的关键规则:
| 概念 | 说明 |
|---|---|
Serializable | 标记接口,标识类可序列化 |
transient | 修饰字段,使其跳过序列化 |
serialVersionUID | 版本控制,确保序列化兼容性 |
ObjectOutputStream | 序列化输出流 |
ObjectInputStream | 反序列化输入流 |
static 字段 | 属于类而非对象,不会被序列化 |
NIO 简介
Java NIO(New IO / Non-blocking IO)是在 Java 1.4 中引入的新 IO API,位于 java.nio 包中。相比传统 IO,NIO 提供了更高效的缓冲区和通道式 I/O 操作。
Path 和 Files 类
java.nio.file.Path 和 java.nio.file.Files 是 NIO 中最核心的两个工具类,提供了简洁而强大的文件操作 API。
import java.nio.file.*;
import java.io.IOException;
import java.util.List;
public class PathFilesExample {
public static void main(String[] args) throws IOException {
// 创建 Path 对象
Path dir = Paths.get("nio_demo");
Path file = dir.resolve("hello.txt");
// 创建目录和文件
Files.createDirectories(dir);
Files.createFile(file);
// 写入内容
Files.writeString(file, "Hello NIO!\n这是第二行内容。");
// 读取所有行
List<String> lines = Files.readAllLines(file);
lines.forEach(System.out::println);
// 获取文件属性
System.out.println("文件大小: " + Files.size(file) + " 字节");
System.out.println("是否为目录: " + Files.isDirectory(dir));
System.out.println("最后修改时间: " + Files.getLastModifiedTime(file));
// 复制文件
Path copy = dir.resolve("hello_copy.txt");
Files.copy(file, copy, StandardCopyOption.REPLACE_EXISTING);
// 移动文件
Path moved = dir.resolve("hello_moved.txt");
Files.move(file, moved, StandardCopyOption.REPLACE_EXISTING);
// 删除文件
Files.deleteIfExists(moved);
Files.deleteIfExists(copy);
Files.deleteIfExists(file);
Files.deleteIfExists(dir);
}
}walk 遍历目录
Files.walk() 可以递归遍历目录树,返回一个 Stream<Path>。
import java.nio.file.*;
import java.io.IOException;
import java.util.stream.Stream;
public class WalkExample {
public static void main(String[] args) {
Path startDir = Paths.get(".");
try (Stream<Path> paths = Files.walk(startDir)) {
paths.filter(Files::isRegularFile) // 只保留普通文件
.filter(p -> p.toString().endsWith(".java")) // 只保留 .java 文件
.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
}
}list 列出目录
Files.list() 仅列出当前目录下的内容(不递归),同样返回 Stream<Path>。
import java.nio.file.*;
import java.io.IOException;
import java.util.stream.Stream;
public class ListExample {
public static void main(String[] args) throws IOException {
Path dir = Paths.get("src");
if (Files.exists(dir) && Files.isDirectory(dir)) {
try (Stream<Path> entries = Files.list(dir)) {
System.out.println("=== " + dir + " 目录下的内容 ===");
entries.forEach(entry -> {
String type = Files.isDirectory(entry) ? "[目录]" : "[文件]";
System.out.println(type + " " + entry.getFileName());
});
}
}
}
}Files 工具类常用方法
import java.nio.file.*;
import java.io.IOException;
public class FilesUtilExample {
public static void main(String[] args) throws IOException {
Path source = Paths.get("source.txt");
Path target = Paths.get("backup/source_backup.txt");
// 创建文件
Files.createFile(source);
// 写入内容
Files.writeString(source, "Hello NIO Files!");
// 创建父目录并复制
Files.createDirectories(target.getParent());
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
// 读取内容
String content = Files.readString(target);
System.out.println("读取内容: " + content);
// 判断文件是否存在
boolean exists = Files.exists(source);
System.out.println("文件是否存在: " + exists);
// 删除
Files.delete(source);
Files.deleteIfExists(target);
Files.deleteIfExists(target.getParent());
}
}ByteBuffer 和 Channel 概念
NIO 的核心是通道(Channel) 和缓冲区(Buffer) 的概念:
- Channel(通道):类似于传统 IO 流,但支持双向读写,数据总是从 Channel 读入 Buffer,或从 Buffer 写入 Channel
- Buffer(缓冲区):本质上是一个内存块,可以从中读写数据
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class ChannelBufferExample {
public static void main(String[] args) {
// 使用 FileChannel 读写文件
try (RandomAccessFile file = new RandomAccessFile("nio_channel.txt", "rw");
FileChannel channel = file.getChannel()) {
// 准备要写入的数据
String data = "Java NIO Channel 和 Buffer 示例";
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put(data.getBytes());
writeBuffer.flip(); // 切换为读模式,准备写入 Channel
channel.write(writeBuffer);
// 将位置重置到文件开头
channel.position(0);
// 读取数据
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(readBuffer);
if (bytesRead != -1) {
readBuffer.flip(); // 切换为读模式
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
System.out.println("读取内容: " + new String(bytes));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}NIO vs 传统 IO 对比
| 对比维度 | 传统 IO (java.io) | NIO (java.nio) |
|---|---|---|
| 面向对象 | 面向流(Stream-oriented) | 面向缓冲区(Buffer-oriented) |
| 方向性 | 单向(输入流或输出流) | 双向(Channel 可读可写) |
| 阻塞模式 | 阻塞 I/O | 支持非阻塞 I/O(Selector) |
| 核心组件 | InputStream/OutputStream/Reader/Writer | Channel/Buffer/Selector |
| 文件操作 | File 类功能有限 | Path + Files 提供丰富工具方法 |
| 性能 | 适合中小文件,简单场景 | 适合大文件和高并发场景 |
小结
Java IO 流体系设计精良,层次分明:
- 字节流(InputStream/OutputStream)是 IO 操作的基础,适用于所有类型文件
- 字符流(Reader/Writer)专为文本处理设计,内部处理编码转换
- 转换流(InputStreamReader/OutputStreamWriter)连接字节流和字符流,用于解决编码问题
- 缓冲流通过内部缓冲区减少系统调用,显著提升 IO 性能
- 对象流实现了 Java 对象的序列化和反序列化,配合
Serializable、transient和serialVersionUID提供完整的对象持久化方案 - NIO(Path/Files/Channel/Buffer)提供了更现代、更高效的文件操作方式,适合处理大文件和高并发场景
在实际开发中,应根据具体场景选择合适的 IO 方案:
- 操作二进制文件(图片、视频等)→ 使用字节流
- 处理文本文件 → 使用字符流,必要时加缓冲流
- 需要编码控制 → 使用转换流
- 保存对象状态 → 使用对象流
- 追求性能和现代化 API → 使用 NIO 的 Path + Files