Java I/O (Input/Output) 库是 Java 平台处理输入和输出操作的核心组件。它提供了一套丰富的类和接口,用于读取和写入数据到各种源和目标,包括文件、内存、网络连接等。Java I/O 的设计基于流 (Stream) 的概念,数据以顺序的方式在源和目标之间流动。

核心思想:Java I/O 库通过“流”的抽象,提供统一的 API 来处理各种数据源和目标间的读写操作。它分为字节流和字符流,以及节点流和处理流,并不断演进以提供更高效和灵活的 I/O 能力 (NIO, NIO.2)。


一、Java I/O 核心概念 (Classic I/O - java.io 包)

Java 的经典 I/O 库 (java.io 包) 基于流的概念,将数据视为字节序列或字符序列。

1.1 流 (Streams) 的分类

所有 I/O 都围绕着流进行。流是一个抽象的概念,代表了数据在生产者和消费者之间传输的通道。

1.1.1 按照数据单位划分

  1. 字节流 (Byte Streams)
    • 以字节 (byte) 为单位进行读写,适用于处理所有类型的数据,如图片、音频、视频、二进制文件以及任何文本文件。
    • 基类:InputStream (输入流) 和 OutputStream (输出流)。
    • 常用子类:FileInputStream, FileOutputStream, BufferedInputStream, BufferedOutputStream, DataInputStream, DataOutputStream, ObjectInputStream, ObjectOutputStream 等。
  2. 字符流 (Character Streams)
    • 以字符 (char) 为单位进行读写,适用于处理文本数据。它会处理字符编码问题,确保文本的正确性。
    • 基类:Reader (输入流) 和 Writer (输出流)。
    • 常用子类:FileReader, FileWriter, BufferedReader, BufferedWriter, InputStreamReader, OutputStreamWriter 等。

选择建议

  • 处理非文本文件(如图片、视频、序列化对象、字节码)时,必须使用字节流
  • 处理纯文本文件时,推荐使用字符流,因为它可以自动处理字符编码,避免乱码问题。

1.1.2 按照流的角色划分

  1. 节点流 (Node Streams) / 源/目标流
    • 直接连接到数据源或数据目标,如文件、内存数组或网络连接。
    • 负责从源读取原始数据或向目标写入原始数据。
    • 例如:FileInputStream (连接文件)、FileOutputStream (连接文件)、ByteArrayInputStream (连接字节数组)。
  2. 处理流 (Processing Streams) / 包装流 (Wrapper Streams) / 过滤流 (Filter Streams)
    • 不直接连接数据源或目标,而是连接到另一个流。
    • 通过对已存在的流进行封装,增强其功能或提供更高级的 I/O 操作。
    • 例如:BufferedInputStream (为另一个 InputStream 提供缓冲)、DataInputStream (为另一个 InputStream 提供读取基本数据类型的功能)。

组合使用
处理流通常会包装一个节点流,形成一个“流的链”。这种设计模式是装饰器模式的经典应用。

1.2 常用经典 I/O 类详解

1.2.1 文件 I/O (字节流)

  • FileInputStream:从文件中读取字节。
  • FileOutputStream:向文件中写入字节。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileByteStreamExample {
public static void main(String[] args) {
String sourceFile = "input.txt";
String destFile = "output_byte.txt";

try (FileInputStream fis = new FileInputStream(sourceFile);
FileOutputStream fos = new FileOutputStream(destFile)) {

int byteRead;
while ((byteRead = fis.read()) != -1) { // 逐字节读取
fos.write(byteRead); // 逐字节写入
}
System.out.println("File copied successfully using byte streams!");
} catch (IOException e) {
e.printStackTrace();
}
}
}

