编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

    • README
    • 入门教程

      • README
      • 基础语法
      • 数据类型
      • 运算符
      • 字符串和数组
      • 流程语句
      • 函数方法
      • 类和对象
      • 继承和多态
      • 接口和抽象类
      • 异常处理
      • 集合框架
      • IO流和File
        • 12.1 File类
          • 12.1.1 File类介绍
          • 12.1.2 File常用方法
          • 12.1.3 目录操作
          • 12.1.4 综合案例:文件信息浏览器
          • 12.1.5 File类训练题
        • 12.2 IO流概述
          • 12.2.1 IO流分类
          • 12.2.2 IO流体系
          • 12.2.3 综合案例:IO流分类演示器
          • 12.2.4 IO流概述训练题
        • 12.3 字节流
          • 12.3.1 FileInputStream
          • 12.3.2 FileOutputStream
          • 12.3.3 文件复制
          • 12.3.4 综合案例:文件复制与校验
          • 12.3.5 字节流训练题
        • 12.4 字符流
          • 12.4.1 FileReader
          • 12.4.2 FileWriter
          • 12.4.3 综合案例:文本文件处理器
          • 12.4.4 字符流训练题
        • 12.5 缓冲流
          • 12.5.1 BufferedReader
          • 12.5.2 BufferedWriter
          • 12.5.3 综合案例:日志文件分析器
          • 12.5.4 缓冲流训练题
          • 12.5.5 NIO简介(JDK7+)
        • 12.6 序列化
          • 12.6.1 序列化概念
          • 12.6.2 序列化实现
          • 12.6.3 综合案例:对象持久化实验
          • 12.6.4 序列化训练题
          • 12.6.5 桥接流详解
      • 线程和锁
      • 泛型
      • 注解和反射
    • 综合案例

    • 专栏博客

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Java入门精通
  • 入门教程
杨充
2026-04-07
目录

IO流和File

# 12.IO流和File

# 目录介绍

  • 12.1 File类
    • 12.1.1 File类介绍
    • 12.1.2 File常用方法
    • 12.1.3 目录操作
    • 12.1.4 综合案例:文件信息浏览器
    • 12.1.5 File类训练题
  • 12.2 IO流概述
    • 12.2.1 IO流分类
    • 12.2.2 IO流体系
    • 12.2.3 综合案例:IO流分类演示器
    • 12.2.4 IO流概述训练题
  • 12.3 字节流
    • 12.3.1 FileInputStream
    • 12.3.2 FileOutputStream
    • 12.3.3 文件复制
    • 12.3.4 综合案例:文件复制与校验
    • 12.3.5 字节流训练题
  • 12.4 字符流
    • 12.4.1 FileReader
    • 12.4.2 FileWriter
    • 12.4.3 综合案例:文本文件处理器
    • 12.4.4 字符流训练题
  • 12.5 缓冲流
    • 12.5.1 BufferedReader
    • 12.5.2 BufferedWriter
    • 12.5.3 综合案例:日志文件分析器
    • 12.5.4 缓冲流训练题
    • 12.5.5 NIO简介(JDK7+)
  • 12.6 序列化
    • 12.6.1 序列化概念
    • 12.6.2 序列化实现
    • 12.6.3 综合案例:对象持久化实验
    • 12.6.4 序列化训练题
    • 12.6.5 桥接流详解

# 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");
1
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();        // 删除文件
1
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() ? " [目录]" : ""));
    }
}
1
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();
    }
}
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
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
1
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")));
1
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();
    }
}
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
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();
}
1
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();
}
1
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);
        }
    }
}
1
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();
    }
}
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
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();
}
1
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();
}
1
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();
    }
}
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
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();
}
1
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();
}
1
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();
    }
}
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
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);
}
1
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();
}
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

# 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();
    }
}
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
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";
}
1
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("你好世界");
}
1
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())
1
2
上次更新: 2026/06/10, 11:13:41
集合框架
线程和锁

← 集合框架 线程和锁→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式