一、先把核心概念讲透(纯大白话)
1. 为什么需要这两个流?
咱们先想一个场景:你想把 Java 里的ArrayList<Book>(书籍集合)存到电脑的文件里,直接存肯定不行 ——Java 的集合是「内存里的对象」,文件是「硬盘上的字节」,得有两个 “工具” 帮忙转换:
- FileOutputStream:负责「打通程序和文件的字节通道」,只能搬 “字节”,但不认识 Java 对象;
- ObjectOutputStream:负责「把 Java 对象打包成字节」,然后交给 FileOutputStream 搬到文件里。
2. 大白话解释(举生活例子)
| 流名称 | 大白话角色 | 生活比喻 |
|---|---|---|
| FileOutputStream | 「字节搬运工」 | 快递站的 “运输货车”,只能拉 “纸箱(字节)”,但不知道纸箱里装的是啥 |
| ObjectOutputStream | 「对象打包员」 | 快递站的 “打包员”,把你的 “电脑(Java 对象)” 拆成零件装纸箱,交给货车运走 |
简单说:ObjectOutputStream 负责把对象拆成字节,FileOutputStream 负责把字节送到文件里,俩必须配合用,少一个都不行。
3. 前端视角解释(前端开发者能懂的类比)
如果把 Java 程序比作「前端页面」,文件比作「后端数据库」:
- FileOutputStream = AJAX 请求的「网络通道」:只负责传输二进制数据,不管数据是啥格式;
- ObjectOutputStream = 前端的「JSON.stringify ()」:把前端的对象(比如
{bName: 'Java入门', author: '老马'})转成字符串(字节),才能通过网络通道传给后端。
反过来:
- FileInputStream = AJAX 的「响应通道」;
- ObjectInputStream = 前端的「JSON.parse ()」:把后端返回的字符串转回对象。
二、核心代码示例(书籍上架功能)
1. 第一步:写 Book 类(必须序列化,贴 “可打包” 标签)
import java.io.Serializable;
// 实现Serializable = 给Book类贴“可打包”标签,不然打包员不认识
public class Book implements Serializable {
private String bNo; // 书籍编号
private String bName; // 书籍名称
private String bAuthor;// 书籍作者
// 构造方法:创建书籍对象
public Book(String bNo, String bName, String bAuthor) {
this.bNo = bNo;
this.bName = bName;
this.bAuthor = bAuthor;
}
// getter方法:获取书籍信息
public String getbNo() { return bNo; }
public String getbName() { return bName; }
public String getbAuthor() { return bAuthor; }
// 打印书籍信息时更友好
@Override
public String toString() {
return "编号:" + bNo + " | 书名:" + bName + " | 作者:" + bAuthor;
}
}
2. 第二步:核心工具类(封装对象读写逻辑)
import java.io.*;
import java.util.ArrayList;
public class BookIOUtil {
// 固定文件路径:存书籍数据的地方
private static final String FILE_PATH = "d:\book_data.txt";
/**
* 把书籍集合写到文件里(核心:ObjectOutputStream + FileOutputStream)
* @param bookList 要保存的书籍集合
*/
public static void writeBooks(ArrayList<Book> bookList) {
// 1. 找到要存的文件(相当于找到快递收件地址)
File file = new File(FILE_PATH);
// 2. try-with-resources:自动关流,不用手动写close()
try (
// ① 初始化“运输货车”:打通程序→文件的字节通道
FileOutputStream fos = new FileOutputStream(file);
// ② 初始化“打包员”:把对象打包成字节,交给货车
ObjectOutputStream oos = new ObjectOutputStream(fos)
) {
// 3. 打包员把整个书籍集合打包,交给货车运到文件里
oos.writeObject(bookList);
System.out.println("✅ 书籍数据保存成功!");
} catch (IOException e) {
System.out.println("❌ 保存失败:" + e.getMessage());
}
}
/**
* 从文件里读取书籍集合(反向操作:ObjectInputStream + FileInputStream)
* @return 读取到的书籍集合,文件不存在则返回空集合
*/
public static ArrayList<Book> readBooks() {
File file = new File(FILE_PATH);
// 文件不存在,返回空集合
if (!file.exists()) {
return new ArrayList<>();
}
try (
// ① 初始化“收货货车”:打通文件→程序的字节通道
FileInputStream fis = new FileInputStream(file);
// ② 初始化“拆包员”:把字节拆回对象
ObjectInputStream ois = new ObjectInputStream(fis)
) {
// 3. 拆包员把文件里的字节拆回书籍集合
return (ArrayList<Book>) ois.readObject();
} catch (Exception e) {
System.out.println("❌ 读取失败:" + e.getMessage());
return new ArrayList<>();
}
}
}
3. 第三步:书籍管理主程序(上架 + 展示功能)
import java.util.ArrayList;
import java.util.Scanner;
public class BookManager {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
ArrayList<Book> bookList = BookIOUtil.readBooks(); // 先读已有的书籍
while (true) {
System.out.println("\n===== 书籍管理系统 =====");
System.out.println("1. 上架新书 2. 展示所有书籍 3. 退出");
System.out.print("请选择功能:");
int choice = sc.nextInt();
sc.nextLine(); // 清除换行符
switch (choice) {
case 1:
// 上架新书
System.out.print("请输入书籍编号:");
String bNo = sc.nextLine();
System.out.print("请输入书籍名称:");
String bName = sc.nextLine();
System.out.print("请输入书籍作者:");
String bAuthor = sc.nextLine();
bookList.add(new Book(bNo, bName, bAuthor)); // 加到集合
BookIOUtil.writeBooks(bookList); // 保存到文件
break;
case 2:
// 展示所有书籍
if (bookList.isEmpty()) {
System.out.println("⚠️ 暂无书籍,请先上架!");
break;
}
System.out.println("\n📚 所有书籍:");
for (Book book : bookList) {
System.out.println(book);
}
break;
case 3:
System.out.println("👋 退出系统!");
sc.close();
return;
default:
System.out.println("❌ 输入错误,请重新选择!");
}
}
}
}
三、关键细节(避坑指南)
-
为什么必须套两层流?
- FileOutputStream 只认字节,不认对象;ObjectOutputStream 只负责打包对象,不负责和文件交互;
- 类比:前端发请求,必须先 JSON.stringify () 转字符串,再通过 AJAX 通道发,俩步骤缺一不可。
-
Book 类必须实现 Serializable?
- 相当于给 Book 类贴 “可打包” 标签,告诉 ObjectOutputStream:这个类可以拆成字节存文件;
- 不贴标签的话,打包员会报错:“这个东西我不认识,没法打包!”。
-
try-with-resources 的好处?
- 自动关闭 fos 和 oos,不用手动写 close ();
- 类比:前端请求完自动关闭连接,不用手动清理,避免内存泄漏。
四、运行测试步骤
- 运行 BookManager,选择 1,输入书籍信息(比如:001、Java IO 流入门、老马);
- 选择 2,能看到刚上架的书籍;
- 退出程序后重新运行,选择 2,之前上架的书籍还在(因为存在文件里了);
- 打开
d:\book_data.txt,虽然是乱码(二进制),但程序能正确读取(拆包还原)。
五、核心总结
- FileOutputStream:是程序和文件之间的「字节通道」,只负责传输字节,不管内容;
- ObjectOutputStream:是「对象打包器」,把 Java 对象转成字节,交给通道传输;
- 俩配合才能实现 “对象存文件”,反向用 FileInputStream+ObjectInputStream 实现 “文件读对象”;
- 自定义类必须实现 Serializable,否则无法打包。