Java基础快速入门: 转换流与对象操作流

本文纲要

  1. 转换流概念

    底层读取机制回顾

    转换流的桥梁作用

    体系结构与API解读

  2. 转换流指定编码读写

    乱码问题成因

    使用InputStreamReader指定码表读取

    使用OutputStreamWriter指定码表写出

    JDK11后字符流直接指定编码

  3. 对象操作流基本特点

    传统写入对象属性的弊端

    对象流整体写入思想

  4. 对象序列化------ObjectOutputStream

    序列化定义

    Serializable接口与标记性接口

    序列化代码示例

  5. 对象反序列化------ObjectInputStream

    反序列化读取对象

    强转与异常处理

  6. 对象操作流的两个注意点

    serialVersionUID序列号不一致问题

    手动指定序列号 & 解决异常

    transient瞬态关键字

  7. 对象操作流练习

    多个对象的序列化与反序列化

    EOFException的处理

    利用集合整体序列化

转换流概念

复习字符流底层读取

字符流底层其实也是字节流,按字节逐个读取数据。

  • 纯英文或数字(如ABC,对应码表值97,98,99):字节流读取97 → 98 → 99。
  • 包含中文(UTF‑8编码,一个中文占3字节,例如-23, -69, -111表示一个汉字):
    • 同样逐字节读取,第一个中文字节的第一个字节是负数;
    • 检测到负数,就知道遇到了中文,会按当前编码一次读取多个字节(GBK读2个,UTF‑8读3个),再将这多个字节转换为字符。

真正在工作的一直是字节流,但上层我们看到的是字符流。转换流就是负责在字节流和字符流之间做转换。
#mermaid-svg-RLzvJ9FsWEJcxvpw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RLzvJ9FsWEJcxvpw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RLzvJ9FsWEJcxvpw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RLzvJ9FsWEJcxvpw .error-icon{fill:#552222;}#mermaid-svg-RLzvJ9FsWEJcxvpw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RLzvJ9FsWEJcxvpw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RLzvJ9FsWEJcxvpw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RLzvJ9FsWEJcxvpw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RLzvJ9FsWEJcxvpw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RLzvJ9FsWEJcxvpw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RLzvJ9FsWEJcxvpw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RLzvJ9FsWEJcxvpw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RLzvJ9FsWEJcxvpw .marker.cross{stroke:#333333;}#mermaid-svg-RLzvJ9FsWEJcxvpw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RLzvJ9FsWEJcxvpw p{margin:0;}#mermaid-svg-RLzvJ9FsWEJcxvpw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-RLzvJ9FsWEJcxvpw .cluster-label text{fill:#333;}#mermaid-svg-RLzvJ9FsWEJcxvpw .cluster-label span{color:#333;}#mermaid-svg-RLzvJ9FsWEJcxvpw .cluster-label span p{background-color:transparent;}#mermaid-svg-RLzvJ9FsWEJcxvpw .label text,#mermaid-svg-RLzvJ9FsWEJcxvpw span{fill:#333;color:#333;}#mermaid-svg-RLzvJ9FsWEJcxvpw .node rect,#mermaid-svg-RLzvJ9FsWEJcxvpw .node circle,#mermaid-svg-RLzvJ9FsWEJcxvpw .node ellipse,#mermaid-svg-RLzvJ9FsWEJcxvpw .node polygon,#mermaid-svg-RLzvJ9FsWEJcxvpw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RLzvJ9FsWEJcxvpw .rough-node .label text,#mermaid-svg-RLzvJ9FsWEJcxvpw .node .label text,#mermaid-svg-RLzvJ9FsWEJcxvpw .image-shape .label,#mermaid-svg-RLzvJ9FsWEJcxvpw .icon-shape .label{text-anchor:middle;}#mermaid-svg-RLzvJ9FsWEJcxvpw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RLzvJ9FsWEJcxvpw .rough-node .label,#mermaid-svg-RLzvJ9FsWEJcxvpw .node .label,#mermaid-svg-RLzvJ9FsWEJcxvpw .image-shape .label,#mermaid-svg-RLzvJ9FsWEJcxvpw .icon-shape .label{text-align:center;}#mermaid-svg-RLzvJ9FsWEJcxvpw .node.clickable{cursor:pointer;}#mermaid-svg-RLzvJ9FsWEJcxvpw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-RLzvJ9FsWEJcxvpw .arrowheadPath{fill:#333333;}#mermaid-svg-RLzvJ9FsWEJcxvpw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-RLzvJ9FsWEJcxvpw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-RLzvJ9FsWEJcxvpw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RLzvJ9FsWEJcxvpw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RLzvJ9FsWEJcxvpw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RLzvJ9FsWEJcxvpw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-RLzvJ9FsWEJcxvpw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-RLzvJ9FsWEJcxvpw .cluster text{fill:#333;}#mermaid-svg-RLzvJ9FsWEJcxvpw .cluster span{color:#333;}#mermaid-svg-RLzvJ9FsWEJcxvpw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RLzvJ9FsWEJcxvpw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RLzvJ9FsWEJcxvpw rect.text{fill:none;stroke-width:0;}#mermaid-svg-RLzvJ9FsWEJcxvpw .icon-shape,#mermaid-svg-RLzvJ9FsWEJcxvpw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RLzvJ9FsWEJcxvpw .icon-shape p,#mermaid-svg-RLzvJ9FsWEJcxvpw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-RLzvJ9FsWEJcxvpw .icon-shape .label rect,#mermaid-svg-RLzvJ9FsWEJcxvpw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RLzvJ9FsWEJcxvpw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RLzvJ9FsWEJcxvpw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RLzvJ9FsWEJcxvpw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 文件 (字节形式)
字节输入流
转换流

