上一篇文章我们了解了什么是序列化以及如何实现序列化,下面我们来继续看看序列化中的其他问题。
序列化静态变量
默认实现Serializable接口的序列化是对于一个类的非static,非transient的实例变量进行序列化与反序列化。刚刚上面也说了,如果要对static实例变量进行序列化就要使用Externalizable接口,手动实现。
序列化时只保存
1)对象的类型
2)对象属性的类型
3)对象属性的值
实现Externalizable接口的类完全由自身来控制序列化的行为。而且必须实现writeExternal(ObjectOutput out)和readExternal(ObjectInput in)。
注意在对实现了这个接口的对象进行反序列化的时候,会先调用类的不带参数的构造函数,这个和之前的默认反序列化方式是不一样的。
Externalizable不常用,因为Externalizable可以用 transient+Serializable代替。
但是Externalizable可以实现自主的序列化过程,作用有以下几点
1. Externalizable中序列化static成员变量。
2. 对敏感字段的加密。
在大多数jdk源码中,往往使用另一种方式来自己实现序列化。
它们都继承了Serializable,但是它们同时也实现了writeObject与readObject的私有方法,把不想序列化的属性transient,来实现自己需要的序列化。
1、 通过writeObject()方法,写入要保存的变量。writeObject的原始定义是在ObjectOutputStream.java中,我们按照如下示例覆盖即可:
private void writeObject(ObjectOutputStream out) throws IOException{
out.defaultWriteObject();// 使定制的writeObject()方法可以利用自动序列化中内置的逻辑。
out.writeInt(id); // 若要保存“int类型的值”,则使用writeInt()
out.writeObject(new Person(1,"李")); // 若要保存“Object对象”,则使用writeObject()
}
2、通过readObject()方法,读取之前保存的变量。readObject的原始定义是在ObjectInputStream.java中,我们按照如下示例覆盖即可:
private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException{
in.defaultReadObject(); // 使定制的readObject()方法可以利用自动序列化中内置的逻辑。
id = in.readInt(); // 若要读取“int类型的值”,则使用readInt()
Person per = (Person)in.readObject(); // 若要读取“Object对象”,则使用readObject()
}
那么有一点就很奇怪了,它们被外部类调用但事实上这是两个private的方法。并且它们既不存在于java.lang.Object,也没有在Serializable中声明。那么ObjectOutputStream如何使用它们的呢?
如果聪明一点的同学可能会马上反应过来用反射来实现,具体的可以自己去看一下源码。
以ObjectInputStream 为例(ObjectOutputStream同样):
首先,会调用readObject(),通过Object obj = readObject0(false);调用readObject0;里面有这一句return checkResolve(readOrdinaryObject(unshared));调用readOrdinaryObject方法 在readOrdinaryObject会调用readSerialData(obj, desc);然后readSerialData会调用slotDesc.invokeReadObject(obj, this);这里调用ObjectStreamClass的invokeReadObject(Object obj, ObjectInputStream in)
里面 readObjectMethod.invoke(obj, new Object[]{ in });这显然是一个通过反射进行的方法调用,那么readObjectMethod是什么方法?别急继续往下看,到ObjectStreamClass(final Class> cl)
发现里面有这一句readObjectMethod = getPrivateMethod(cl, “readObject”,new Class>[] { ObjectInputStream.class },Void.TYPE),getPrivateMethod方法如下:
/**
* Returns non-static private method with given signature defined by given
* class, or null if none found. Access checks are disabled on the
* returned method (if any).
*/
private static Method getPrivateMethod(Class<?> cl, String name,
Class<?>[] argTypes,
Class<?> returnType)
{
try {
Method meth = cl.getDeclaredMethod(name, argTypes);
meth.setAccessible(true);
int mods = meth.getModifiers();
return ((meth.getReturnType() == returnType) &&
((mods & Modifier.STATIC) == 0) &&
((mods & Modifier.PRIVATE) != 0)) ? meth : null;
} catch (NoSuchMethodException ex) {
return null;
}
}
到此,我们就明白了这是通过反射来实现的。
父类的序列化
情境:一个子类实现了 Serializable 接口,它的父类都没有实现 Serializable 接口,序列化该子类对象,然后反序列化后输出父类定义的某变量的数值,该变量数值与序列化时的数值不同。
解决:要想将父类对象也序列化,就需要让父类也实现Serializable 接口。如果父类不实现的话的,就需要有默认的无参的构造函数。在父类没有实现 Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。如果你kao虑到这种序列化的情况,在父类无参构造函数中对变量进行初始化,否则的话,父类变量值都是默认声明的值,如 int 型的默认是 0,string 型的默认是 null。
序列化与反序列化的几种情况
- 1、Person类序列化之后,从A端传输到B端,然后在B端进行反序列化。在序列化Person和反序列化Person的时候,A端和B端都需要存在一个相同的类。如果两处的serialVersionUID不一致,会产生什么错误呢?
java.io.InvalidClassException: serializable.Person; local class incompatible: stream classdesc serialVersionUID = 123456789, local class serialVersionUID = 12345678
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
- 2、假设两处serialVersionUID一致,如果A端增加一个字段,然后序列化,而B端不变,然后反序列化,会是什么情况呢?
新增 public int age; 生成序列化文件,代表A端。删除 public int age,反序列化,代表B端,最后的结果为:执行序列化,反序列化正常,但是A端增加的字段丢失(被B端忽略)。
- 3、假设两处serialVersionUID一致,如果B端减少一个字段,A端不变,会是什么情况呢?
序列化,反序列化正常,B端字段少于A端,A端多的字段值丢失(被B端忽略)。
- 4、假设两处serialVersionUID一致,如果B端增加一个字段,A端不变,会是什么情况呢?
序列化,反序列化正常,B端新增加的int字段被赋予了默认值0。
参考
java类中serialversionuid 作用 是什么?举个例子说明