1. 什么是IO?为什么需要它?
在编程世界里,**I/O(Input/Output,输入/输出)** 是指计算机与外部设备(如硬盘、网络、键盘、屏幕)之间进行数据交换的过程。
在Java中,所有的I/O操作都被抽象为"流(Stream)"的概念。你可以把流想象成一根管道,数据就像水流一样,从一端流向另一端。
2. File类:文件的"身份证"
在读写文件之前,我们需要先认识文件本身。java.io.File 类并不代表文件的内容,而是代表文件或目录的路径名抽象表示。
2.1 常用操作
import java.io.File;
import java.io.IOException;
public class FileDemo {
public static void main(String\[\] args) {
// 创建一个File对象(此时文件还不一定存在)
File file = new File("test.txt");
try {
// 创建新文件
if (file.createNewFile()) {
System.out.println("文件创建成功!");
}
// 获取文件信息
System.out.println("文件名:" + file.getName());
System.out.println("绝对路径:" + file.getAbsolutePath());
System.out.println("是否可读:" + file.canRead());
System.out.println("文件大小:" + file.length() + " 字节");
} catch (IOException e) {
e.printStackTrace();
}
}
}
💡 重点记忆:
- File 只是"文件的名片",不是文件内容。
- 常用方法:exists(), isFile(), isDirectory(), mkdir(), delete()。
3. 流的分类:字节流 vs 字符流
Java的流体系非常庞大,但我们可以按两个维度来划分:
| 类型 | 抽象基类 | 处理单位 | 适用场景 |
|---|---|---|---|
| 字节流 | InputStream / OutputStream | 字节(8位) | 图片、视频、所有二进制文件 |
| 字符流 | Reader / Writer | 字符(16位) | 文本文件(.txt, .java, .csv等) |
3.1 为什么要有字符流?
因为不同国家有不同的文字编码(UTF-8、GBK等)。如果用字节流读取中文文本,可能会出现乱码。字符流内部会进行编码转换,保证你读到的是正确的字符。
4. 实战编码:文本读取与统计
4.1 需求说明
编写一个程序:
- 读取一个文本文件(如 input.txt)
- 统计文件的行数 和字符数(不含换行符)
- 将统计结果写入一个新的文件(output.txt)
4.2 使用 BufferedReader 和 BufferedWriter
为什么要用 Buffered?因为直接读写硬盘非常慢!缓冲流会在内存中开辟一个缓冲区,一次性读写一大块数据,效率提升数十倍。
完整代码示例
import java.io.*;
public class FileStatsDemo {
public static void main(String\[\] args) {
String inputFile = "input.txt";
String outputFile = "output.txt";
// 为了确保演示能运行,先创建一个测试文件
createTestFile(inputFile);
int lineCount = 0;
int charCount = 0;
// 使用 try-with-resources 自动关闭资源(非常重要!)
try (
BufferedReader br = new BufferedReader(new FileReader(inputFile));
BufferedWriter bw = new BufferedWriter(new FileWriter(outputFile))
) {
String line;
// 逐行读取
while ((line = br.readLine()) != null) {
lineCount++; // 行数累加
charCount += line.length(); // 字符数累加
}
// 写入统计结果
bw.write("===== 文件统计报告 =====");
bw.newLine(); // 写入换行符
bw.write("源文件:" + inputFile);
bw.newLine();
bw.write("总行数:" + lineCount);
bw.newLine();
bw.write("总字符数:" + charCount);
bw.newLine();
bw.write("=======================");
System.out.println("统计完成!结果已写入 " + outputFile);
} catch (IOException e) {
System.err.println("发生IO错误:" + e.getMessage());
}
}
private static void createTestFile(String fileName) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter(fileName))) {
bw.write("Hello, 世界!");
bw.newLine();
bw.write("这是第二行文本。");
bw.newLine();
bw.write("Java IO 很有趣。");
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果(output.txt内容):
===== 文件统计报告 =====
源文件:input.txt
总行数:3
总字符数:24
=======================
4.3 代码优化点解析
- try-with-resources:JDK 7+ 特性,无论是否发生异常,都会自动调用 close() 方法,防止内存泄漏。
- BufferedReader.readLine():一次读一行,返回 null 表示读到文件末尾,非常方便做行数统计。
- BufferedWriter.newLine():写入系统相关的换行符,跨平台兼容。
5. 对象序列化:让对象"永生"
前面我们处理的都是文本和字节。但如果我们想把一个Java对象 (比如一个 User 对象)保存到硬盘上,或者通过网络发送,该怎么办?这就需要序列化。
5.1 什么是序列化?
- 序列化(Serialization):把Java对象转换成字节序列的过程。
- 反序列化(Deserialization):把字节序列恢复为Java对象的过程。
5.2 实现 Serializable 接口
要让一个类可以被序列化,最简单的方式是实现 java.io.Serializable 接口。这是一个标记接口,里面没有任何方法,只是告诉JVM:"这个类的对象可以被序列化"。
import java.io.Serializable;
// 实现Serializable接口
public class User implements Serializable {
// 强烈建议显式声明serialVersionUID
private static final long serialVersionUID = 1L;
private String name;
private int age;
private String email;
// transient 关键字:表示该字段不参与序列化
private transient String password;
public User(String name, int age, String email, String password) {
this.name = name;
this.age = age;
this.email = email;
this.password = password;
}
@Override
public String toString() {
return "User{name='" + name + "', age=" + age +
", email='" + email + "', password='" + password + "'}";
}
}
5.3 序列化与反序列化实战
import java.io.*;
public class SerializationDemo {
public static void main(String\[\] args) {
String filePath = "user.ser"; // .ser 是序列化文件的常见后缀
// 1. 序列化:将对象写入文件
serializeObject(filePath);
// 2. 反序列化:从文件读取对象
deserializeObject(filePath);
}
private static void serializeObject(String filePath) {
User user = new User("张三", 25, "zhangsan@example.com", "123456");
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(filePath))) {
oos.writeObject(user);
System.out.println("对象已序列化:" + user);
System.out.println("密码字段(序列化前):" + user.getPassword()); // 注意:这里需要加getter才能访问,上面代码没写,演示用toString即可
} catch (IOException e) {
e.printStackTrace();
}
}
private static void deserializeObject(String filePath) {
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(filePath))) {
User restoredUser = (User) ois.readObject();
System.out.println("对象已反序列化:" + restoredUser);
System.out.println("密码字段(反序列化后):" + restoredUser.getPassword()); // 输出 null
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
运行输出分析:
对象已序列化:User{name='张三', age=25, email='zhangsan@example.com', password='123456'}
密码字段(序列化前):123456
对象已反序列化:User{name='张三', age=25, email='zhangsan@example.com', password='null'}
密码字段(反序列化后):null
可以看到,transient 修饰的 password 字段在反序列化后变成了 null,因为它没有被写入文件。
6. 序列化核心注意事项(面试常考!)
- serialVersionUID :
- 如果你不显式定义,JVM会根据类结构自动生成一个。一旦你修改了类(比如加了个字段),自动生成的UID会变,导致反序列化失败抛出 InvalidClassException。
- 最佳实践:永远显式声明 private static final long serialVersionUID = 1L;。
- transient 关键字 :
- 用于修饰不需要序列化的字段(如密码、敏感信息、临时缓存)。
- 父类继承 :
- 如果父类实现了 Serializable,子类自动可序列化。
- 如果父类没实现,子类序列化时,父类的字段会被忽略(需父类有无参构造器才能反序列化)。
- 静态变量不会被序列化 :
- 因为静态变量属于类,不属于对象实例。
7. 本章总结
| 知识点 | 核心类/接口 | 关键点 |
|---|---|---|
| 文件操作 | File | 抽象路径,不操作内容 |
| 字节流 | FileInputStream / FileOutputStream | 万能流,适合二进制 |
| 字符流 | FileReader / FileWriter | 专治文本乱码 |
| 缓冲流 | BufferedReader / BufferedWriter | 效率高,推荐日常使用 |
| 对象序列化 | ObjectOutputStream / ObjectInputStream | 对象持久化 |
| 标记接口 | Serializable | 开启序列化能力 |
| 控制字段 | transient | 排除敏感字段 |
8. 每日一练
- 基础:编写程序,复制一张图片(使用字节流 FileInputStream/FileOutputStream)。
- 进阶:编写一个简单的"日志系统",将程序运行的日志(带时间戳)用 BufferedWriter 追加写入 app.log 文件。
- 挑战:创建一个 Student 类(包含姓名、成绩列表),实现序列化。将3个Student对象存入文件,然后读取出来计算平均分。