zoukankan      html  css  js  c++  java
  • 序列化与serialVersionUID

    序列化与serialVersionUID


    存储一个对象时,对象所属类的描述信息也必须存储。类的描述信息包括:

    1. 类名
    2. 序列化版本ID(serialVersionUID)
    3. 描述序列化方法的标志集
    4. 对数据域的描述

    serialVersionUID相当于类的“指纹”。serialVersionUID是通过对类、超类、接口、域类型和方法签名按照规范方式排序,然后将安全散列算法(SHA)应用于这些数据而获得的。

    SHA是一种可以为较大的信息块提供“指纹”的高效算法,无论数据库尺寸有多大,生成的“指纹”总之20个字节的数据包。它是通过在数据上执行一个灵巧的位操作序列而创建的,这个序列在本质上可以保证无论这些数据以何种方式发生改变,其指纹100%会跟着发生改变。序列化机制只使用了SHA码的前8个字节作为类的“指纹”,即便如此,当类的数据域或方法发生变化时,其“指纹”跟着发生改变的可能性还是非常大。

    在反序列化一个对象时,会拿保存的类指纹与类当前的指纹进行比对,如果它们不匹配,说明这个类的定义在该对象被序列化以后发生过改变,因此会产生一个异常。


    重现异常

    有Employee类:

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Employee implements Serializable {
        private String name;
        private Integer age;
        private char sex;
        private Double salary;
    }
    

    执行下面的代码:

    public static void main(String[] args) throws Exception {
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("employee.dat"));
        Employee employee = new Employee("xzy", 22, 'm', 100000.0);
        outputStream.writeObject(employee);
        
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("employee.dat"));
        Employee employee1 = (Employee) inputStream.readObject();
    }
    

    代码顺利执行完成,控制台打印出如下信息:

    Employee(name=xzy, age=22, sex=m, salary=100000.0)
    

    若先将对象存储:

    ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("employee.dat"));
    Employee employee = new Employee("xzy", 22, 'm', 100000.0);
    outputStream.writeObject(employee);
    

    然后对Employee类进行略微的修改:将成员变量salary的名字改为“salary_”。

    private Double salary_;//salary → lalary_
    

    最后尝试反序列化对象:

    ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("employee.dat"));
    Employee employee1 =  (Employee) inputStream.readObject();
    System.out.println(employee1);
    

    上述代码在执行过程中抛出异常,异常信息如下所示:

    Exception in thread "main" java.io.InvalidClassException: com.learn.java.extend.Employee; local class incompatible: stream classdesc serialVersionUID = -7427550135122105667, local class serialVersionUID = 5815872246558374312

    从异常信息可以看到,对象序列化的时候,Employee类的指纹为-7427550135122105667,反序列化时,Employee类的指纹已经变为了5815872246558374312,二者不匹配,因此抛出异常。


    解决异常

    类的修改很难避免,但类可以表明自己对早期版本保持兼容。

    上述代码运行产生的异常可以这样解决:

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Employee implements Serializable {
        public static final long serialVersionUID = -7427550135122105667L;
        
        private String name;
        private Integer age;
        private char sex;
        private Double salary_;//salary → lalary_
    }
    

    可以看到,Employee类中添加了一个名为serialVersionUID的静态成员变量。如果你观察的再仔细一点还能发现,该变量保存的值就是上文异常信息中,对象序列化时Employee类的“指纹”。先运行一下代码,看看异常解决没有:

    控制台输出信息:

    Employee(name=xzy, age=22, sex=m, salary_=null)
    

    从结果来看,问题确实已经解决了,至少不再抛出异常了。


    初步理解

    “如果一个类具有名为serialVersionUID的静态数据成员,它就不在需要计算指纹,只需直接使用这个值。一旦这个静态数据成员被置于某个类的内部,那么序列化系统就可以读入这个类的不同版本的对象。” ——《Java核心技术》

    上面这段话,我试着理解了一下:如果类中具有名为serialVersionUID的静态成员变量,类就不需要使用SHA计算“指纹”,而是直接将这个值作为指纹。因此,无论类发生怎样的修改,只要serialVersionUID不改变,类的“指纹”就不改变,所以对任意版本的对象进行反序列都可以。

    我将尝试用下面3个测试验证一下我的理解:

    1. 为Employee类添加serialVersionUID,序列化一个对象,修改Employee类,反序列化该对象。
    
        预期结果:反序列化成功。因为serialVersionUID没有改变。
    
    2. 为Employee类添加serialVersionUID,序列化一个对象,修改serialVersionUID,反序列化该对象。
    
        预期结果:反序列化失败。因为serialVersionUID发生改变。
    
    3. 为Employee类添加serialVersionUID,序列化一个对象,为其他类添加相同的serialVersionUID,将对象反序列化为其他对象。
    
        预期结果:类型转换错误。
    

    Employee类:

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Employee implements Serializable {
        private static final long serialVersionUID = 5815872246558374312L;
        private String name;
        private Integer age;
        private char sex;
        private Double salary;
    }
    

    1. 为Employee类添加serialVersionUID,序列化一个对象,修改Employee类,反序列化该对象。

    序列化对象:

    ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("employee.dat"));
    Employee employee = new Employee("xzy", 22, 'm', 100000.0);
    outputStream.writeObject(employee);
    

    修改Employee类:

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Employee implements Serializable {
        private static final long serialVersionUID = 5815872246558374312L;
        private String name;
        private Integer age;
        private char sex;
        private Double salary;
        private String address;//新添加
    }
    

    反序列化该对象:

    ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("employee.dat"));
    Employee employee1 = (Employee) inputStream.readObject();
    System.out.println(employee1);
    

    程序执行正常,控制台信息如下:

    Employee(name=xzy, age=22, sex=m, salary=100000.0, address=null)
    

    1. 为Employee类添加serialVersionUID,序列化一个对象,修改serialVersionUID,反序列化该对象。

    修改serialVersionUID:

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Employee implements Serializable {
        private static final long serialVersionUID = 66666666666666L;//修改
        private String name;
        private Integer age;
        private char sex;
        private Double salary;
    }
    

    反序列化该对象:

    ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("employee.dat"));
    Employee employee1 = (Employee) inputStream.readObject();
    System.out.println(employee1);
    

    程序抛出异常,异常信息如下:

    Exception in thread "main" java.io.InvalidClassException: com.learn.java.extend.Employee; local class incompatible: stream classdesc serialVersionUID = 5815872246558374312, local class serialVersionUID = 66666666666666


    1. 为Employee类添加serialVersionUID,序列化一个对象,为其他类添加相同的serialVersionUID,将对象反序列化为其他对象。

    创建具有相同serialVersionUID的类:

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Test implements Serializable {
        private static final long serialVersionUID = 5815872246558374312L;
        private String name;
        private Integer age;
        private char sex;
        private Double salary;
    }
    

    反序列化对象:

    ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("employee.dat"));
    Test employee1 = (Test) inputStream.readObject();
    System.out.println(employee1);
    

    程序抛出异常,异常信息如下:

    Exception in thread "main" java.lang.ClassCastException: com.learn.java.extend.Employee cannot be cast to com.learn.java.extend.Test


    从以上3个测试的执行结果看,我的理解应该是对的。


    更进一步

    一旦类中添加了名为serialVersionUID的静态成员,那么序列化系统就可以读入这个类的对象的不同版本。

    “如果这个类只有方法产生了变化,那么反序列化时不会有任何问题。但是,如果数据域发生了变化,那么就可能会有问题。” ——《Java核心技术》

    事实上,上文部分代码的执行结果已经反映了这一点,比如:先序列化一个Employee对象,然后在Employee类中添加address属性,最后进行反序列化,得到的对象信息为:

    Employee(name=xzy, age=22, sex=m, salary=100000.0, address=null)
    

    再比如:先序列化一个Employee对象,然后修改Employee类的salary属性,最后进行反序列化,得到的对象信息为:

    Employee(name=xzy, age=22, sex=m, salary_=null)
    

    旧版本的对象可能具有更多或更少的数据域,亦或者是数据域具有不同的类型。在这种情况下,ObjectInputStream将尽力把旧版本对象转换成类现有版本的对象。

    ObjectInputStream会将这个类当前版本的数据域被序列化版本中数据域进行比较,当然,只会考虑非瞬时和非静态的数据域。

    如果,数据域名字匹配但类型不匹配:ObjectInputStream尝试进行类型转换。

    如果,被序列版本具有现有版本所没有的数据域:ObjectInputStream忽略这些额外的数据域。

    如果,被序列版本缺少现有版本所具有的数据域:ObjectInputStream将这些缺少的数据域设置为它们的默认值(如果是对象则是null,如果是数字则是0,如果是boolean则是false)

  • 相关阅读:
    文件的权限与隐藏属性
    Linux工具之bc计算器进制的转换
    老司机教你在windows不用软件隐藏重要文件
    LInux基础命令分类
    Linux文本的处理
    文件的默认权限UMASK
    H3C虚拟化之IRF
    H3C单臂路由配置
    H3C路由交换常用命令
    H3C交换机802.1&dot1x认证
  • 原文地址:https://www.cnblogs.com/XiaoZhengYu/p/12847088.html
Copyright © 2011-2022 走看看