序列化与serialVersionUID
存储一个对象时,对象所属类的描述信息也必须存储。类的描述信息包括:
- 类名
- 序列化版本ID(serialVersionUID)
- 描述序列化方法的标志集
- 对数据域的描述
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;
}
- 为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)
- 为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
- 为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)