Java的输入/输出(IO)系统是程序与外部世界(如文件系统、网络、内存)进行数据交互的核心。它提供了一套强大但略显复杂的类库。本教程将带你从基础概念入手,逐步掌握Java IO的核心用法和最佳实践。
一、核心概念:流(Stream)
在Java中,流(Stream) 是对数据传输通道的抽象。你可以把它想象成一根管道,数据从源头(Source)通过这根管道流入程序(输入流),或者从程序通过这根管道流向目的地(Destination)(输出流)。
根据处理数据的类型,Java IO流主要分为两大类:
- 字节流(Byte Stream)
- 处理单位 :8位字节(
byte)。 - 核心抽象类 :
InputStream(输入)和OutputStream(输出)。 - 适用场景 :处理所有类型的数据,尤其是二进制数据,如图片、音频、视频、可执行文件等。字节流是"万能"的,因为它不关心数据内容,只负责搬运字节。
- 处理单位 :8位字节(
- 字符流(Character Stream)
- 处理单位 :16位字符(
char)。 - 核心抽象类 :
Reader(输入)和Writer(输出)。 - 适用场景 :专门用于处理文本数据 (
.txt,.java,.xml等)。字符流在底层会自动处理字节与字符之间的编码转换,使得读写文本更加方便,但必须注意字符编码问题。
- 处理单位 :16位字符(
一句话总结:处理文本用字符流,处理其他所有文件用字节流。
二、基石:File类
java.io.File 类是文件和目录路径名的抽象表示。它本身不用于读写文件内容,而是用于操作文件和目录的元数据,如创建、删除、重命名、判断是否存在等。
常用方法一览
| 方法分类 | 方法签名 | 说明 |
|---|---|---|
| 创建 | boolean createNewFile() |
原子性地创建一个新文件。 |
boolean mkdir() |
创建一个目录。 | |
boolean mkdirs() |
创建一个目录,包括任何必需但不存在的父目录。 | |
| 删除 | boolean delete() |
删除此抽象路径名表示的文件或目录。 |
| 判断 | boolean exists() |
测试此抽象路径名表示的文件或目录是否存在。 |
boolean isFile() |
测试此抽象路径名表示的文件是否是一个标准文件。 | |
boolean isDirectory() |
测试此抽象路径名表示的文件是否是一个目录。 | |
| 获取信息 | String getName() |
返回由此抽象路径名表示的文件或目录的名称。 |
String getAbsolutePath() |
返回此抽象路径名的绝对路径名字符串。 | |
long length() |
返回由此抽象路径名表示的文件的长度(字节)。 |
代码示例
import java.io.File;
import java.io.IOException;
public class FileDemo {
public static void main(String[] args) {
// 创建一个File对象,指向 "test_dir/test_file.txt"
File file = new File("test_dir", "test_file.txt");
// 1. 获取并打印父目录的绝对路径
File parentDir = file.getParentFile();
System.out.println("父目录路径: " + parentDir.getAbsolutePath());
// 2. 如果父目录不存在,则创建它(包括所有不存在的父级)
if (!parentDir.exists()) {
boolean dirCreated = parentDir.mkdirs();
System.out.println("目录创建" + (dirCreated ? "成功" : "失败"));
}
// 3. 如果文件不存在,则创建它
try {
if (!file.exists()) {
boolean fileCreated = file.createNewFile();
System.out.println("文件创建" + (fileCreated ? "成功" : "失败"));
}
} catch (IOException e) {
e.printStackTrace();
}
// 4. 打印文件信息
System.out.println("文件名: " + file.getName());
System.out.println("是否是一个文件: " + file.isFile());
}
}
三、字节流实战:复制任意文件
字节流是处理二进制数据的首选。下面我们通过复制一张图片来演示 FileInputStream 和 FileOutputStream 的用法。
关键点 :使用缓冲区(byte[] buffer)可以极大地提高IO效率,避免频繁的磁盘读写。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamCopy {
public static void main(String[] args) {
// 源文件和目标文件路径
String sourcePath = "source_image.jpg";
String destPath = "dest_image.jpg";
// 使用 try-with-resources 语句,自动关闭流
try (FileInputStream fis = new FileInputStream(sourcePath);
FileOutputStream fos = new FileOutputStream(destPath)) {
byte[] buffer = new byte[8192]; // 创建一个8KB的缓冲区
int bytesRead;
// 从输入流中读取数据,直到文件末尾(read返回-1)
while ((bytesRead = fis.read(buffer)) != -1) {
// 将读取到的数据写入输出流
fos.write(buffer, 0, bytesRead);
}
System.out.println("文件复制成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
四、字符流实战:读写文本文件
字符流专为文本设计。我们使用 FileReader 和 FileWriter 来读写一个简单的文本文件。
重要警告 :FileReader 和 FileWriter 会使用操作系统的默认字符编码 (例如在中文Windows上是GBK)。如果文件是UTF-8编码,直接读写会导致中文乱码。
最佳实践 :使用 InputStreamReader 和 OutputStreamWriter 作为"桥梁",并显式指定字符编码(如 StandardCharsets.UTF_8)。
import java.io.*;
import java.nio.charset.StandardCharsets;
public class CharacterStreamDemo {
public static void main(String[] args) {
String filePath = "test.txt";
String contentToWrite = "你好,Java IO!\nHello, World!";
// --- 写入文件 ---
// 使用 OutputStreamWriter 并指定 UTF-8 编码,避免乱码
try (OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream(filePath), StandardCharsets.UTF_8);
BufferedWriter writer = new BufferedWriter(osw)) {
writer.write(contentToWrite);
System.out.println("文本写入成功!");
} catch (IOException e) {
e.printStackTrace();
}
// --- 读取文件 ---
// 使用 InputStreamReader 并指定 UTF-8 编码,确保正确读取
try (InputStreamReader isr = new InputStreamReader(
new FileInputStream(filePath), StandardCharsets.UTF_8);
BufferedReader reader = new BufferedReader(isr)) {
String line;
System.out.println("--- 文件内容 ---");
// readLine() 方法可以按行读取,非常方便
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
五、性能优化:缓冲流(Buffered Stream)
无论是字节流还是字符流,直接使用底层的 FileInputStream 或 FileReader 进行逐字节/字符的读写,效率都非常低下,因为每次读写都可能触发一次昂贵的磁盘I/O操作。
缓冲流 (BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter)通过在内存中维护一个缓冲区来解决这个问题。它们一次性从磁盘读取一大块数据到缓冲区,后续的读取操作直接从内存中获取,极大地减少了磁盘访问次数。
性能对比:复制一个10MB的文件,使用缓冲流比不使用快数百倍。
如何使用 :非常简单,只需将节点流(如 FileInputStream)作为参数传递给缓冲流的构造函数即可。
// 字节缓冲流示例
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("source.jpg"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("dest.jpg"))) {
// ... 读写逻辑与之前相同,但效率极高
}
// 字符缓冲流示例
try (BufferedReader br = new BufferedReader(new FileReader("source.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("dest.txt"))) {
String line;
while ((line = br.readLine()) != null) { // readLine() 是BufferedReader的独有方法
bw.write(line);
bw.newLine(); // 写入一个平台相关的换行符
}
}
六、资源管理:try-with-resources
在Java 7之前,我们必须在一个 finally 块中手动关闭所有打开的流,代码冗长且容易出错。
Java 7引入了 try-with-resources 语句,它确保每个声明的资源在语句结束时都会被自动关闭。任何实现了 java.lang.AutoCloseable 接口的对象都可以在此语句中使用,而所有的IO流类都实现了这个接口。
语法
try (ResourceType resource = new ResourceType()) {
// 使用资源
} catch (Exception e) {
// 处理异常
}
优势
- 代码简洁 :无需编写
finally块。 - 安全可靠 :即使
try块中发生异常,资源也会被自动、正确地关闭,有效防止资源泄漏(如"Too many open files"错误)。
本教程中所有代码示例均已采用此最佳实践。
七、高级主题:对象序列化
Java提供了一种机制,可以将对象的状态(即对象的成员变量值)转换为字节流,以便保存到文件中或通过网络传输。这个过程称为序列化(Serialization) 。反之,从字节流恢复对象的过程称为反序列化(Deserialization)。
如何使一个类可序列化
只需让该类实现 java.io.Serializable 接口。这是一个标记接口,内部没有方法。
import java.io.Serializable;
import java.util.Date;
public class User implements Serializable {
// 建议声明一个序列版本号,用于验证序列化与反序列化版本的兼容性
private static final long serialVersionUID = 1L;
private String username;
private transient Date lastLoginTime; // transient关键字表示此字段不参与序列化
public User(String username) {
this.username = username;
this.lastLoginTime = new Date();
}
// ... getter, setter, toString 等方法
}
序列化与反序列化示例
import java.io.*;
public class SerializationDemo {
public static void main(String[] args) {
User user = new User("alice");
String filePath = "user.ser";
// --- 序列化:将对象写入文件 ---
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath))) {
oos.writeObject(user);
System.out.println("对象序列化成功!");
} catch (IOException e) {
e.printStackTrace();
}
// --- 反序列化:从文件读取对象 ---
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath))) {
User loadedUser = (User) ois.readObject();
System.out.println("对象反序列化成功!");
System.out.println("用户名: " + loadedUser.username);
// transient字段在反序列化后为null
System.out.println("上次登录时间: " + loadedUser.lastLoginTime);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
总结与最佳实践
- 明确数据流类型 :处理文本用字符流(
Reader/Writer),处理二进制文件用字节流(InputStream/OutputStream)。 - 始终指定字符编码 :使用
InputStreamReader/OutputStreamWriter并显式传入StandardCharsets.UTF_8,彻底告别中文乱码。 - 拥抱缓冲流 :永远使用
Buffered*流来包装节点流,这是提升IO性能最简单有效的方法。 - 使用 try-with-resources:这是管理IO资源的标准方式,能自动关闭流,保证代码的健壮性。
- 了解NIO :对于更复杂、高性能的IO需求(如网络服务器、大文件映射),可以进一步学习Java NIO(New IO)和NIO.2(
java.nio.file包)。