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字节输出流文件 (字节形式)读字节流 → 转换流 → 字符流字节 → 字符写字符流 → 转换流 → 字节流字符 → 字节分类类型输入流输出流转换流InputStreamReaderOutputStreamWriter别称字符输入流实质是字节→字符字符输出流实质是字符→字节命名非常直观InputStream字节输入 Reader字符 →InputStreamReaderOutputStream字节输出 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{FileReaderfrnewFileReader(C:\\Users\\apple\\Desktop\\a.txt);intch;while((chfr.read())!-1){System.out.println((char)ch);}fr.close();}解决思路 文件是什么编码就用什么编码去读。JDK11 之前使用转换流指定编码使用InputStreamReader指定GBK读取// 如何解决乱码// 文件是什么码表那么咱们就必须使用什么码表去读取privatestaticvoidmethod2()throwsIOException{// 指定使用GBK码表去读取文件InputStreamReaderisrnewInputStreamReader(newFileInputStream(C:\\Users\\apple\\Desktop\\a.txt),GBK);intch;while((chisr.read())!-1){System.out.println((char)ch);}isr.close();}使用OutputStreamWriter指定UTF‑8写出OutputStreamWriteroswnewOutputStreamWriter(newFileOutputStream(C:\\Users\\apple\\Desktop\\b.txt),UTF-8);osw.write(我爱学习,谁也别打扰我);osw.close();注意用 IDEA 以 UTF‑8 写出的文件Windows 记事本打开时也能正确显示因为它会自动识别编码若另存为 ANSIGBK字节数会变化。JDK11 之后字符流直接指定编码// 在JDK11之后,字符流新推出了一个构造,也可以指定编码表FileReaderfrnewFileReader(C:\\Users\\apple\\Desktop\\a.txt,Charset.forName(gbk));intch;while((chfr.read())!-1){System.out.println((char)ch);}fr.close();FileReader新增的两参数构造直接接受Charset对象无需再使用转换流。对象操作流基本特点场景将用户对象用户名、密码保存到本地文件。传统方式用缓冲字符流写入对象的属性值。UserusernewUser(zhangsan,qwer);BufferedWriterbwnewBufferedWriter(newFileWriter(a.txt));bw.write(user.getUsername());bw.newLine();bw.write(user.getPassword());bw.close();缺陷任何人打开 a.txt 都能直接看到用户名和密码数据不安全。对象操作流思想不以属性值为单位写入而是将整个对象以字节形式写入到文件。再次打开文件看到的是乱码只有用对象输入流再读回内存才能还原对象。对象序列化——ObjectOutputStream将对象以字节形式写到本地文件或网络传输称为序列化。对应流ObjectOutputStream对象序列化流。序列化步骤创建ObjectOutputStream包装一个字节输出流如FileOutputStream。调用writeObject(Object obj)写出对象。关闭流。UserusernewUser(zhangsan,qwer);ObjectOutputStreamoosnewObjectOutputStream(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对象反序列化流。ObjectInputStreamoisnewObjectInputStream(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// 第三步:把文件中的对象读到内存. 本地中的序列号和类中的序列号不一致了.// 解决?// 不让虚拟机帮我们自动计算,我们自己手动给出.而且这个值不要变.privatestaticfinallongserialVersionUID1L;// ...}定义格式private static final long serialVersionUID 任意值;小技巧很多 Java 自带类如ArrayList也实现了Serializable并手动指定了serialVersionUID可以直接参考其写法。2 )transient瞬态关键字某些成员变量的值不希望被序列化如密码可以在属性前加transient关键字。publicclassUserimplementsSerializable{privatestaticfinallongserialVersionUID1L;privateStringusername;privatetransientStringpassword;// 不参与序列化// ...}测试// 序列化时写入UserusernewUser(zhangsan,qwer);oos.writeObject(user);// 反序列化读回Usero(User)ois.readObject();System.out.println(o);// User{usernamezhangsan, passwordnull}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{privatestaticfinallongserialVersionUID2L;privateStringname;privateintage;publicStudent(){}publicStudent(Stringname,intage){this.namename;this.ageage;}// getter / setter / toString ...}写入多个对象Students1newStudent(杜子腾,16);Students2newStudent(张三,23);Students3newStudent(李四,24);ObjectOutputStreamoosnewObjectOutputStream(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捕获EOFExceptionObjectInputStreamoisnewObjectInputStream(newFileInputStream(a.txt));while(true){try{Objectoois.readObject();System.out.println(o);}catch(EOFExceptione){break;// 到达文件末尾}}ois.close();方式2利用集合整体序列化一次写入一个集合对象读取时也只需读一次无需处理EOFException。Students1newStudent(杜子腾,16);Students2newStudent(张三,23);Students3newStudent(李四,24);// 写入集合ObjectOutputStreamoosnewObjectOutputStream(newFileOutputStream(a.txt));ArrayListStudentlistnewArrayList();list.add(s1);list.add(s2);list.add(s3);// 我们往本地文件中写的就是一个集合oos.writeObject(list);oos.close();// 读取集合ObjectInputStreamoisnewObjectInputStream(newFileInputStream(a.txt));ArrayListStudentlist2(ArrayListStudent)ois.readObject();for(Studentstudent:list2){System.out.println(student);}ois.close();这种方式代码更简洁推荐使用总结知识点关键类/接口要点转换流InputStreamReader,OutputStreamWriter字节与字符流的桥梁可指定编码读写JDK11后的简化FileReader,FileWriter构造方法可直接传入Charset无需显式使用转换流对象序列化ObjectOutputStream实现Serializable接口writeObject写出整体对象对象反序列化ObjectInputStreamreadObject 读取并强转注意ClassNotFoundException序列号serialVersionUIDprivate static final long防止类修改后反序列化失败需手动指定transient关键字transient修饰的字段不参与序列化用于敏感信息如密码多对象的处理集合 序列化将多个对象放入集合一次性序列化集合避免处理EOFException转换流打通了字节流与字符流的隔阂对象操作流则为持久化对象提供了直接且安全的方案。掌握这些知识Java I/O 的运用将更加灵活高效。