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 按照数据单位划分
字节流 (Byte Streams) :
以字节 (byte) 为单位进行读写,适用于处理所有类型的数据,如图片、音频、视频、二进制文件以及任何文本文件。
基类:InputStream (输入流) 和 OutputStream (输出流)。
常用子类:FileInputStream, FileOutputStream, BufferedInputStream, BufferedOutputStream, DataInputStream, DataOutputStream, ObjectInputStream, ObjectOutputStream 等。
字符流 (Character Streams) :
以字符 (char) 为单位进行读写,适用于处理文本数据。它会处理字符编码问题,确保文本的正确性。
基类:Reader (输入流) 和 Writer (输出流)。
常用子类:FileReader, FileWriter, BufferedReader, BufferedWriter, InputStreamReader, OutputStreamWriter 等。
选择建议 :
处理非文本文件(如图片、视频、序列化对象、字节码)时,必须使用字节流 。
处理纯文本文件时,推荐使用字符流 ,因为它可以自动处理字符编码,避免乱码问题。
1.1.2 按照流的角色划分
节点流 (Node Streams) / 源/目标流 :
直接连接到数据源或数据目标,如文件、内存数组或网络连接。
负责从源读取原始数据或向目标写入原始数据。
例如:FileInputStream (连接文件)、FileOutputStream (连接文件)、ByteArrayInputStream (连接字节数组)。
处理流 (Processing Streams) / 包装流 (Wrapper Streams) / 过滤流 (Filter Streams) :
不直接连接数据源或目标,而是连接到另一个流。
通过对已存在的流进行封装,增强其功能或提供更高级的 I/O 操作。
例如:BufferedInputStream (为另一个 InputStream 提供缓冲)、DataInputStream (为另一个 InputStream 提供读取基本数据类型的功能)。
组合使用 : 处理流通常会包装一个节点流,形成一个“流的链”。这种设计模式是装饰器模式 的经典应用。
graph TD
A[数据源/目标] --> B(节点流: FileInputStream/FileOutputStream)
B --> C(处理流: BufferedInputStream/BufferedOutputStream)
C --> D(处理流: DataInputStream/DataOutputStream)
D --> E(应用代码)
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!" ); 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;class User implements Serializable { private static final long serialVersionUID = 1L ; private String name; private int age; private transient String password; 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); } 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" )) { raf.write(content.getBytes(StandardCharsets.UTF_8)); System.out.println("Content written." ); raf.seek(0 ); byte [] buffer = new byte [5 ]; raf.read(buffer); System.out.println("Read from start: " + new String (buffer, StandardCharsets.UTF_8)); raf.seek(6 ); raf.read(buffer); System.out.println("Read from position 6: " + new String (buffer, StandardCharsets.UTF_8)); raf.seek(6 ); raf.write("World" .getBytes(StandardCharsets.UTF_8)); 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)); } 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(); } catch (IOException e) { e.printStackTrace(); } } } System.out.println("\n--- Using 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,实现非阻塞、多路复用。
graph TD
A[应用程序] --> B[Buffer缓冲区]
B --> C[Channel通道]
C --> D[操作系统/I/O设备]
subgraph NIO Components
C --注册到--> S[Selector选择器]
S --事件通知--> A
end
3.2 NIO 的优点与应用
非阻塞 I/O :一个线程可以处理多个连接的 I/O 操作。
提高了并发性和吞吐量 :非常适合高并发网络编程,如高性能服务器。
灵活性 :Channel 和 Buffer 提供了更细粒度的控制。
主要应用 :高性能网络编程框架 (如 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.nio 和 java.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 则是不可或缺的基础。