1.2.2 文件 I/O (字符流)

  • FileReader:从文件中读取字符。
  • FileWriter:向文件中写入字符。
  • InputStreamReader / OutputStreamWriter:用于在字节流和字符流之间进行转换,可以指定字符编码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class FileCharStreamExample {
public static void main(String[] args) {
String sourceFile = "input.txt";
String destFile = "output_char.txt";

try (FileReader fr = new FileReader(sourceFile);
FileWriter fw = new FileWriter(destFile)) {

int charRead;
while ((charRead = fr.read()) != -1) { // 逐字符读取
fw.write(charRead); // 逐字符写入
}
System.out.println("File copied successfully using character streams!");
} catch (IOException e) {
e.printStackTrace();
}
}
}

1.2.3 缓冲流 (Buffered Streams)

通过在内存中设置缓冲区,减少实际的物理 I/O 操作次数,从而提高读写性能。

  • BufferedInputStream, BufferedOutputStream (字节缓冲流)
  • BufferedReader, BufferedWriter (字符缓冲流)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class BufferedStreamExample {
public static void main(String[] args) {
String sourceFile = "large_input.txt"; // 假设这是一个大文件
String destFile = "large_output.txt";

// 创建一个大文件用于测试
try (BufferedWriter writer = new BufferedWriter(new FileWriter(sourceFile))) {
for (int i = 0; i < 100000; i++) {
writer.write("This is a test line " + i + "\n");
}
} catch (IOException e) {
e.printStackTrace();
}

long startTime = System.nanoTime();
try (BufferedReader br = new BufferedReader(new FileReader(sourceFile));
BufferedWriter bw = new BufferedWriter(new FileWriter(destFile))) {

String line;
while ((line = br.readLine()) != null) { // 逐行读取
bw.write(line);
bw.newLine(); // 写入换行符
}
System.out.println("File copied successfully using buffered streams!");
} catch (IOException e) {
e.printStackTrace();
}
long endTime = System.nanoTime();
System.out.println("Time taken with buffering: " + (endTime - startTime) / 1_000_000 + " ms");
}
}

1.2.4 数据流 (Data Streams)

允许以平台无关的方式读写 Java 的基本数据类型 (int, double, boolean 等) 和字符串。

  • DataInputStream, DataOutputStream
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class DataStreamExample {
public static void main(String[] args) {
String fileName = "data.bin";

// 写入数据
try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(fileName))) {
dos.writeInt(123);
dos.writeDouble(3.14);
dos.writeBoolean(true);
dos.writeUTF("Hello, Data Stream!"); // UTF-8 编码的字符串
System.out.println("Data written to " + fileName);
} catch (IOException e) {
e.printStackTrace();
}

// 读取数据
try (DataInputStream dis = new DataInputStream(new FileInputStream(fileName))) {
int i = dis.readInt();
double d = dis.readDouble();
boolean b = dis.readBoolean();
String s = dis.readUTF();
System.out.println("Read data: int=" + i + ", double=" + d + ", boolean=" + b + ", String=" + s);
} catch (IOException e) {
e.printStackTrace();
}
}
}

1.2.5 对象流 (Object Streams)

用于实现对象的序列化 (Serialization)反序列化 (Deserialization),即将对象转换为字节序列以便存储或传输,以及将字节序列恢复为对象。

  • ObjectInputStream, ObjectOutputStream
  • 对象必须实现 java.io.Serializable 接口才能被序列化。
  • transient 关键字:被 transient 修饰的字段在对象序列化时不会被保存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

// 必须实现 Serializable 接口
class User implements Serializable {
private static final long serialVersionUID = 1L; // 序列化版本UID
private String name;
private int age;
private transient String password; // 标记为 transient,不参与序列化

public User(String name, int age, String password) {
this.name = name;
this.age = age;
this.password = password;
}

@Override
public String toString() {
return "User{name='" + name + "', age=" + age + ", password='" + password + "'}";
}
}

public class ObjectStreamExample {
public static void main(String[] args) {
String fileName = "user.ser";
User user = new User("Alice", 30, "mySecretPassword");

// 序列化对象
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName))) {
oos.writeObject(user);
System.out.println("User object serialized to " + fileName);
} catch (IOException e) {
e.printStackTrace();
}

