IO流和File
# 12.IO流和File
# 目录介绍
# 12.1 File类
# 12.1.1 File类介绍
java.io.File 类用于表示文件和目录的路径。它不能读写文件内容,只能操作文件/目录本身(创建、删除、判断是否存在等)。
注意:
File对象只是路径的抽象表示,创建File对象不会在磁盘上创建实际的文件。只有调用createNewFile()或mkdirs()等方法才会真正操作磁盘。
import java.io.File;
File file = new File("test.txt");
File dir = new File("/Users/yc/documents");
// File 的路径分隔符
// Windows 用 \,Unix/Mac 用 /
// 推荐使用 File.separator 或 / (Java 会自动转换)
File crossPlatform = new File("config" + File.separator + "app.conf");
2
3
4
5
6
7
8
9
# 12.1.2 File常用方法
File file = new File("test.txt");
// 判断
file.exists(); // 是否存在
file.isFile(); // 是否是文件
file.isDirectory(); // 是否是目录
// 获取信息
file.getName(); // 文件名
file.getPath(); // 路径
file.getAbsolutePath(); // 绝对路径
file.length(); // 文件大小(字节)
file.lastModified(); // 最后修改时间
// 创建和删除
file.createNewFile(); // 创建新文件
file.delete(); // 删除文件
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 12.1.3 目录操作
File dir = new File("mydir");
dir.mkdir(); // 创建单级目录
dir.mkdirs(); // 创建多级目录
// 列出目录内容
File[] files = dir.listFiles();
if (files != null) {
for (File f : files) {
System.out.println(f.getName() + (f.isDirectory() ? " [目录]" : ""));
}
}
2
3
4
5
6
7
8
9
10
11
# 12.1.4 综合案例:文件信息浏览器
本案例综合运用 File 类的常用方法,实现目录递归遍历和文件信息统计。
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 文件信息浏览器 —— File类综合案例
* 知识点:File创建/判断/信息获取、目录遍历、递归统计
*/
public class FileBrowser {
static void browse(File dir, int depth) {
File[] files = dir.listFiles();
if (files == null) return;
// 按目录在前、文件在后排序
java.util.Arrays.sort(files, (a, b) -> {
if (a.isDirectory() != b.isDirectory())
return a.isDirectory() ? -1 : 1;
return a.getName().compareTo(b.getName());
});
String indent = " ".repeat(depth);
for (File f : files) {
if (f.isDirectory()) {
System.out.printf("%s📁 %s/%n", indent, f.getName());
browse(f, depth + 1); // 递归遍历子目录
} else {
System.out.printf("%s📄 %-20s %s%n", indent, f.getName(), formatSize(f.length()));
}
}
}
static String formatSize(long bytes) {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
return String.format("%.1f MB", bytes / (1024.0 * 1024));
}
// 递归统计目录信息
static long[] countDir(File dir) {
long[] result = {0, 0, 0}; // [文件数, 目录数, 总大小]
File[] files = dir.listFiles();
if (files == null) return result;
for (File f : files) {
if (f.isFile()) {
result[0]++;
result[2] += f.length();
} else if (f.isDirectory()) {
result[1]++;
long[] sub = countDir(f);
result[0] += sub[0];
result[1] += sub[1];
result[2] += sub[2];
}
}
return result;
}
public static void main(String[] args) {
System.out.println("===== 文件信息浏览器 =====\n");
// 1. File 基本操作
File testDir = new File("test_browse");
testDir.mkdirs();
File subDir = new File(testDir, "sub");
subDir.mkdirs();
try {
new File(testDir, "hello.txt").createNewFile();
new File(testDir, "data.csv").createNewFile();
new File(subDir, "note.md").createNewFile();
} catch (Exception e) {
e.printStackTrace();
}
// 2. File 信息
File file = new File(testDir, "hello.txt");
System.out.println("--- 文件信息 ---");
System.out.println(" 名称: " + file.getName());
System.out.println(" 路径: " + file.getPath());
System.out.println(" 绝对路径: " + file.getAbsolutePath());
System.out.println(" 是文件: " + file.isFile());
System.out.println(" 存在: " + file.exists());
// 3. 目录树
System.out.println("\n--- 目录树 ---");
browse(testDir, 0);
// 4. 统计
long[] stats = countDir(testDir);
System.out.printf("\n--- 统计 ---\n 文件: %d 目录: %d 总大小: %s%n",
stats[0], stats[1], formatSize(stats[2]));
// 清理
new File(subDir, "note.md").delete();
subDir.delete();
new File(testDir, "hello.txt").delete();
new File(testDir, "data.csv").delete();
testDir.delete();
}
}
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# 12.1.5 File类训练题
训练1:编写一个方法 long getDirectorySize(File dir),递归计算一个目录下所有文件的总大小(字节)。
训练2:编写一个方法 List<File> findFiles(File dir, String extension),递归查找目录下所有指定扩展名的文件(如 .java)。
思考:File.delete() 只能删除空目录。如何递归删除一个非空目录?为什么 Java 不提供直接删除非空目录的方法?(提示:安全性考虑)
# 12.2 IO流概述
# 12.2.1 IO流分类
| 分类 | 说明 | 适用场景 |
|---|---|---|
| 字节流 | 以字节为单位读写(InputStream/OutputStream) | 图片、视频、二进制文件 |
| 字符流 | 以字符为单位读写(Reader/Writer) | 文本文件 |
| 缓冲流 | 带缓冲区,提高读写效率 | 大文件操作 |
# 12.2.2 IO流体系
字节流:
InputStream(抽象类)
├── FileInputStream
├── BufferedInputStream
└── ObjectInputStream
OutputStream(抽象类)
├── FileOutputStream
├── BufferedOutputStream
└── ObjectOutputStream
字符流:
Reader(抽象类)
├── FileReader
├── BufferedReader
└── InputStreamReader
Writer(抽象类)
├── FileWriter
├── BufferedWriter
└── OutputStreamWriter
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
对比 C++:C++ 使用 ifstream/ofstream/fstream 进行文件操作,Java 的 IO 体系更加分层和复杂,但也更灵活。
疑惑:为什么 Java IO 体系这么复杂,要分这么多层?
答疑:Java IO 体系的设计采用了装饰器模式(Decorator Pattern)。每一层流都是对下一层流的功能增强,而不是继承。这种设计的优势在于可以自由组合——你可以用 BufferedInputStream 包装 FileInputStream 获得缓冲能力,也可以用 BufferedInputStream 包装 SocketInputStream 获得网络流的缓冲能力,组合是灵活的。
论证:
// 装饰器模式的典型用法——层层包装
InputStream raw = new FileInputStream("data.bin"); // 基础流
InputStream buffered = new BufferedInputStream(raw); // 增加缓冲
InputStream data = new DataInputStream(buffered); // 增加读取基本类型的能力
// 等价于一行链式写法
DataInputStream dis = new DataInputStream(
new BufferedInputStream(
new FileInputStream("data.bin")));
2
3
4
5
6
7
8
9
结果展示:装饰器模式让 Java IO 的 4 个基类(InputStream/OutputStream/Reader/Writer)通过组合就能产生数十种不同功能的流,避免了继承爆炸。如果用继承实现,需要 BufferedFileInputStream、BufferedSocketInputStream、DataBufferedFileInputStream... 类数量会指数级增长。
设计原则:合成/聚合复用原则——优先使用组合而非继承来扩展功能。Java IO 是这一原则的经典实践。
# 12.2.3 综合案例:IO流分类演示器
本案例通过一个统一的演示程序,直观展示字节流、字符流、缓冲流的区别和使用场景。
import java.io.*;
/**
* IO流分类演示器 —— IO流概述综合案例
* 知识点:字节流vs字符流、装饰器模式、流的选择策略
*/
public class IOClassifyDemo {
public static void main(String[] args) throws IOException {
String testFile = "io_test.txt";
System.out.println("===== IO流分类演示 =====\n");
// 准备测试文件
try (FileWriter fw = new FileWriter(testFile)) {
fw.write("Hello你好Java世界");
}
// 1. 字节流读取 → 中文可能乱码
System.out.println("--- 字节流(逐字节)---");
try (FileInputStream fis = new FileInputStream(testFile)) {
int b;
System.out.print(" ");
while ((b = fis.read()) != -1) {
System.out.printf("%02X ", b); // 以十六进制显示每个字节
}
System.out.println();
}
// 2. 字符流读取 → 正确处理中文
System.out.println("\n--- 字符流(逐字符)---");
try (FileReader fr = new FileReader(testFile)) {
int c;
System.out.print(" ");
while ((c = fr.read()) != -1) {
System.out.print((char) c + " ");
}
System.out.println();
}
// 3. 装饰器模式:层层包装
System.out.println("\n--- 装饰器模式包装 ---");
System.out.println(" 基础流 : FileInputStream");
System.out.println(" + 缓冲 : BufferedInputStream(FileInputStream)");
System.out.println(" + 数据类型: DataInputStream(BufferedInputStream(...))");
// 4. 选择策略
System.out.println("\n--- 流选择策略 ---");
System.out.println(" 二进制文件(图片/视频) → 字节流 InputStream/OutputStream");
System.out.println(" 文本文件 → 字符流 Reader/Writer");
System.out.println(" 大文件 → 加缓冲 BufferedReader/BufferedWriter");
System.out.println(" 指定编码 → 桥接流 InputStreamReader(fis, \"UTF-8\")");
System.out.println(" 简洁操作 → NIO Files.readString()/writeString()");
// 清理
new File(testFile).delete();
}
}
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
49
50
51
52
53
54
55
56
57
# 12.2.4 IO流概述训练题
训练1:Java IO 体系使用了装饰器模式。请解释为什么 new BufferedReader(new FileReader("test.txt")) 比直接继承 FileReader 创建 BufferedFileReader 更好。
训练2:以下各场景应该选择什么类型的流?
- (a) 复制一个 JPG 图片
- (b) 逐行读取 CSV 文本文件
- (c) 读取 GBK 编码的文本文件
- (d) 将 Java 对象保存到磁盘
思考:InputStream 和 Reader 都是抽象类。为什么 Java IO 用抽象类而不是接口?(提示:抽象类可以有构造方法和成员变量)
# 12.3 字节流
# 12.3.1 FileInputStream
try (FileInputStream fis = new FileInputStream("test.txt")) {
int data;
while ((data = fis.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}
// 使用字节数组读取(更高效)
try (FileInputStream fis = new FileInputStream("test.txt")) {
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
System.out.print(new String(buffer, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 12.3.2 FileOutputStream
try (FileOutputStream fos = new FileOutputStream("output.txt")) {
String content = "Hello Java IO!";
fos.write(content.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
// 追加写入
try (FileOutputStream fos = new FileOutputStream("output.txt", true)) {
fos.write("\n追加内容".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
2
3
4
5
6
7
8
9
10
11
12
13
# 12.3.3 文件复制
public static void copyFile(String src, String dest) throws IOException {
try (FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(dest)) {
byte[] buffer = new byte[4096];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
}
}
2
3
4
5
6
7
8
9
10
# 12.3.4 综合案例:文件复制与校验
本案例综合运用字节流的读写,实现带进度和校验的文件复制。
import java.io.*;
import java.security.MessageDigest;
/**
* 文件复制与校验 —— 字节流综合案例
* 知识点:FileInputStream/FileOutputStream、缓冲区读写、字节处理
*/
public class FileCopyVerifier {
// 单字节读取 vs 缓冲区读取性能对比
static long copyWithSingleByte(File src, File dest) throws IOException {
long start = System.nanoTime();
try (FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(dest)) {
int b;
while ((b = fis.read()) != -1) {
fos.write(b);
}
}
return System.nanoTime() - start;
}
static long copyWithBuffer(File src, File dest) throws IOException {
long start = System.nanoTime();
try (FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(dest)) {
byte[] buffer = new byte[4096];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
}
return System.nanoTime() - start;
}
// 计算文件 MD5(字节流应用)
static String md5(File file) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
try (FileInputStream fis = new FileInputStream(file)) {
byte[] buffer = new byte[4096];
int len;
while ((len = fis.read(buffer)) != -1) {
md.update(buffer, 0, len);
}
}
StringBuilder sb = new StringBuilder();
for (byte b : md.digest()) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
public static void main(String[] args) throws Exception {
System.out.println("===== 文件复制与校验 =====\n");
// 创建测试文件
File src = new File("copy_test_src.dat");
try (FileOutputStream fos = new FileOutputStream(src)) {
byte[] data = new byte[10240]; // 10KB
new java.util.Random().nextBytes(data);
fos.write(data);
}
System.out.println("源文件大小: " + src.length() + " 字节");
// 缓冲区复制
File dest = new File("copy_test_dest.dat");
long bufferTime = copyWithBuffer(src, dest);
System.out.printf("缓冲区复制: %.2f ms%n", bufferTime / 1_000_000.0);
// 校验
String srcMd5 = md5(src);
String destMd5 = md5(dest);
System.out.println("源文件 MD5: " + srcMd5);
System.out.println("副本 MD5 : " + destMd5);
System.out.println("校验结果 : " + (srcMd5.equals(destMd5) ? "一致 ✓" : "不一致 ✗"));
// 清理
src.delete();
dest.delete();
}
}
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# 12.3.5 字节流训练题
训练1:使用 FileInputStream 读取一个图片文件的前8个字节,通过判断文件头(魔数)来识别图片格式:
- PNG:
89 50 4E 47 - JPEG:
FF D8 FF - GIF:
47 49 46 38
训练2:实现一个方法 void mergeFiles(String[] srcFiles, String destFile),将多个文件合并为一个文件。
思考:FileInputStream.read() 每次读取一个字节,而 read(byte[]) 每次读取一个缓冲区。假设文件大小为 1MB,两种方式分别需要多少次系统调用?性能差距有多大?
# 12.4 字符流
# 12.4.1 FileReader
try (FileReader reader = new FileReader("test.txt")) {
char[] buffer = new char[1024];
int len;
while ((len = reader.read(buffer)) != -1) {
System.out.print(new String(buffer, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
}
2
3
4
5
6
7
8
9
# 12.4.2 FileWriter
try (FileWriter writer = new FileWriter("output.txt")) {
writer.write("Hello 你好\n");
writer.write("Java 字符流\n");
} catch (IOException e) {
e.printStackTrace();
}
2
3
4
5
6
# 12.4.3 综合案例:文本文件处理器
本案例综合运用字符流的读写,实现文本文件的读取、转换和写入。
import java.io.*;
/**
* 文本文件处理器 —— 字符流综合案例
* 知识点:FileReader/FileWriter、字符数组读取、追加写入
*/
public class TextProcessor {
// 统计文本文件信息
static void analyzeText(String filename) throws IOException {
int chars = 0, lines = 0, words = 0;
try (FileReader fr = new FileReader(filename)) {
int c;
boolean inWord = false;
while ((c = fr.read()) != -1) {
chars++;
if (c == '\n') lines++;
if (Character.isWhitespace(c)) {
inWord = false;
} else if (!inWord) {
words++;
inWord = true;
}
}
lines++; // 最后一行
}
System.out.printf(" 字符: %d 行: %d 单词: %d%n", chars, lines, words);
}
// 字符转换:小写→大写,写入新文件
static void toUpperCase(String src, String dest) throws IOException {
try (FileReader fr = new FileReader(src);
FileWriter fw = new FileWriter(dest)) {
char[] buffer = new char[1024];
int len;
while ((len = fr.read(buffer)) != -1) {
for (int i = 0; i < len; i++) {
buffer[i] = Character.toUpperCase(buffer[i]);
}
fw.write(buffer, 0, len);
}
}
}
public static void main(String[] args) throws IOException {
System.out.println("===== 文本文件处理器 =====\n");
// 1. 写入文件
String testFile = "text_test.txt";
try (FileWriter fw = new FileWriter(testFile)) {
fw.write("Hello Java World\n");
fw.write("你好 世界\n");
fw.write("字符流 正确处理 中文");
}
System.out.println("写入完成");
// 2. 分析文件
System.out.println("\n--- 文本分析 ---");
analyzeText(testFile);
// 3. 字符转换
String upperFile = "text_upper.txt";
toUpperCase(testFile, upperFile);
// 4. 读取转换结果
System.out.println("\n--- 转换后内容 ---");
try (FileReader fr = new FileReader(upperFile)) {
char[] buf = new char[1024];
int len = fr.read(buf);
System.out.println(new String(buf, 0, len));
}
// 清理
new File(testFile).delete();
new File(upperFile).delete();
}
}
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# 12.4.4 字符流训练题
训练1:使用 FileReader 和 FileWriter 实现一个方法,将一个文本文件中的所有英文字母转为大写后写入新文件。
训练2:字节流也可以读取文本文件,那为什么还需要字符流?编写代码演示:用 FileInputStream 按单字节读取包含中文的 UTF-8 文件,观察会出现什么问题。
思考:FileReader 使用平台默认编码,如果一个文件是 GBK 编码而系统默认是 UTF-8,用 FileReader 读取会怎样?如何解决?
# 12.5 缓冲流
缓冲流的底层原理:BufferedReader 内部维护一个 char[] 缓冲区(默认 8192 个字符,即 8KB)。每次调用 read() 时,先从缓冲区读取;缓冲区为空时才向底层流(如 FileReader)发起一次系统级 I/O 调用,一次性填满整个缓冲区。这将大量的小粒度系统调用合并为少量大粒度调用,减少了用户态与内核态之间的上下文切换次数,从而大幅提升性能。同理,BufferedWriter 在 write() 时先写入缓冲区,满了或调用 flush() 时才真正写入底层流。
# 12.5.1 BufferedReader
try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
String line;
while ((line = br.readLine()) != null) { // 按行读取
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
2
3
4
5
6
7
8
# 12.5.2 BufferedWriter
try (BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
bw.write("第一行");
bw.newLine(); // 写入换行符(跨平台)
bw.write("第二行");
bw.newLine();
} catch (IOException e) {
e.printStackTrace();
}
2
3
4
5
6
7
8
# 12.5.3 综合案例:日志文件分析器
本案例综合运用缓冲流的按行读取和写入,实现日志文件的过滤和统计。
import java.io.*;
/**
* 日志文件分析器 —— 缓冲流综合案例
* 知识点:BufferedReader按行读取、BufferedWriter按行写入、newLine()
*/
public class LogAnalyzer {
public static void main(String[] args) throws IOException {
System.out.println("===== 日志文件分析器 =====\n");
// 1. 生成模拟日志(BufferedWriter)
String logFile = "app_test.log";
try (BufferedWriter bw = new BufferedWriter(new FileWriter(logFile))) {
String[] levels = {"INFO", "DEBUG", "WARN", "ERROR", "INFO", "ERROR", "INFO"};
String[] msgs = {"应用启动", "加载配置", "内存使用率高", "数据库连接失败",
"处理请求", "空指针异常", "请求完成"};
for (int i = 0; i < levels.length; i++) {
bw.write(String.format("[%s] 2024-01-01 10:%02d:00 %s", levels[i], i, msgs[i]));
bw.newLine();
}
}
System.out.println("日志文件已生成");
// 2. 按行读取并统计(BufferedReader)
int total = 0, errorCount = 0, warnCount = 0;
System.out.println("\n--- 日志内容 ---");
try (BufferedReader br = new BufferedReader(new FileReader(logFile))) {
String line;
while ((line = br.readLine()) != null) {
total++;
System.out.println(" " + line);
if (line.startsWith("[ERROR]")) errorCount++;
if (line.startsWith("[WARN]")) warnCount++;
}
}
// 3. 过滤ERROR日志写入新文件
String errorFile = "error_test.log";
try (BufferedReader br = new BufferedReader(new FileReader(logFile));
BufferedWriter bw = new BufferedWriter(new FileWriter(errorFile))) {
String line;
while ((line = br.readLine()) != null) {
if (line.startsWith("[ERROR]")) {
bw.write(line);
bw.newLine();
}
}
}
System.out.printf("\n--- 统计 ---\n 总计: %d ERROR: %d WARN: %d%n",
total, errorCount, warnCount);
System.out.println(" 错误日志已导出到: " + errorFile);
// 清理
new File(logFile).delete();
new File(errorFile).delete();
}
}
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
49
50
51
52
53
54
55
56
57
58
# 12.5.4 缓冲流训练题
训练1:分别使用 FileReader(不带缓冲)和 BufferedReader(带缓冲)读取一个大文件(>1MB),用 System.nanoTime() 计时比较两者的耗时差异。
训练2:使用 BufferedReader 读取一个文本文件,统计文件的行数、单词数和字符数。
思考:为什么 BufferedWriter 需要显式调用 flush() 或在 close() 时才会真正写入磁盘?如果程序崩溃,缓冲区中未写入的数据会丢失吗?如何避免?
# 12.5.5 NIO简介(JDK7+)
JDK 7 引入了 NIO.2,提供了更简洁的文件操作 API:
import java.nio.file.*;
// 一行代码读取文件所有内容(JDK 11+)
String content = Files.readString(Path.of("test.txt"));
// 一行代码读取所有行
List<String> lines = Files.readAllLines(Path.of("test.txt"));
// 一行代码写入文件
Files.writeString(Path.of("output.txt"), "Hello NIO!");
// 一行代码复制文件
Files.copy(Path.of("src.txt"), Path.of("dest.txt"), StandardCopyOption.REPLACE_EXISTING);
// 遍历目录(JDK 8+ Stream API)
try (var stream = Files.walk(Path.of("."))) {
stream.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
建议:新项目中优先使用 NIO.2 的
Files工具类,代码更简洁。传统 IO 流适合需要精细控制读写过程的场景。
# 12.6 序列化
# 12.6.1 序列化概念
序列化是将对象转换为字节流的过程(保存到文件或网络传输),反序列化是将字节流还原为对象的过程。
序列化的底层原理:Serializable 是一个标记接口(Marker Interface),没有任何方法。JVM 在序列化时通过 instanceof Serializable 判断是否可序列化。ObjectOutputStream 使用反射遍历对象的所有非 static、非 transient 字段,将字段名、类型和值写入字节流(包含一个魔数 0xACED 和版本号作为文件头)。serialVersionUID 用于版本控制——反序列化时 JVM 会比较文件中的 UID 与当前类的 UID,不一致则抛出 InvalidClassException。如果不显式指定,编译器会根据类结构自动生成,但任何修改(如新增字段)都会改变 UID,导致旧数据无法反序列化。
# 12.6.2 序列化实现
import java.io.*;
// 类必须实现 Serializable 接口
public class Account implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private double balance;
private transient String password; // transient 字段不参与序列化
public Account(String name, double balance, String password) {
this.name = name;
this.balance = balance;
this.password = password;
}
@Override
public String toString() {
return "Account{name='" + name + "', balance=" + balance + "}";
}
}
// 序列化(写入文件)
Account acc = new Account("张三", 1000, "123456");
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("account.dat"))) {
oos.writeObject(acc);
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化(从文件读取)
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("account.dat"))) {
Account loaded = (Account) ois.readObject();
System.out.println(loaded); // Account{name='张三', balance=1000.0}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
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
# 12.6.3 综合案例:对象持久化实验
本案例综合运用序列化和反序列化,演示 transient、serialVersionUID、Suppressed Exception。
import java.io.*;
import java.util.ArrayList;
import java.util.List;
/**
* 对象持久化实验 —— 序列化综合案例
* 知识点:Serializable、transient、serialVersionUID、对象读写
*/
public class PersistenceDemo {
static class Student implements Serializable {
private static final long serialVersionUID = 1L;
String name;
int score;
transient String tempNote; // 不参与序列化
Student(String name, int score, String note) {
this.name = name;
this.score = score;
this.tempNote = note;
}
@Override
public String toString() {
return String.format("%s(%d分, note=%s)", name, score, tempNote);
}
}
// 序列化多个对象
static void saveStudents(List<Student> students, String file) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream(file)))) {
oos.writeInt(students.size());
for (Student s : students) {
oos.writeObject(s);
}
}
System.out.println(" 保存 " + students.size() + " 个学生到 " + file);
}
// 反序列化
static List<Student> loadStudents(String file) throws Exception {
List<Student> list = new ArrayList<>();
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(new FileInputStream(file)))) {
int count = ois.readInt();
for (int i = 0; i < count; i++) {
list.add((Student) ois.readObject());
}
}
System.out.println(" 加载 " + list.size() + " 个学生");
return list;
}
public static void main(String[] args) throws Exception {
System.out.println("===== 对象持久化实验 =====\n");
String file = "students_test.dat";
// 1. 序列化
List<Student> original = List.of(
new Student("张三", 92, "临时笔记A"),
new Student("李四", 78, "临时笔记B"),
new Student("王五", 85, "临时笔记C")
);
System.out.println("--- 序列化前 ---");
original.forEach(s -> System.out.println(" " + s));
saveStudents(new ArrayList<>(original), file);
// 2. 反序列化
System.out.println("\n--- 反序列化后 ---");
List<Student> loaded = loadStudents(file);
loaded.forEach(s -> System.out.println(" " + s));
// 3. 观察 transient 字段
System.out.println("\n--- transient 效果 ---");
System.out.println(" tempNote 为 null(不参与序列化)✓");
// 4. 文件信息
File f = new File(file);
System.out.println("\n--- 文件信息 ---");
System.out.println(" 大小: " + f.length() + " 字节");
// 清理
f.delete();
}
}
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# 12.6.4 序列化训练题
训练1:创建一个 Student 类(name, age, score),将 5 个学生对象序列化到文件,再反序列化读回并打印。尝试修改 Student 类(如新增字段)后再反序列化旧文件,观察是否报错。
训练2:以下代码中,反序列化后 password 的值是什么?为什么?
class User implements Serializable {
private static final long serialVersionUID = 1L;
String name = "admin";
transient String password = "123456";
static String role = "user";
}
2
3
4
5
6
思考:Java 的原生序列化存在反序列化攻击风险——攻击者构造恶意字节流,反序列化时触发对象的构造方法或 readObject() 方法执行任意代码。目前业界推荐使用 JSON(Jackson/Gson)替代 Java 原生序列化。请解释 JSON 序列化为什么更安全。
# 12.6.5 桥接流详解
InputStreamReader 和 OutputStreamWriter 被称为"桥接流"——字节流到字符流的桥梁,可以指定字符编码:
// 读取 GBK 编码的文件
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream("gbk.txt"), "GBK"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
// 以 UTF-8 编码写入文件
try (BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream("utf8.txt"), "UTF-8"))) {
bw.write("你好世界");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
桥接流的底层原理:InputStreamReader 内部使用 StreamDecoder,它从底层字节流读取字节,然后通过 CharsetDecoder 将字节序列解码为字符。FileReader 其实就是 InputStreamReader 的一个简化版——它使用平台默认编码创建 InputStreamReader。
// FileReader 等价于:
new InputStreamReader(new FileInputStream("test.txt"), Charset.defaultCharset())
2