对于Java序列化的认知,我一直停留在最浅显的认知上(把那个要序列化的类实现 Serializable接口就可以了)。但是我工作也将近有两年了,见到Serializable的次数越来越多,我便对它产生了浓厚的兴趣。最近也是因为疫情的原因,刚刚复工没多久,工作不饱和,有点时间研究研究了。
关于Java序列化:
Java 序列化是早在JDK 1.1 时就已经引入的一组开创性的特性,用于将 Java 对象转换为字节数组,便于存储或传输。此后,仍然可以将字节数组转换回 Java 对象原有的状态。
序列化的思想是“冻结”对象状态,然后写到磁盘或者在网络中传输;反序列化的思想是“解冻”对象状态,重新获得可用的 Java 对象。
查看源码,发现Serializable接口里边实际什么都没有。但是关于他的注释描述确是一大堆。大致描述的内容就是类序列化的实现方式、不支持序列化时会出现的问题等等,足见这个接口的重要性。
来个例子:
首先创建一个Person类,只有name和age两个字段,不实现Serializable接口。
再创建一个测试类,通过 ObjectOutputStream 将Person写入到文件当中,这就是一种序列化的过程;
再通过 ObjectInputStream 将Person从文件中读出来,这就是一种反序列化的过程。
由于Person没有实现Serializable,运行时会报错:
顺着报错的堆栈信息,找到ObjectOutputStream的writeObject0() 方法。部分源码如下:
可以看出ObjectOutputStream在序列化的时候,会判断被序列化的对象是哪一种类型,字符串?数组?枚举?还是 Serializable,如果全都不是的话,抛出 NotSerializableException。
而如果Person实现了Serializable就可以序列化成功了。
以ObjectOutputStream为例,其一个对象序列化的时候会依次调用:writeObject()
→writeObject0()
→writeOrdinaryObject()
→writeSerialData()
→invokeWriteObject()
→defaultWriteFields()
。
以ObjectInputStream为例,其一个对象序列化的时候会依次调用: readObject()
→readObject0()
→readOrdinaryObject()
→readSerialData()
→defaultReadFields()
。
到这里其实能看出:Serializable 接口之所以定义为空,是因为它只起到了一个标识的作用,告诉程序实现了它的对象是可以被序列化的,但真正序列化和反序列化的操作并不需要它来完成。
特殊的static 和 transient:
static
和 transient
修饰的字段是不会被序列化的。
如下面的例子
所以可以看出:
- 被static修饰的字段不会被序列化
- 被transient修饰的,反序列化后的值为null,而不是序列化前的值。
因为序列化保存的是对象的状态,static 修饰的字段属于类的状态,因此可以证明序列化并不保存 static 修饰的字段。
而transient 的中文字义为“临时的”,它可以阻止字段被序列化到文件中,在被反序列化后,transient 字段的值被设为初始值,比如 int 型的初始值为 0,对象型的初始值为 null。
serialVersionUID:
serialVersionUID 被称为序列化 ID,它是决定 Java 对象能否反序列化成功的重要因子。在反序列化时,Java 虚拟机会把字节流中的 serialVersionUID 与被序列化类中的 serialVersionUID 进行比较,如果相同则可以进行反序列化,否则就会抛出序列化版本不一致的异常。
所以建议当一个类实现了 Serializable 接口后,该类最好产生一个序列化 ID,就像下面这样:
附上idea设置提醒产生一个序列化 ID:
我们来验证一下这个问题:
思路大致是这样的:先序列化一个对象,然后改变serialVersionUID,然后再反序列化。看看结果
序列化:
改变serialVersionUID:
反序列化:
结果:InvalidClassException
总结:
小小的Serializable 竟然有这么多可以研究的内容!
今后如果再遇到序列化问题以及InvalidClassException异常,我想应该能够明白问题的原因了。