// 反序列化对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName))) {
User deserializedUser = (User) ois.readObject();
System.out.println("User object deserialized: " + deserializedUser); // password 将为 null
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}

1.2.6 File

java.io.File 类用于表示文件或目录的路径名抽象。它提供了创建、删除、重命名文件和目录,查询文件属性(大小、修改时间)等操作。注意:File 类本身不进行实际的 I/O 操作,它只处理文件系统路径和元数据。

1.2.7 RandomAccessFile

RandomAccessFile 允许在文件中的任何位置进行随机读写,而不是只能顺序读写。它既可以作为输入流也可以作为输出流,并且支持定位到文件的特定位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;

public class RandomAccessFileExample {
public static void main(String[] args) {
String fileName = "random_access.txt";
String content = "Hello RandomAccessFile!";

try (RandomAccessFile raf = new RandomAccessFile(fileName, "rw")) { // "rw" 读写模式
// 写入数据
raf.write(content.getBytes(StandardCharsets.UTF_8));
System.out.println("Content written.");

// 移动到文件开头
raf.seek(0);
byte[] buffer = new byte[5];
raf.read(buffer); // 读取 "Hello"
System.out.println("Read from start: " + new String(buffer, StandardCharsets.UTF_8));

// 移动到特定位置 (跳过 "Hello ")
raf.seek(6);
raf.read(buffer); // 读取 "Rando"
System.out.println("Read from position 6: " + new String(buffer, StandardCharsets.UTF_8));

// 在特定位置写入 (覆盖部分内容)
raf.seek(6); // 再次移动到 "Random" 的 'R'
raf.write("World".getBytes(StandardCharsets.UTF_8)); // 覆盖 "Rando" 为 "World"

// 移动到文件开头,读取全部内容
raf.seek(0);
byte[] fullContent = new byte[(int) raf.length()];
raf.readFully(fullContent);
System.out.println("Full content after modification: " + new String(fullContent, StandardCharsets.UTF_8));
// 预期输出: Hello WorldomAccessFile!
} catch (IOException e) {
e.printStackTrace();
}
}
}

二、资源管理与异常处理

2.1 try-with-resources 语句

