本文纲要
-
转换流概念
底层读取机制回顾
转换流的桥梁作用
体系结构与API解读 -
转换流指定编码读写
乱码问题成因
使用InputStreamReader指定码表读取
使用OutputStreamWriter指定码表写出
JDK11后字符流直接指定编码 -
对象操作流基本特点
传统写入对象属性的弊端
对象流整体写入思想 -
对象序列化——
ObjectOutputStream
序列化定义
Serializable接口与标记性接口
序列化代码示例 -
对象反序列化——
ObjectInputStream
反序列化读取对象
强转与异常处理 -
对象操作流的两个注意点
serialVersionUID序列号不一致问题
手动指定序列号 & 解决异常
transient瞬态关键字 -
对象操作流练习
多个对象的序列化与反序列化
EOFException的处理
利用集合整体序列化
转换流概念
复习字符流底层读取
字符流底层其实也是字节流,按字节逐个读取数据。
- 纯英文或数字(如ABC,对应码表值97,98,99):字节流读取97 → 98 → 99。
- 包含中文(UTF‑8编码,一个中文占3字节,例如-23, -69, -111表示一个汉字):
- 同样逐字节读取,第一个中文字节的第一个字节是负数;
- 检测到负数,就知道遇到了中文,会按当前编码一次读取多个字节(GBK读2个,UTF‑8读3个),再将这多个字节转换为字符。
真正在工作的一直是字节流,但上层我们看到的是字符流。转换流就是负责在字节流和字符流之间做转换。
- 读:字节流 → 转换流 → 字符流(字节 → 字符)
- 写:字符流 → 转换流 → 字节流(字符 → 字节)
分类
| 类型 | 输入流 | 输出流 |
|---|---|---|
| 转换流 | InputStreamReader | OutputStreamWriter |
| 别称 | 字符输入流(实质是字节→字符) | 字符输出流(实质是字符→字节) |
命名非常直观:InputStream(字节输入) + Reader(字符) → InputStreamReader;
OutputStream(字节输出) + Writer(字符) → OutputStreamWriter。
API文档中的描述:
InputStreamReader:从字节流到字符流的桥梁,读取字节并使用指定编码将其解码为字符。OutputStreamWriter:从字符流到字节流的桥梁,使用指定编码将写入的字符编码为字节。
底层源码验证
在 Java 中,FileReader 继承自 InputStreamReader,其构造方法内部实际上创建了字节流并传递给父类转换流:
// FileReader 的构造
public FileReader(String fileName) throws FileNotFoundException {
super(new FileInputStream(fileName));
}
可见,字符文件读取依赖的底层就是转换流 + 字节流。
转换流指定编码读写
乱码之源
文件编码与IDE(或程序)编码不一致时会产生乱码。
例如,Windows 记事本默认编码为 GBK,而 IDEA 默认使用 UTF‑8。
直接使用 FileReader 读取 GBK 文件:
// 方法1:直接读取会产生乱码
// 因为文件是GBK码表,而idea默认的是UTF-8编码格式
private static void method1() throws IOException {
FileReader fr = new FileReader("C:\\Users\\apple\\Desktop\\a.txt");
int ch;
while ((ch = fr.read()) != -1){
System.out.println((char) ch);
}
fr.close();
}
解决思路: 文件是什么编码,就用什么编码去读。
JDK11 之前:使用转换流指定编码
使用 InputStreamReader 指定 GBK 读取
// 如何解决乱码?
// 文件是什么码表,那么咱们就必须使用什么码表去读取
private static void method2() throws IOException {
// 指定使用GBK码表去读取文件
InputStreamReader isr = new InputStreamReader(
new FileInputStream("C:\\Users\\apple\\Desktop\\a.txt"), "GBK");
int ch;
while ((ch = isr.read()) != -1){
System.out.println((char) ch);
}
isr.close();
}
使用 OutputStreamWriter 指定 UTF‑8 写出
OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream("C:\\Users\\apple\\Desktop\\b.txt"), "UTF-8");
osw.write("我爱学习,谁也别打扰我");
osw.close();
注意:用 IDEA 以 UTF‑8 写出的文件,Windows 记事本打开时也能正确显示,因为它会自动识别编码;若另存为 ANSI(GBK),字节数会变化。
JDK11 之后:字符流直接指定编码
// 在JDK11之后,字符流新推出了一个构造,也可以指定编码表
FileReader fr = new FileReader("C:\\Users\\apple\\Desktop\\a.txt", Charset.forName("gbk"));
int ch;
while ((ch = fr.read()) != -1){
System.out.println((char) ch);
}
fr.close();
FileReader 新增的两参数构造,直接接受 Charset 对象,无需再使用转换流。
对象操作流基本特点
场景:将用户对象(用户名、密码)保存到本地文件。
传统方式:用缓冲字符流写入对象的属性值。
User user = new User("zhangsan", "qwer");
BufferedWriter bw = new BufferedWriter(new FileWriter("a.txt"));
bw.write(user.getUsername());
bw.newLine();
bw.write(user.getPassword());
bw.close();
缺陷:任何人打开 a.txt 都能直接看到用户名和密码,数据不安全。
对象操作流思想:
- 不以属性值为单位写入,而是将整个对象以字节形式写入到文件。
- 再次打开文件看到的是乱码,只有用对象输入流再读回内存,才能还原对象。
对象序列化——ObjectOutputStream
将对象以字节形式写到本地文件(或网络传输),称为序列化。
对应流:ObjectOutputStream(对象序列化流)。
序列化步骤
- 创建
ObjectOutputStream,包装一个字节输出流(如FileOutputStream)。 - 调用
writeObject(Object obj)写出对象。 - 关闭流。
User user = new User("zhangsan", "qwer");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("a.txt"));
oos.writeObject(user);
oos.close();
Serializable 接口
直接运行上述代码会抛出 NotSerializableException:
抛出一个实例需要一个 Serializable 接口。
要求:要被序列化的类必须实现 java.io.Serializable 接口。
// 如果想要这个类的对象能被序列化,那么这个类必须要实现一个接口 Serializable
// Serializable 接口的意义:
// 称之为是一个标记性接口,里面没有任何的抽象方法
// 只要一个类实现了这个Serializable接口,那么就表示这个类的对象可以被序列化
public class User implements Serializable {
private String username;
private String password;
// 构造 / getter / setter / toString...
}
再次运行序列化代码,成功将对象写入 a.txt。
对象反序列化——ObjectInputStream
将文件中保存的对象读回到内存,称为反序列化。
对应流:ObjectInputStream(对象反序列化流)。
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt"));
User o = (User) ois.readObject(); // readObject()返回Object,需要强转
System.out.println(o);
ois.close();
readObject() 返回 Object 类型,需强转为原来的具体类,并处理 ClassNotFoundException。
对象操作流的两个注意点
1 ) 序列号 serialVersionUID
现象:对类进行修改(如将 private 改为 public)后,再反序列化之前序列化的文件,会抛出 InvalidClassException。
异常关键信息:
local class incompatible:
stream classdesc serialVersionUID = -5824992206458892149,
local class serialVersionUID = 4900133124572371851
原因:
- 第一次序列化时,JVM 根据类信息(成员变量、方法等)自动计算一个序列号,并写入文件。
- 修改类之后,JVM 重新计算序列号,类中序列号与文件中的不一致,导致报错。
解决:手动固定 serialVersionUID,不让 JVM 自动计算。
public class User implements Serializable {
// serialVersionUID 序列号
// 如果我们自己没有定义,那么虚拟机会根据类中的信息自动的计算出一个序列号。
// 问题:如果我们修改了类中的信息,那么虚拟机会再次计算出一个序列号。
// 第一步:把User对象序列化到本地. --- -5824992206458892149
// 第二步:修改了javabean类. 导致 --- 类中的序列号 4900133124572371851
// 第三步:把文件中的对象读到内存. 本地中的序列号和类中的序列号不一致了.
// 解决?
// 不让虚拟机帮我们自动计算,我们自己手动给出.而且这个值不要变.
private static final long serialVersionUID = 1L;
// ...
}
定义格式:private static final long serialVersionUID = <任意值>;
小技巧:很多 Java 自带类(如 ArrayList)也实现了 Serializable 并手动指定了 serialVersionUID,可以直接参考其写法。
2 ) transient 瞬态关键字
某些成员变量的值不希望被序列化(如密码),可以在属性前加 transient 关键字。
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // 不参与序列化
// ...
}
测试:
// 序列化时写入
User user = new User("zhangsan","qwer");
oos.writeObject(user);
// 反序列化读回
User o = (User) ois.readObject();
System.out.println(o); // User{username='zhangsan', password='null'}
password 未被序列化,因此读取时为 null(默认值)。
对象操作流练习
需求:创建多个学生对象,序列化到文件,再反序列化到内存。
项目代码结构
otheriomodule/src/com/wb/convertedio/
├── Student.java
├── User.java
├── ConvertedDemo1.java
├── ConvertedDemo2.java
├── ConvertedDemo3.java
├── ConvertedDemo4.java
├── ConvertedDemo5.java
├── ConvertedDemo6.java
└── ConvertedDemo7.java
学生类定义
public class Student implements Serializable {
private static final long serialVersionUID = 2L;
private String name;
private int age;
public Student() {}
public Student(String name, int age) { this.name = name; this.age = age; }
// getter / setter / toString ...
}
写入多个对象
Student s1 = new Student("杜子腾", 16);
Student s2 = new Student("张三", 23);
Student s3 = new Student("李四", 24);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("a.txt"));
oos.writeObject(s1);
oos.writeObject(s2);
oos.writeObject(s3);
oos.close();
读取并处理 EOFException
错误示范(不能用 null 或 -1 判断结尾):
// 对象输入流读到结束不会返回null或-1,会抛出EOFException
/* while((obj = ois.readObject()) != null){
System.out.println(obj);
} */
正确方式1:捕获 EOFException
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt"));
while (true) {
try {
Object o = ois.readObject();
System.out.println(o);
} catch (EOFException e) {
break; // 到达文件末尾
}
}
ois.close();
方式2:利用集合整体序列化
一次写入一个集合对象,读取时也只需读一次,无需处理 EOFException。
Student s1 = new Student("杜子腾", 16);
Student s2 = new Student("张三", 23);
Student s3 = new Student("李四", 24);
// 写入集合
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("a.txt"));
ArrayList<Student> list = new ArrayList<>();
list.add(s1);
list.add(s2);
list.add(s3);
// 我们往本地文件中写的就是一个集合
oos.writeObject(list);
oos.close();
// 读取集合
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt"));
ArrayList<Student> list2 = (ArrayList<Student>) ois.readObject();
for (Student student : list2) {
System.out.println(student);
}
ois.close();
这种方式代码更简洁,推荐使用
总结
| 知识点 | 关键类/接口 | 要点 |
|---|---|---|
| 转换流 | InputStreamReader, OutputStreamWriter | 字节与字符流的桥梁;可指定编码读写 |
| JDK11后的简化 | FileReader, FileWriter | 构造方法可直接传入 Charset,无需显式使用转换流 |
| 对象序列化 | ObjectOutputStream | 实现 Serializable 接口,writeObject 写出整体对象 |
| 对象反序列化 | ObjectInputStream | readObject 读取并强转,注意 ClassNotFoundException |
序列号 serialVersionUID | private static final long | 防止类修改后反序列化失败,需手动指定 |
transient 关键字 | transient | 修饰的字段不参与序列化,用于敏感信息如密码 |
| 多对象的处理 | 集合 + 序列化 | 将多个对象放入集合,一次性序列化集合,避免处理 EOFException |
转换流打通了字节流与字符流的隔阂,对象操作流则为持久化对象提供了直接且安全的方案。掌握这些知识,Java I/O 的运用将更加灵活高效。
2750

被折叠的 条评论
为什么被折叠?