InputStreamReader
字符流
内存 (字符形式)
内存 (字符形式)
字符流
转换流

OutputStreamWriter
字节输出流
文件 (字节形式)

  • :字节流 → 转换流 → 字符流(字节 → 字符)
  • :字符流 → 转换流 → 字节流(字符 → 字节)

分类

类型 输入流 输出流
转换流 InputStreamReader OutputStreamWriter
别称 字符输入流(实质是字节→字符) 字符输出流(实质是字符→字节)

命名非常直观:InputStream(字节输入) + Reader(字符) → InputStreamReader

OutputStream(字节输出) + Writer(字符) → OutputStreamWriter

API文档中的描述:

  • InputStreamReader:从字节流到字符流的桥梁,读取字节并使用指定编码将其解码为字符。
  • OutputStreamWriter:从字符流到字节流的桥梁,使用指定编码将写入的字符编码为字节。

底层源码验证

在 Java 中,FileReader 继承自 InputStreamReader,其构造方法内部实际上创建了字节流并传递给父类转换流:

java 复制代码
// FileReader 的构造 
public FileReader(String fileName) throws FileNotFoundException {
    super(new FileInputStream(fileName));
}

可见,字符文件读取依赖的底层就是转换流 + 字节流。

转换流指定编码读写

乱码之源

文件编码与IDE(或程序)编码不一致时会产生乱码。

例如,Windows 记事本默认编码为 GBK ,而 IDEA 默认使用 UTF‑8

直接使用 FileReader 读取 GBK 文件:

java 复制代码
// 方法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 读取

java 复制代码
// 如何解决乱码?
// 文件是什么码表,那么咱们就必须使用什么码表去读取 
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 写出

java 复制代码
    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 之后:字符流直接指定编码

java 复制代码
// 在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 对象,无需再使用转换流。

对象操作流基本特点

场景:将用户对象(用户名、密码)保存到本地文件。

传统方式:用缓冲字符流写入对象的属性值。

java 复制代码
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(对象序列化流)。

序列化步骤

  1. 创建 ObjectOutputStream,包装一个字节输出流(如 FileOutputStream)。
  2. 调用 writeObject(Object obj) 写出对象。
  3. 关闭流。
java 复制代码
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 接口。

java 复制代码
// 如果想要这个类的对象能被序列化,那么这个类必须要实现一个接口 Serializable 
// Serializable 接口的意义:
// 称之为是一个标记性接口,里面没有任何的抽象方法 
// 只要一个类实现了这个Serializable接口,那么就表示这个类的对象可以被序列化 
public class User implements Serializable {
    private String username;
    private String password;
    // 构造 / getter / setter / toString...
}

再次运行序列化代码,成功将对象写入 a.txt。

对象反序列化------ObjectInputStream

将文件中保存的对象读回到内存,称为反序列化。

对应流:ObjectInputStream(对象反序列化流)。

java 复制代码
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 

原因:

  1. 第一次序列化时,JVM 根据类信息(成员变量、方法等)自动计算一个序列号,并写入文件。
  2. 修改类之后,JVM 重新计算序列号,类中序列号与文件中的不一致,导致报错。

解决:手动固定 serialVersionUID,不让 JVM 自动计算。

java 复制代码
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 关键字。

java 复制代码
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String username;
    private transient String password; // 不参与序列化 
    // ...
}

测试:

java 复制代码
// 序列化时写入 
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(默认值)。

对象操作流练习

需求:创建多个学生对象,序列化到文件,再反序列化到内存。

项目代码结构

t 复制代码
otheriomodule/src/com/wb/convertedio/
├── Student.java 
├── User.java 
├── ConvertedDemo1.java 
├── ConvertedDemo2.java 
├── ConvertedDemo3.java 
├── ConvertedDemo4.java 
├── ConvertedDemo5.java 
├── ConvertedDemo6.java 
└── ConvertedDemo7.java 

学生类定义

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 ...
}

写入多个对象

java 复制代码
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 判断结尾):

java 复制代码
// 对象输入流读到结束不会返回null或-1,会抛出EOFException 
/* while((obj = ois.readObject()) != null){
       System.out.println(obj);
   } */

正确方式1:捕获 EOFException

java 复制代码
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

java 复制代码
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 的运用将更加灵活高效。