在 Java 7 引入的 try-with-resources 语句是管理 I/O 资源的最佳实践。它确保在 try 块结束后,所有实现了 java.lang.AutoCloseable 接口的资源(包括所有的 I/O 流)都会被自动关闭,无论是否发生异常。这极大地简化了代码并避免了资源泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class TryWithResourcesExample {
public static void main(String[] args) {
String fileName = "input.txt"; // 假设存在此文件

// 传统方式 (需要手动关闭资源,可能因异常导致资源泄漏)
BufferedReader reader1 = null;
try {
reader1 = new BufferedReader(new FileReader(fileName));
String line;
while ((line = reader1.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader1 != null) {
try {
reader1.close(); // 必须在 finally 块中关闭
} catch (IOException e) {
e.printStackTrace();
}
}
}

System.out.println("\n--- Using try-with-resources ---");

// 使用 try-with-resources (推荐)
try (BufferedReader reader2 = new BufferedReader(new FileReader(fileName))) {
String line;
while ((line = reader2.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

2.2 I/O 异常 (IOException)

几乎所有的 I/O 操作都可能抛出 IOException 或其子类。捕获这些异常是健壮 I/O 编程的关键。

三、Java NIO (New I/O - java.nio 包) 概述

自 Java 1.4 引入的 NIO (New I/O) 库,提供了一种与传统 I/O 不同的处理方式,主要特点是非阻塞 I/O

3.1 为什么引入 NIO?

传统 I/O (阻塞 I/O - BIO) 的一个主要问题是,当一个线程执行读写操作时,它会被阻塞直到数据可用或写入完成。在高并发场景下,这会导致服务器需要创建大量线程,消耗大量资源。

NIO 通过以下核心组件解决了这个问题:

  • Channel (通道):类似于流,但它是双向的,可以读也可以写。
  • Buffer (缓冲区):所有数据都通过缓冲区读写,这是数据与 Channel 交互的唯一方式。
  • Selector (选择器):一个 Selector 可以监听多个 Channel 上的 I/O 事件 (如连接、读、写),允许单个线程管理多个 Channel,实现非阻塞、多路复用。

3.2 NIO 的优点与应用

  • 非阻塞 I/O:一个线程可以处理多个连接的 I/O 操作。
  • 提高了并发性和吞吐量:非常适合高并发网络编程,如高性能服务器。
  • 灵活性ChannelBuffer 提供了更细粒度的控制。

主要应用:高性能网络编程框架 (如 Netty、Mina)、Web 服务器、消息队列等。

四、Java NIO.2 (Files API - java.nio.file 包) 概述

Java 7 引入的 NIO.2 进一步增强了文件系统操作,提供了一套更加现代、强大和易用的文件 I/O API。

4.1 为什么引入 NIO.2?

传统的 java.io.File 类有诸多限制:

  • 方法较少,功能有限。
  • 路径处理不够灵活。
  • 错误处理不完善。
  • 不支持符号链接等高级文件系统特性。

NIO.2 旨在提供一个功能更强大、更健壮、更统一的文件系统接口。

4.2 核心类

  • Path:代表文件系统中的路径,替代了 File 类在路径操作上的不足。
  • Paths:用于创建 Path 实例的工厂类。
  • Files:一个静态工具类,提供了大量用于文件和目录操作的方法,如复制、移动、删除、创建、读取属性、遍历文件树等。

4.3 NIO.2 的优点与常见操作

  • 丰富的操作:提供了更多文件系统操作方法。
  • 更强大的路径操作:支持相对路径、绝对路径、规范化等。
  • 符号链接支持:更好地处理符号链接。
  • 原子操作:一些文件操作保证原子性。
  • 遍历文件树:通过 Files.walkFileTree() 递归遍历目录。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

public class NIO2Example {
public static void main(String[] args) {
Path path = Paths.get("new_file.txt");
String content = "Hello, NIO.2!";

try {
// 写入文件
Files.write(path, content.getBytes());
System.out.println("File written using NIO.2: " + path.toAbsolutePath());

// 读取文件所有行
List<String> lines = Files.readAllLines(path);
System.out.println("File content: " + lines.get(0));

// 检查文件是否存在
boolean exists = Files.exists(path);
System.out.println("File exists: " + exists);

// 删除文件
Files.delete(path);
System.out.println("File deleted.");
System.out.println("File exists after deletion: " + Files.exists(path));

} catch (IOException e) {
e.printStackTrace();
}
}
}

五、总结与选择

Java I/O 库从最初的 java.io 包发展到 java.niojava.nio.file,提供了不同层次的抽象和功能来满足各种 I/O 需求。

  • 经典 I/O (java.io)

    • 优点:易于理解和使用,适用于大多数常规的文件和流操作,特别是处理小文件和顺序读写。
    • 缺点:阻塞 I/O,在大规模并发场景下效率较低。
    • 适用场景:日常文件读写、配置读取、简单的数据处理。
  • NIO (java.nio)

    • 优点:非阻塞、基于事件驱动,适用于高并发、高性能的网络应用。
    • 缺点:API 相对复杂,学习曲线较陡峭。
    • 适用场景:高性能网络服务器、聊天应用、消息队列、实现自定义网络协议。
  • NIO.2 (java.nio.file)

    • 优点:功能强大、API 现代且易用,提供了丰富的文件系统操作,解决了 File 类的不足。
    • 缺点:主要用于文件系统操作,不直接涉及流的读写。
    • 适用场景:所有涉及文件和目录操作的场景,如文件管理工具、文件处理服务、大型项目中的文件系统交互。

在实际开发中,通常会根据具体需求混合使用这些 API。例如,对于文件系统操作,优先使用 NIO.2;对于常规的流式数据处理,java.io 配合 try-with-resources 依然是简洁高效的选择;而对于高并发网络通信,NIO 则是不可或缺的基础。