Java 序列化与反序列化

序列化机制允许将实现序列化的 Java 对象转换成字节序列,保存在磁盘上,或者通过网络传输,以备以后重新恢复成原来的对象。

序列化(Serialize):将一个 Java 对象写入 IO 流中;

反序列化(Deserialize):从 IO 流中恢复该 Java 对象。

在 Java 中,让某个对象支持序列化机制,它的类必须实现如下两个接口之一:

  • Serializable
  • Externalizable

两个对比,Externalizable 在编程上会复杂很多,所以大多数时候都是用 Serializable 进行序列化,这里只介绍 Serializable

使用 Serializable 进行序列化

使用 Serializable 来实现序列化非常简单,主要让目标类实现 Serializable 接口即可,无需实现任何方法。

下面通过具体代码说明:

可序列化的实体类

import java.io.Serializable;

/**
 * 实现 Serializable接口,表明这个类可序列化
 */
public class Person implements Serializable {

    private String name;
    private int age;

    public Person(String name, int age) {
        System.out.println("这是构造器");
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
	// 为了测试输出使用 toString()
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

客户端序列化测试

import java.io.*;

public class SerialTest {
    public static void main(String[] args) {
        try {
            // 创建 ObjectOutputStream 输出流,用于序列化对象
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
            //创建一个对象
            Person person = new Person("诸葛亮", 32);
            //将对象写入输出流
            oos.writeObject(person);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

运行客户端程序后,可以在 object.txt 中看到 person 对象的信息(当然,是乱码的形式)。

ObjectOutputStream 输出流和 ObjectInputStream 输入流是用于对象序列化的一个处理流,建立在一个文件节点流之上,具体可以去看 API 。

反序列化操作

import java.io.*;

public class SerialTest {
    public static void main(String[] args) {
        try {
            // 创建 ObjectInputStream 输入流
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
	    // 从输入流读取一个 Java 对象,并强转为 Person 对象
            Person person = (Person) ois.readObject();
            System.out.println(person);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}

在控制到输出

Person{name='诸葛亮', age=32}

这就完成了一个反序列化过程。这个过程有几个重点要注意的地方:

  1. 反序列化读取的仅仅的 Java 对象的数据,而不是 Java 类,所以反序列化时必须提供 Java 对象所属类的 class 文件,否则就会有 ClassNotFoundException 异常。
  2. 反序列化时,并没有看到控制台打印构造器里的输出语句,这表明反序列化机制无须用过构造器来初始化 Java 对象
  3. 当一个可序列化类有多个父类时(直接父类或间接父类),这些父类要么有无参构造器,要么是可序列化的----否则反序列化时会抛异常。如果父类只是带无参构造器,并不是可序列化的,则该父类中定义的成员变量值不会序列化到二进制流中。
  4. 如果使用序列化机制向同一个文件中写入多个 Java 对象,使用反序列化时必须按对象实际写入的顺序读取

对象引用的序列化

前面使用的成员变量分别是 String 和 int 类型,如果一个类的成员变量类型不是基本数据类型或 String 类型,而是另一个引用类型,那么这个引用类必须是可序列化的,否则拥有该类型成员变量的类也是不可序列化的

public class Teacher implements Serializable {

    private String name;
    // 要求 Person 是可序列化的
    private Person student;

    public Teacher(String name, Person student) {
        this.name = name;
        this.student = student;
    }
    //以下省略 setter 和 getter 方法
    ...
}

【提示】

因为 Teacher 对象持有一个 Person 对象的引用,在序列化 Teacher 对象时程序会顺带将该 Person 对象也进行序列化,所以要求 Person 类必须是可序列化的,否则 Teacher 类序列化会失效。

序列化多个对象

import java.io.*;

public class SerialTest {
    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
            
            Person per = new Person("孙悟空", 500);
            Teacher t1 = new Teacher("唐三藏", per);
            Teacher t2 = new Teacher("菩提祖师", per);
            //依次写入4个对象,其中 t1 写入两次
            oos.writeObject(per);
            oos.writeObject(t1);
            oos.writeObject(t2);
            oos.writeObject(t1);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

上面依次序列化了 4 个对象,其中 t1 重复写入,后面比较反序列化的对象是否为同一个。

反序列化

import java.io.*;

public class SerialTest {
    public static void main(String[] args) {
        try {
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
            
            //依次从输入流中取出 4个 对象
            Person person = (Person) ois.readObject();
            Teacher t1 = (Teacher) ois.readObject();
            Teacher t2 = (Teacher) ois.readObject();
            Teacher t3 = (Teacher) ois.readObject();
            
            System.out.println(person);		// 输出 Person{name='孙悟空', age=500}
            System.out.println(t1.getStudent() == person);	// 输出 true
            System.out.println(t2.getStudent() == person);	// 输出 true
            System.out.println(t2 == t1);	// 输出 false
            System.out.println(t3 == t1);	// 输出 true
            
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

在上面代码中,t1,t2,t3 的类都是 Teacher,通过比较 t1 和 t2 不是同一对象,但 t1 和 t3 是同一个对象,这是为什么呢?

首先我们要知道这几个对象在 JVM 上的内存分配是怎么样的,如图所示:

在序列化对象的时候,Java 采用了一种特殊的序列化算法,大致如下。

  • 所有保存到磁盘中的对象都有一个序列化编号。
  • 当程序视图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象(在本次虚拟机中)从未被序列化过,系统才会将该对象转换成字节序列并输出。
  • 如果某个对象已经序列化过,程序将会直接输出一个序列化编号,而不是再次重新序列化该对象。

所以在上面代码中,实际上只序列化了三个对象。t1 对象已经被序列化过,再次执行序列化时直接输出它的序列化编号,所以在磁盘当中其实只存在一个,所以在判断 t3 == t1 时输出为 true

.

但这样就引起了一个潜在的问题 ---- 当序列化一个可变对象时,在序列化一次对象之后,去改变这个对象的值再序列化一次,程序只是输出前面的序列化编号,后面改变的值并没有被序列化保存。

import java.io.*;

public class SerialTest {
    public static void main(String[] args) {
        try {
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
	    //创建对象,并序列化
            Person person = new Person("诸葛亮", 35);
            oos.writeObject(person);
	    //更改这个对象的值,再次序列化
            person.setName("娜可露露");
            oos.writeObject(person);

            Person p1 = (Person) ois.readObject();
            Person p2 = (Person) ois.readObject();
	    // 输出 true,即反序列化后 p1 等于 p2
            System.out.println(p1 == p2);
            // 输出 “诸葛亮”,即更改后的实例变量并没有被序列化
            System.out.println(p2.getName());       
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

自定义序列化

在一些特殊的场景,如果一个类中的某些变量是敏感信息,例如银行账号信息等,这时不希望系统将该实例变量值序列化;或者某个变量的引用类型是不可序列化的,又不想在序列化时抛异常。

可以在实例变量前使用 transient 关键字修饰,可以指定 Java 序列化时无须理会该实例变量。

例如还是一样的 Teacher 类:

public class Teacher implements Serializable {

    private String name;
    // 在实例化 Teacher 实例时,忽略这个变量;所以即使 Person 不可序列化也没关系
    private transient Person student;

    public Teacher(String name, Person student) {
        this.name = name;
        this.student = student;
    }
    //以下省略 setter 和 getter 方法
    ...
}

【提示】

transient 关键字只能用于修饰实例变量,不可修饰 Java 程序中的其他成分。

.

版本号

前面说过,反序列化 Java 对象时必须提供该对象的 class 文件,现在的问题是,随着项目的升级,系统的 class 文件也会升级,Java 如何保证两个 class 文件的兼容性?

没错,就是版本号!

Java 序列化机制中,提供有一个 private static final 的 serialVersionUID 变量值, 该变量值用于标识该Java 类的序列化版本号。也就是说,如果一个类升级后,只要它的 serialVersionUID 变量值保持不变,序列化机制就会把它们当成同一个序列化版本。

使用很简单:

public class Person implements Serializable {
    // 为该类指定一个版本号
    private static final long serialVersionUID = 125L;
    ...
}

如果不显式定义 serialVersionUID 类变量的值,该类变量的值也将由 JVM 根据类的相关信息计算得出,但修改后的类每次自动分配的结果往往不同,从而造成对象反序列化因为类版本不兼容而失败。

还有一个缺点就是,不利于不同 JVM 之间的移植,因为不同的 JVM 计算的策略可能不同。

总的来说,就是每次我们要使用序列化时,都显示定义版本号就行了。

.

如果类的修改确实会导致反序列化失败,则应该为该类的 serialVersionUID 类变量重新分配值。

那对类的哪些修改可能会导致反序列化失败呢?分几种情况讨论。

  • 如果修改类时仅仅修改了方法,反序列化不受影响
  • 如果修改类时仅仅修改了静态变量或瞬态实例变量(瞬态变量就是被 transient 修饰的变量),反序列化也不受影响
  • 如果修改类时修改了非瞬态的实例变量,则可能导致序列化版本不兼容,这时应该更新 serialVersionUID 类变量的值。
  • 如果只是新增或者删除类中的实例变量,序列化版本可以兼容,这时可以不更新 serialVersionUID 类变量的值;但是反序列化得到的新增的实例变量值都是 null 或 0 。

posted @ 2021-09-14 17:47  乐子不痞  阅读(235)  评论(0编辑  收藏  举报
回到顶部