1、序列化是干啥用的?
序列化的原本意图是希望对一个Java对象作一下“变换”,变成字节序列,这样一来方便持久化存储到磁盘,避免程序运行结束后对象就从内存里消失,另外变换成字节序列也更便于网络运输和传播,所以概念上很好理解:
- 序列化:把Java对象转换为字节序列。
- 反序列化:把字节序列恢复为原先的Java对象。
而且序列化机制从某种意义上来说也弥补了平台化的一些差异,毕竟转换后的字节流可以在其他平台上进行反序列化来恢复对象。
2、对象序列化的方式?
在Java中,如果一个对象要想实现序列化,必须要实现下面两个接口之一:
- Serializable 接口
- Externalizable 接口
2.1 Serializable 接口
一个对象想要被序列化,那么它的类就要实现此接口或者它的子接口。
这个对象的所有属性(包括private属性、包括其引用的对象)都可以被序列化和反序列化来保存、传递。不想序列化的字段可以使用transient修饰。
由于Serializable对象完全以它存储的二进制位为基础来构造,因此并不会调用任何构造函数,因此Serializable类无需默认构造函数,但是当Serializable类的父类没有实现Serializable接口时,反序列化过程会调用父类的默认构造函数,因此该父类必需有默认构造函数,否则会抛异常。
使用transient关键字阻止序列化虽然简单方便,但被它修饰的属性被完全隔离在序列化机制之外,导致了在反序列化时无法获取该属性的值,而通过在需要序列化的对象的Java类里加入writeObject()方法与readObject()方法可以控制如何序列化各属性,甚至完全不序列化某些属性或者加密序列化某些属性。
2.2 Externalizable 接口
它是Serializable接口的子类,用户要实现的writeExternal()和readExternal() 方法,用来决定如何序列化和反序列化。
因为序列化和反序列化方法需要自己实现,因此可以指定序列化哪些属性,而transient在这里无效。
对Externalizable对象反序列化时,会先调用类的无参构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为private、默认或protected级别,会抛出java.io.InvalidException: no valid constructor异常,因此Externalizable对象必须有默认构造函数,而且必需是public的。
2.3 对比
使用时,你只想隐藏一个属性,比如用户对象user的密码pwd,如果使用Externalizable,并除了pwd之外的每个属性都写在writeExternal()方法里,这样显得麻烦,可以使用Serializable接口,并在要隐藏的属性pwd前面加上transient就可以实现了。如果要定义很多的特殊处理,就可以使用Externalizable。
当然这里我们有一些疑惑,Serializable 中的writeObject()方法与readObject()方法科可以实现自定义序列化,而Externalizable 中的writeExternal()和readExternal() 方法也可以,他们有什么异同呢?
- readExternal(),writeExternal()两个方法,这两个方法除了方法签名和readObject(),writeObject()两个方法的方法签名不同之外,其方法体完全一样。
- 需要指出的是,当使用Externalizable机制反序列化该对象时,程序会使用public的无参构造器创建实例,然后才执行readExternal()方法进行反序列化,因此实现Externalizable的序列化类必须提供public的无参构造。
- 虽然实现Externalizable接口能带来一定的性能提升,但由于实现ExternaLizable接口导致了编程复杂度的增加,所以大部分时候都是采用实现Serializable接口方式来实现序列化。
3、序列化版本号serialVersionUID
我们知道,反序列化必须拥有class文件,但随着项目的升级,class文件也会升级,序列化怎么保证升级前后的兼容性呢?
Java序列化提供了一个private static final long serialVersionUID
的序列化版本号,只有版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。 ```java public class Person implements Serializable { //序列化版本号 private static final long serialVersionUID = 1111013L; private String name; private int age; //省略构造方法及get,set }
```
如果反序列化使用的class的版本号与序列化时使用的不一致,反序列化会报InvalidClassException异常。
序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。
什么情况下需要修改serialVersionUID呢?分三种情况。
- 如果只是修改了方法,反序列化不容影响,则无需修改版本号;
- 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;
- 如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。