IO 流实战:多种方式实现学生列表读写 & 效率对比


在 Java IO 开发中,字节流、缓冲流、数据流是最常用的文件读写方案。本文我们基于 学生实体类,分别不同方法实现数据的保存与读取,同时分析不同流的特性、使用区别以及运行效率。

文中所有读写方法均封装为工具静态方法,方便重复调用。


首先定义 Student 实体类,实现序列化接口,包含学号、姓名、年龄、分数、是否毕业 5 个成员属性

import java.io.Serializable;

public class Student implements Serializable {
    // 添加serialVersionUID以便序列化
    private static final long serialVersionUID = 1L;

    private String id;
    private String name;
    private int age;
    private double score;
    private boolean graduated;

    public Student() {}

    public Student(String id, String name, int age, double score, boolean graduated) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.score = score;
        this.graduated = graduated;
    }

    public String getId() { return id; }
    public void setId(String id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }

    public double getScore() { return score; }
    public void setScore(double score) { this.score = score; }

    public boolean isGraduated() { return graduated; }
    public void setGraduated(boolean graduated) { this.graduated = graduated; }

    @Override
    public String toString() {
        return String.format("Student{id='%s', name='%s', age=%d, score=%.1f, graduated=%b}",
                id, name, age, score, graduated);
    }
}

1.FileOutputStream

Java 最底层的原始字节流无缓冲区、无数据类型支持,仅单纯搬运二进制字节。

优点:底层通用、兼容性强;

缺点:频繁 IO 读写,效率偏低,需要手动处理编码、字段分隔。

保存步骤:

  1. 创建文件字节输出流,使用 try-with-resources 自动关闭流;
  2. 遍历学生列表,将对象字段拼接为字符串,以逗号分隔字段、换行分隔每条数据
  3. 统一使用 UTF-8 编码转为字节数组写入文件。
//                                           学生列表                  文件路径
//记得要抛异常,创建完字节流后可以用try-surround with resourse 可以自动关闭字节流
public static void saveByFileOutputStream(List<Student> students, String filePath) throws IOException {
    try (FileOutputStream fos = new FileOutputStream(filePath)) {
        for(Student student: students)
        {
            //用UTF-8的编码写入
            fos.write(student.getId().getBytes("UTF-8"));
            //要用‘,’来分隔每个参数,方便后面还原列表
            fos.write(',');
            fos.write(student.getName().getBytes("UTF-8"));
            fos.write(',');
            fos.write(String.valueOf(student.getAge()).getBytes("UTF-8"));
            fos.write('\n');
        }
    }
         }

加载列表步骤:

  1. 一次性读取文件全部字节,转为完整字符串;
  2. 按换行分割出每一条学生数据,再按逗号分割单个字段;
  3. 解析字段类型,封装为 Student 对象存入集合。

代码如下

public static void loadByFileInputStream(List<Student> students, String filePath) throws IOException {
        try (FileInputStream fis = new FileInputStream(filePath)) {
            byte[] bytes=fis.readAllBytes();
            String content=new String(bytes);
            String[] lines=content.split("\n");
            for(String line:lines)
            {
                //trim:移除字符串 首尾 的空格、制表符、换行、回车等空白字符,中间内容不变。
                String trimLine = line.trim();
                if (trimLine.isEmpty()) {
                    continue;
                }
                String[] fields = trimLine.split(",");
                String id = fields[0];
                String name = fields[1];
                int age = Integer.parseInt(fields[2]);
                double score = Double.parseDouble(fields[3]);
                boolean graduated = Boolean.parseBoolean(fields[4]);

                Student student = new Student(id, name, age, score, graduated);
                students.add(student);
            }
        }
    }

2.BufferedOutputStream

在基础字节流外层包装了缓冲区,数据先写入内存缓冲区,满了再一次性写入磁盘,大幅减少磁盘 IO 次数,效率远高于原始字节流

同样没有原生类型支持,仍需要手动拼接、分割字符串。

保存列表步骤:

  1. 用BufferedOutputStream包装FileOutputStream
  2. 创建一个StringBuilder来拼接每个学生的信息
  3. 将StringBuilder的字符串转换为字节
  4. 写入字节

代码如下

 public static void saveByBufferedOutputStream(List<Student> students, String filePath) throws IOException {
        try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath))) {
            StringBuilder sb=new StringBuilder();
            for(Student s: students)
            {
                sb.append(s.getId()).append(',').
                append(s.getName()).append(',').
                append(s.getAge()).append(',').
                append(s.getScore()).append(',').
                append(s.isGraduated()).append('\n');
            }
            byte[] bytes=sb.toString().getBytes(StandardCharsets.UTF_8);
            bos.write(bytes);
            bos.flush();
        }
    }

加载学生列表

  1. 创建一个学生列表来存储,后续当返回值
  2. 创建BufferedReader来读取字节
  3. 一行一行地读并写入学生列表
  4. 返回学生列表

