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个),再将这多个字节转换为字符。

真正在工作的一直是字节流,但上层我们看到的是字符流。转换流就是负责在字节流和字符流之间做转换。

文件 (字节形式)

字节输入流

转换流
InputStreamReader

字符流

内存 (字符形式)

内存 (字符形式)

字符流

转换流
OutputStreamWriter

字节输出流

文件 (字节形式)

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

分类

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

命名非常直观: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(对象序列化流)。

序列化步骤

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

原因:

  1. 第一次序列化时,JVM 根据类信息(成员变量、方法等)自动计算一个序列号,并写入文件。
  2. 修改类之后,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 写出整体对象
对象反序列化ObjectInputStreamreadObject 读取并强转,注意 ClassNotFoundException
序列号 serialVersionUIDprivate static final long防止类修改后反序列化失败,需手动指定
transient 关键字transient修饰的字段不参与序列化,用于敏感信息如密码
多对象的处理集合 + 序列化将多个对象放入集合,一次性序列化集合,避免处理 EOFException

转换流打通了字节流与字符流的隔阂,对象操作流则为持久化对象提供了直接且安全的方案。掌握这些知识,Java I/O 的运用将更加灵活高效。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值