【Java I/O 流】4 - 7 序列化与反序列化

§4-7 序列化与反序列化

4-7.1 字节流的序列化与反序列化流

序列化流与反序列化流是字节流的高级流,其体系如下如所示:

image

序列化和反序列化是针对 Java 对象而言的。序列化允许将 Java 对象的数据写到本地文件中,而反序列化则允许将本地文件中的 Java 对象数据还原。

4-7.2 序列化:对象输出流

ObjectOutputStream 可以把 Java 对象写到本地文件中。

构造方法

构造方法 描述
ObjectOutputStream(OutputStream out) 创建一条关联指定输出流的对象输出流
  • 序列化流属于高级流,该构造方法要求传入一个基本流对象;

    为了效率,一般考虑在参数中包装基本流(new);

  • 关闭流释放资源时,只需要关闭高级流即可,方法内部会自行关闭所依赖的基本流;

序列化方法

方法 描述
void write(byte[] buf) 写出一个字节数组
void write(byte[] buf, int off, int len) 写出一个字节子数组
void write(int val) 写出一个字节
void writeBoolean(boolean val) 写出一个布尔值
void writeByte(int val) 写出一个 8 位字节
void writeBytes(String str) 将字符串作为字节序列写出
void writeChar(int val) 写出一个 16 位字符
void writeChars(String str) 将字符串作为字符序列写出
void writeDouble(double val) 写出一个 64 位双精度浮点数
void writeFloat(double val) 写出一个 32 位单精度浮点数
void writeInt(int val) 写出一个 32 位整型
void writeLong(long val) 写出一个 64 位长整型
void writeShort(int val) 写出一个 16 位短整型
void writeObject(Object obj) 将指定对象写到流上
  • 序列化也支持基本数据类型,但建议使用基于对象的序列化;

  • 序列化后的数据,对于人类不可读,或难以阅读;

  • 若要序列化 Java 对象,该对象的类必须实现 Serializable 接口,序列化会自动进行;

    该接口是一个标志性接口,其中没有任何方法;实现此接口的类允许被序列化,否则将会抛出异常 NotSerializableException

  • 若要指定 Java Bean 中哪些成员变量不被序列化,可在该变量前用瞬态关键字 transient 修饰即可(仅 Serializable 实现类);

    另外,调用 writeObject 时,静态成员也无法被序列化;

    序列化引用成员,该引用成员的类必须满足可序列化要求,即实现 Serializable 接口;

  • 类也可以选择实现 Externalizable 接口实现序列化,该接口继承了 Serializable,用于手动选择序列化的进行;

    该接口有两个无返回值的抽象方法 readExternal(ObjectInput in)writeExternal(ObjectOut out),必须重载这两个方法以指定序列化与反序列化的成员;使用该接口可以更灵活地指定需要序列化的对象和序列化顺序;

    实现了 Externalizable 的类需要进行序列化与反序列化时,通过实例调用其重载的 writeExternalreadExternal 方法即可,重载方法的实现应当避免使用 writeObjectreadObject,而是使用其他方法选择序列化与反序列化的成员(如 writeInt, readInt 等);

    必须保证序列化与反序列化的数据顺序相同,否则抛出异常;

    为保证反序列化能够调用成员进行,建议为类提供一个无参构造;

  • 序列化得到的文件不能修改,一旦修改,则无法反序列化;

4-7.3 反序列化:对象输入流

ObjectInputStream 可以将由 ObjectOutputStream 序列化的 Java 对象还原为 Java 对象。

构造方法

构造方法 描述
ObjectInputStream(InputStream in) 创建一条关联指定输入流的对象输入流
  • 序列化流属于高级流,该构造方法要求传入一个基本流对象;

    为了效率,一般考虑在参数中包装基本流(new);

  • 关闭流释放资源时,只需要关闭高级流即可,方法内部会自行关闭所依赖的基本流;

反序列化方法

方法 描述
int read() 读取一个字节
int read(byte[] buf, int off, int len) 将数据读取到字节数组中
boolean readBoolean() 读取一个布尔值
byte readByte() 读取一个 8 位字节
char readChar() 读取一个 16 位字符
double readDouble() 读取一个 64 位双精度浮点数
float readFloat() 读取一个 32 位单精度浮点数
int readInt() 读取一个 32 位整型
long readLong() 读取一个 64 位长整型
short readShort() 读取一个 16 位短整型
Object readObject() 读取一个来自对象输出流的对象
  • 反序列化也支持基本数据类型,但建议使用基于对象的反序列化;

  • 读取对象可能会抛出编译时异常 ClassNotFoundException,且该方法读到文件末尾处再次调用会抛出异常 EOFException

  • 序列化与反序列化时,都会根据对应 Java Bean 类的变量和方法计算该类的序列版本 UID(serialVersionUID);

    若在反序列化时发现计算得出的版本号与序列化时的版本号不一致,则会抛出异常 InvalidClassException

    为解决此问题,只需要在 Java Bean 中使用常量固定其版本号即可,固定方法只能是:

    @java.io.Serial
    private static final long serialVersionUID = XXXXXXL;
    

    其中,@java.io.Serial 是注解。版本号一旦固定,则不应再更改。版本号可由类的属性和方法计算得出,这一步骤可用 IDEA 自动生成;

    IDEA 自动警告未指定序列版本的设置方法:在设置中搜索 Serial,勾选 没有 serialVersionUID 的可序列化非 static 内部类不带 serialVersionUID 的可序列化类 选项;

    实现了 Externalizable 接口的类也应当指定版本号;

