MENU

序列化与反序列化(二)深入序列化

July 1, 2018 • Code

前言:在之前的 序列化与反序列(一)序列化初窥 文章中对序列化概念、使用方式等做了一个大概介绍。本文进一步深入学习有关序列化的知识。

本文涉及序列化的知识点

主要涉及

  • serialVersionUID
  • transient
  • 静态变量与序列化

serialVersionUID

serialVersionUID 是在被序列化类中的一个属性,主要作用是在进行反序列时进行版本的兼容性控制

在之前 序列化与反序列(一)序列化初窥 文章中的栗子也已经用到:


当 JVM 在进行反序列时会先进行 serialVersionUID 比对:将序列化文件或流中的 serialVersionUID 与本地相应实体类中的 serialVersionUID 进行比对,如果不一致则无法进行反序列化。因此如果要禁止之前序列化的文件被反序化成新修改过的实体类,只需修改实体类中的 serialVersionUID 即可。

默认值为 1L。

实际上,serialVersionUID 属性并不是可序列的必要条件。如果实体类中没有显式定义 serialVersionUID 属性,Java 序列化机制会根据类的内容自动生成一个 serialVersionUID 作序列化版本比较用,任何对类的改动都会改变该值。在这种情况下,改动类必定会导致反序列化失败,因为生成的 serialVersionUID 不同

因此在序列类中最好显式定义 serialVersionUID,这样有利于控制序列化版本兼容性。同时如果不显式定义,serialVersionUID 由 JVM 自动计算,不同 JVM 的计算方式可能存在差异,也会导致序列化失败,降低可移植性。

Intellij IDEA 的生成 serialVersionUID

IDEA 默认不会为实现 Serializable 接口的实体类自动生成 serialVersionUID 属性,可通过下面方法设置。

  • 设置提示

  • 光标置于类名上,在 Mac 上按 Option ⌥ + Enter (Win 平台上是 Alt + Enter)显示提示,最终生成字段。

transient

一个类如果实现了 Serializable 接口,那么对象实例中所有的状态变量都会被序列化,而在实际中,需序列化的实体类中可能存在着敏感属性(如密码等),此时有两种做法:

  • 对序列化后的结果进行加密。
  • 不序列化敏感属性信息。

transient 关键字就可以实现第二种做法,被 transient 修饰的属性信息在序列化过程中被替换为 0null 存储。换句话说,这些属性的状态只存在于当前 JVM 进程生命周期中,因此是「短暂的」。

transient 仅可以修饰变量,不可修饰方法与类。

栗子

  • 实体类 Student,其中 stuName 属性为 transient。
/**
 * @author kbrx93
 */
public class Student implements Serializable {

    private long stuId;
    transient private String stuName;

    public Student(long stuId, String stuName) {
        this.stuId = stuId;
        this.stuName = stuName;
    }

    // getter and setter
    ...
    ...
    
    @Override
    public String toString() {
        return "Student{" +
                "stuId=" + stuId +
                ", stuName='" + stuName + '\'' +
                '}';
    }
}

  • 执行序列化及反序列的类 TranDemo
/**
 * @author kbrx93
 */
public class TranDemo {

    public static void main(String[] args) {

        // 1. 实例化对象并设置属性
        Student student = new Student(2015051593L, "kbrx93");

        // 2. 实例化对象输出流,并将 student 对象序列化到文件中
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.ser"))) {
            oos.writeObject(student);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 3. 实例化对象输入流
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.ser"))) {
            // 4. 从文件中读取对象状态信息并构造对象
            Student student2 =(Student) ois.readObject();
            System.out.println(student2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 结果


由结果可见:被 transient 修改的属性 stuName 在反序列出来为 null,说明在序列化时并没有进行存储。

transient 与 Externalizable

transient 关键字阻止属性序列化的作用仅限于 Serializable 接口,如果是 Externalizable 接口,则无作用。

栗子
  • 实体类 Student,其中 stuName 属性为 transient
public class Student implements Externalizable {

    private long stuId;
    transient private String stuName;
    private String gender;

    public Student(long stuId, String stuName, String gender) {
        this.stuId = stuId;
        this.stuName = stuName;
        this.gender = gender;
    }

    // 必须要有无参构造器
    public Student() {
    }
    
    // getter and setter
    ...
    ...

    @Override
    public String toString() {
        return "Student{" +
                "stuId=" + stuId +
                ", stuName='" + stuName + '\'' +
                ", age=" + gender +
                '}';
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(stuName);
        out.writeObject(gender);
        out.writeLong(stuId);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.stuName = (String) in.readObject();
        this.gender = (String) in.readObject();
        this.stuId = in.readLong();
    }
}

Externalizable 实现时反序列化时,对应的类必须要有无参构造器,因为本质上反序列化就是构造对象的过程。同时在 writeExternalreadExternal 方法中属性的顺序需一一对应。

  • 执行序列化及反序列的类 TranDemo 同上,只需修改 Student 类的构造参数即可。

  • 结果


结果显示被 transient 修饰的属性 stuName 还是被序列化了,Externalizable 接口的序列与 transient 无关

静态变量与序列化

序列化操作的是对象的状态,而静态变量本质上属于类的状态,因此静态变量是无法被序列化的

栗子

  • 实体类 Student,其中 stuName 属性为 static
public class Student implements Serializable {

    private static final long serialVersionUID = 3341068285506875279L;
    static String stuName;

    @Override
    public String toString() {
        return stuName;
    }
}
  • 执行序列化及反序列的类 TranDemo
public class TranDemo {

    public static void main(String[] args) {

        /**
         * 1. 静态变量本质上是属于类的,通过下面的初始化顺序也可以看出
         * stuName 属性与 student 对象没有直接关系。
         */
        Student.stuName = "kbrx93";
        Student student = new Student();

        // 2. 实例化对象输出流,并将 student 对象序列化到文件中
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.ser"))) {
            oos.writeObject(student);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 3. 实例化对象输入流
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.ser"))) {
            // 4. 从文件中读取对象状态信息并构造对象
            Student student2 =(Student) ois.readObject();
            System.out.println(student2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 结果:实际运行后会发现,反序化时依旧输出了结果,这并不是因为静态变量被序列化了,输出的是当前 JVM 中 stuName 的值。只需在第 2 步和第 3 步中间改变 stuName 的值,如增加 Student.stuName = "39xrbk" 语句即可证明这一结论。

  • 改变值后的结果

参考

Last Modified: July 5, 2018
Archives QR Code
QR Code for this page
Tipping QR Code