Java基础快速入门: 转换流与对象操作流
本文纲要
转换流概念
底层读取机制回顾
转换流的桥梁作用
体系结构与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 的构造publicFileReader(StringfileName)throwsFileNotFoundException{super(newFileInputStream(fileName));}可见,字符文件读取依赖的底层就是转换流 + 字节流。
转换流指定编码读写
乱码之源
文件编码与IDE(或程序)编码不一致时会产生乱码。
例如,Windows 记事本默认编码为GBK,而 IDEA 默认使用UTF‑8。
直接使用FileReader读取GBK文件:
// 方法1:直接读取会产生乱码// 因为文件是GBK码表,而idea默认的是UTF-8编码格式privatestaticvoidmethod1()throwsIOException{FileReaderfr=newFileReader("C:\\Users\\apple\\Desktop\\a.txt");intch;while((ch=fr.read())!=-1){System.out.println((char)ch);}fr.close();}解决思路: 文件是什么编码,就用什么编码去读。
JDK11 之前:使用转换流指定编码
使用InputStreamReader指定GBK读取
// 如何解决乱码?// 文件是什么码表,那么咱们就必须使用什么码表去读取privatestaticvoidmethod2()throwsIOException{// 指定使用GBK码表去读取文件InputStreamReaderisr=newInputStreamReader(newFileInputStream("C:\\Users\\apple\\Desktop\\a.txt"),"GBK");intch;while((ch=isr.read())!=-1){System.out.println((char)ch);}isr.close();}使用OutputStreamWriter指定UTF‑8写出
OutputStreamWriterosw=newOutputStreamWriter(newFileOutputStream("C:\\Users\\apple\\Desktop\\b.txt"),"UTF-8");osw.write("我爱学习,谁也别打扰我");osw.close();注意:用 IDEA 以 UTF‑8 写出的文件,Windows 记事本打开时也能正确显示,因为它会自动识别编码;若另存为 ANSI(GBK),字节数会变化。
JDK11 之后:字符流直接指定编码
// 在JDK11之后,字符流新推出了一个构造,也可以指定编码表FileReaderfr=newFileReader("C:\\Users\\apple\\Desktop\\a.txt",Charset.forName("gbk"));intch;while((ch=fr.read())!=-1){System.out.println((char)ch);}fr.close();FileReader新增的两参数构造,直接接受Charset对象,无需再使用转换流。
对象操作流基本特点
场景:将用户对象(用户名、密码)保存到本地文件。
传统方式:用缓冲字符流写入对象的属性值。
Useruser=newUser("zhangsan","qwer");BufferedWriterbw=newBufferedWriter(newFileWriter("a.txt"));bw.write(user.getUsername());bw.newLine();bw.write(user.getPassword());bw.close();缺陷:任何人打开 a.txt 都能直接看到用户名和密码,数据不安全。
对象操作流思想:
- 不以属性值为单位写入,而是将整个对象以字节形式写入到文件。
- 再次打开文件看到的是乱码,只有用对象输入流再读回内存,才能还原对象。
对象序列化——ObjectOutputStream
将对象以字节形式写到本地文件(或网络传输),称为序列化。
对应流:ObjectOutputStream(对象序列化流)。
序列化步骤
- 创建
ObjectOutputStream,包装一个字节输出流(如FileOutputStream)。 - 调用
writeObject(Object obj)写出对象。 - 关闭流。
Useruser=newUser("zhangsan","qwer");ObjectOutputStreamoos=newObjectOutputStream(newFileOutputStream("a.txt"));oos.writeObject(user);oos.close();Serializable接口
直接运行上述代码会抛出NotSerializableException:
抛出一个实例需要一个Serializable接口。
要求:要被序列化的类必须实现java.io.Serializable接口。
// 如果想要这个类的对象能被序列化,那么这个类必须要实现一个接口 Serializable// Serializable 接口的意义:// 称之为是一个标记性接口,里面没有任何的抽象方法// 只要一个类实现了这个Serializable接口,那么就表示这个类的对象可以被序列化publicclassUserimplementsSerializable{privateStringusername;privateStringpassword;// 构造 / getter / setter / toString...}再次运行序列化代码,成功将对象写入 a.txt。
对象反序列化——ObjectInputStream
将文件中保存的对象读回到内存,称为反序列化。
对应流:ObjectInputStream(对象反序列化流)。
ObjectInputStreamois=newObjectInputStream(newFileInputStream("a.txt"));Usero=(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 自动计算。
publicclassUserimplementsSerializable{// serialVersionUID 序列号// 如果我们自己没有定义,那么虚拟机会根据类中的信息自动的计算出一个序列号。// 问题:如果我们修改了类中的信息,那么虚拟机会再次计算出一个序列号。// 第一步:把User对象序列化到本地. --- -5824992206458892149// 第二步:修改了javabean类. 导致 --- 类中的序列号 4900133124572371851// 第三步:把文件中的对象读到内存. 本地中的序列号和类中的序列号不一致了.// 解决?// 不让虚拟机帮我们自动计算,我们自己手动给出.而且这个值不要变.privatestaticfinallongserialVersionUID=1L;// ...}定义格式:private static final long serialVersionUID = <任意值>;
小技巧:很多 Java 自带类(如ArrayList)也实现了Serializable并手动指定了serialVersionUID,可以直接参考其写法。
2 )transient瞬态关键字
某些成员变量的值不希望被序列化(如密码),可以在属性前加transient关键字。
publicclassUserimplementsSerializable{privatestaticfinallongserialVersionUID=1L;privateStringusername;privatetransientStringpassword;// 不参与序列化// ...}测试:
// 序列化时写入Useruser=newUser("zhangsan","qwer");oos.writeObject(user);// 反序列化读回Usero=(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学生类定义
publicclassStudentimplementsSerializable{privatestaticfinallongserialVersionUID=2L;privateStringname;privateintage;publicStudent(){}publicStudent(Stringname,intage){this.name=name;this.age=age;}// getter / setter / toString ...}写入多个对象
Students1=newStudent("杜子腾",16);Students2=newStudent("张三",23);Students3=newStudent("李四",24);ObjectOutputStreamoos=newObjectOutputStream(newFileOutputStream("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
ObjectInputStreamois=newObjectInputStream(newFileInputStream("a.txt"));while(true){try{Objecto=ois.readObject();System.out.println(o);}catch(EOFExceptione){break;// 到达文件末尾}}ois.close();方式2:利用集合整体序列化
一次写入一个集合对象,读取时也只需读一次,无需处理EOFException。
Students1=newStudent("杜子腾",16);Students2=newStudent("张三",23);Students3=newStudent("李四",24);// 写入集合ObjectOutputStreamoos=newObjectOutputStream(newFileOutputStream("a.txt"));ArrayList<Student>list=newArrayList<>();list.add(s1);list.add(s2);list.add(s3);// 我们往本地文件中写的就是一个集合oos.writeObject(list);oos.close();// 读取集合ObjectInputStreamois=newObjectInputStream(newFileInputStream("a.txt"));ArrayList<Student>list2=(ArrayList<Student>)ois.readObject();for(Studentstudent: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 的运用将更加灵活高效。