4-7.4 案例演示

需求:序列化多个自定义对象(Student,但个数不确定),并反序列化后输出。

可以让 Java Bean 实现 Serializable 后,将多个对象放到集合中,将集合序列化即可。

import java.io.*;
import java.util.ArrayList;
import java.util.Random;

public class Serialize {
    public static void main(String[] args) {
        String[] names = {"张三", "李四", "王五", "赵六", "陈七", "钱八", "秦九", "马十"};
        String[] addresses = {"广州", "上海", "深圳", "北京", "重庆", "杭州", "南京", "厦门"};
        ArrayList<Student> list = new ArrayList<>();
        Random rnd = new Random();

        //序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("JavaSE\\aaa\\objout.txt"));
        int loops = rnd.nextInt(10) + 1;
        System.out.println("循环次数:" + loops);
        for (int i = 0; i < loops; i++) {
            Student student = new Student(names[rnd.nextInt(names.length)]
                                          , rnd.nextInt(31)
                                          , addresses[rnd.nextInt(addresses.length)]);
            list.add(student);
        }
        oos.writeObject(list);  //将集合写入即可
        oos.close();
    }
}

反序列化时,也直接反序列化集合即可。

public class Deserialize {
    public static void main(String[] args) {
        //反序列化流
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("JavaSE\\aaa\\objout.txt"));
        ArrayList<Student> res = (ArrayList<Student>) ois.readObject();
        ois.close();
        System.out.println(res);
    }
}

4-7.5 ExternalizableSerializable 接口用法

两个接口都可以用于序列化与反序列化,先重复一遍二者的共同点:

  • 实现类都可以被序列化;
  • 实现类都应当固定版本号(serialVersionUID)以防抛出类无效异常;

而它们的不同点在于:

  • Serializable 是标记类接口,无抽象方法,序列化与反序列化自动进行;

    通过调用对象输入输出流中的 writeObjectreadObject 完成序列化与反序列化;

  • ExternalizableSerializable 的子接口,其中有两个抽象方法:

    void writeExternal(ObjectOut out);	//写出
    void readExternal(ObjectIn in);		//读取
    

    实现了该接口的方法应当重载这两个方法,并通过成员调用这两个方法完成对象的序列化与反序列化;

    读写要配套,读写数据顺序必须保持一致,且建议提供一个无参构造器;

  • Serializable 的实现类在序列化时会跳过由 transientstatic 修饰的成员;

  • Externalizable 的实现类在序列化时更加灵活,是否序列化完全取决于方法的实现,而非成员变量的修饰符;

示例一Serializable 自动序列化

Serializable 的实现类:

package com.io.serial;

import java.io.*;

public class Student implements Serializable {
    //固定版本号,防止 JavaBean 修改后重新计算版本号导致版本号不匹配
    @Serial
    private static final long serialVersionUID = -7214721743963933347L;
    private String name;
    private int age;
    private transient String address;   //瞬态关键字,不进行序列化

    //构造器、Getters & Setters等方法省略
}

在测试类中:

import com.io.serial.Student;
import java.io.*;

public class SerialDemo {
    public static void main(String[] args) {
        Student std1 = new Student("张三", 23, "广州");
        Student std2 = new Student("李四", 24, "深圳");
        
        //序列化
        ObjectOutputStream oos = new ObjectOutputStream(
            new FileOutputStream("output\\serial.txt"));
        oos.writeObject(std1);
        oos.writeObject(std2);
        oos.close;
        
        //反序列化
        ObjectInputStream ois = new ObjectInputStream(
            new FileInputStream("output\\serial.txt"));
        Object o1 = ois.readObject();
        Object o2 = ois.readObject();
        ois.close();
        
        //输出反序列化结果
        System.out.println(o1);
        System.out.println(o2);
    }
}

输出结果为:

张三(23, null)
李四(24, null)

静态变量与瞬态变量并未序列化到文件中。

示例二Externalizable 手动序列化

Externalizable 实现类:

package com.io.serial;

import java.io.*;

public class Teacher implements Externalizable {
    @Serial
    private static final long serialVersionUID = 888902281129162579L;
    private String name;
    private int age;
    private transient String address;
    
    //构造器、Getters & Setters等方法省略

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(this.name);
        out.writeInt(this.age);
        out.writeUTF(this.address);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.name = in.readUTF();
        this.age = in.readInt();
        this.address = in.readUTF();	//读写数据的顺序要保证相同
    }
}

在测试类中:

import java.io.*;

public class Externalize {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //Externalize 接口测试
        Teacher tcr1 = new Teacher("张三", 23, "广州");
        Teacher tcr2 = new Teacher("李四", 24, "深圳");
        Teacher rec1 = new Teacher();
        Teacher rec2 = new Teacher();

        //序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("JavaSE\\aaa\\externalize.txt"));
        tcr1.writeExternal(oos);
        tcr2.writeExternal(oos);
        oos.close();

        //反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("JavaSE\\aaa\\externalize.txt"));
        rec1.readExternal(ois);
        rec2.readExternal(ois);
        ois.close();

        //输出反序列化结果
        System.out.println(rec1);
        System.out.println(rec2);
    }
}

输出结果:

张三(23, 广州)
李四(24, 深圳)

Externalizable 的灵活度更高,可手动决定哪些变量可以序列化。

posted @ 2023-08-20 23:06  Zebt  阅读(61)  评论(0)    收藏  举报