代码如下:

 public static List<Student> loadByBufferedInputStream(String filePath) throws IOException {
        List<Student> students = new ArrayList<>();
        try (BufferedReader br = new BufferedReader
                (new InputStreamReader(new FileInputStream(filePath)))) {

            String lines;
            while ((lines = br.readLine()) != null) {
                String line = lines.trim();
                if (line.isEmpty()) {
                    continue;
                } else {
                    String[] fields = line.split(",");
                    String id = fields[0];
                    String name = fields[1];
                    int age = Integer.parseInt(fields[2]);
                    double score = Double.parseDouble(fields[3]);
                    boolean graduated = Boolean.parseBoolean(fields[4]);

                    Student student = new Student(id, name, age, score, graduated);
                    students.add(student);
                }
            }
        }
        return students;
    }

3.DataOutputStream

专门用于读写Java 基本数据类型的包装流,区别于普通字节流:

  1. 自带类型支持:提供 writeIntwriteDoublewriteUTF 等专属方法,读写时自动区分数据类型,无需手动做字符串与基本类型的转换;
  2. 纯二进制存储:文件以二进制形式保存,无法直接用文本编辑器正常查看;
  3. 强顺序要求读取字段的顺序、类型,必须和写入时完全一致,否则数据错乱、程序报错;
  4. 不支持直接读写自定义对象:需要手动拆分、拼装每一个成员字段。

保存步骤:

  1. 第一步先写入学生总数,作为读取端的循环标记;
  2. 遍历集合,按照固定顺序,逐个写入学生的学号、姓名、年龄、分数、毕业状态;
  3. 刷新缓冲区
public static void saveByDataOutputStream(List<Student> students, String filePath) throws IOException {

        try(DataOutputStream dos = new DataOutputStream(new FileOutputStream(filePath))) {
            //这个代码是让文件知道有多少个数据要读
            dos.writeInt(students.size());
            for(Student s: students)
            {
                dos.writeUTF(s.getId());
                dos.writeUTF(s.getName());
                dos.writeInt(s.getAge());
                dos.writeDouble(s.getScore());
                dos.writeBoolean(s.isGraduated());
            }
            dos.flush();
         };

}

加载步骤:

  1. 先读取提前存入的学生总数
  2. 根据总数循环,按和写入完全一致的顺序读取每一个字段;
  3. 将读取到的数据手动封装为 Student 对象,存入集合返回。
public static List<Student> loadByDataInputStream(String filePath) throws IOException {
    List<Student> students = new ArrayList<>();
    try (DataInputStream dis = new DataInputStream(new FileInputStream(filePath))) {
        int size = dis.readInt();
        for (int i = 0; i < size; i++) {
            String id = dis.readUTF();
            String name = dis.readUTF();
            int age = dis.readInt();
            double score = dis.readDouble();
            boolean graduated = dis.readBoolean();

            Student stu=new Student(id,name,age,score,graduated);
            students.add(stu);
        }
    }
    return students;
}

4.ObjectOutputStream

Java 原生对象序列化方案,直接读写完整 Java 对象 / 集合,不需要手动拆分、拼接字段;底层自带对象结构、长度信息管理,搭配缓冲流还能提升读写效率。

  1. 前提:待序列化的类必须实现Serializable序列化接口;
  2. 写入:直接把 List 整体写出,不用循环处理单条数据;
  3. 读取:一次读出整个 List 对象,仅需做类型强转;
  4. 二进制存储,不能用文本编辑器直接查看内容。

保存步骤:

多层包装流文件输出流→缓冲输出流→对象输出流,调用writeObject写入学生列表集合

public static void saveByObjectOutputStream(List<Student> students, String filePath) throws IOException {
    try (ObjectOutputStream oos = new ObjectOutputStream
            (new BufferedOutputStream(new FileOutputStream(filePath)))){
                oos.writeObject(students);
                oos.flush();
            }
}

加载步骤:

读取:多层包装流文件输入流→缓冲输入流→对象输入流,调用readObject读取后强转为List返回

public static List<Student> loadByObjectInputStream(String filePath) throws IOException, ClassNotFoundException {

    try (ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream(filePath))))
    {
        Object obj=ois.readObject();
        return (List<Student>)obj;
    }
}

5.I/O性能对比测试结果

下面让我们来对比一下这4种流的效率

数据量 方法 保存时间(ms) 加载时间(ms) 文件大小(KB) 验证结果

100 FOS 28 15 4 ✓
100 BOS 1 3 4 ✓
100 DOS 5 14 2 ✓
100 OOS 6 18 3 ✓

1000 FOS 51 19 40 ✓
1000 BOS 2 14 40 ✓
1000 DOS 41 24 29 ✓
1000 OOS 4 13 37 ✓

10000 FOS 502 41 406 ✓
10000 BOS 5 23 406 ✓
10000 DOS 389 179 295 ✓
10000 OOS 9 29 374 ✓

  1. 写入效率(由快到慢)
    BOS > OOS > DOS > FOS

  2. 读取效率(综合表现)
    小数据量下:BOS> OOS >DOS>FOS ;
    大数据量下:BOS>OOS> FOS>DOS

三、文件体积对比(由小到大)
DOS < OOS < FOS = BOS
磁盘,IO 开销极大,数据量越大。


纸上得来终觉浅,动手实测才能真切体会到不同 IO 流的差异。

IO 是 Java 基础中高频使用的知识点,理清它们的性能与适用场景,也是夯实编程能力的重要一步。

如果喜欢我的内容,觉得我的内容有帮助到你的,可以动动小手点赞收藏,我们下期再见!
在这里插入图片描述

更多推荐