【java】IO流效率对比
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 读写,效率偏低,需要手动处理编码、字段分隔。
保存步骤:
- 创建文件字节输出流,使用
try-with-resources自动关闭流; - 遍历学生列表,将对象字段拼接为字符串,以逗号分隔字段、换行分隔每条数据;
- 统一使用 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');
}
}
}
加载列表步骤:
- 一次性读取文件全部字节,转为完整字符串;
- 按换行分割出每一条学生数据,再按逗号分割单个字段;
- 解析字段类型,封装为
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 次数,效率远高于原始字节流。
同样没有原生类型支持,仍需要手动拼接、分割字符串。
保存列表步骤:
- 用BufferedOutputStream包装FileOutputStream
- 创建一个
StringBuilder来拼接每个学生的信息 - 将StringBuilder的字符串转换为字节
- 写入字节
代码如下
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();
}
}
加载学生列表
- 创建一个学生列表来存储,后续当返回值
- 创建
BufferedReader来读取字节 - 一行一行地读并写入学生列表
- 返回学生列表
代码如下:
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 基本数据类型的包装流,区别于普通字节流:
- 自带类型支持:提供
writeInt、writeDouble、writeUTF等专属方法,读写时自动区分数据类型,无需手动做字符串与基本类型的转换; - 纯二进制存储:文件以二进制形式保存,无法直接用文本编辑器正常查看;
- 强顺序要求:读取字段的顺序、类型,必须和写入时完全一致,否则数据错乱、程序报错;
- 不支持直接读写自定义对象:需要手动拆分、拼装每一个成员字段。
保存步骤:
- 第一步先写入
学生总数,作为读取端的循环标记; - 遍历集合,按照固定顺序,逐个写入学生的学号、姓名、年龄、分数、毕业状态;
- 刷新缓冲区
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();
};
}
加载步骤:
- 先读取提前存入的
学生总数; - 根据总数循环,按和写入完全一致的顺序读取每一个字段;
- 将读取到的数据手动封装为
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 对象 / 集合,不需要手动拆分、拼接字段;底层自带对象结构、长度信息管理,搭配缓冲流还能提升读写效率。
- 前提:待序列化的类必须实现
Serializable序列化接口; - 写入:直接把 List 整体写出,不用循环处理单条数据;
- 读取:一次读出整个 List 对象,仅需做类型强转;
- 二进制存储,不能用文本编辑器直接查看内容。
保存步骤:
多层包装流文件输出流→缓冲输出流→对象输出流,调用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 ✓
-
写入效率(由快到慢)
BOS > OOS > DOS > FOS -
读取效率(综合表现)
小数据量下:BOS> OOS >DOS>FOS ;
大数据量下:BOS>OOS> FOS>DOS
三、文件体积对比(由小到大)
DOS < OOS < FOS = BOS
磁盘,IO 开销极大,数据量越大。
纸上得来终觉浅,动手实测才能真切体会到不同 IO 流的差异。
IO 是 Java 基础中高频使用的知识点,理清它们的性能与适用场景,也是夯实编程能力的重要一步。
如果喜欢我的内容,觉得我的内容有帮助到你的,可以动动小手点赞收藏,我们下期再见!
更多推荐

所有评论(0)