java序列化详解2

上一篇文章我们了解了什么是序列化以及如何实现序列化,下面我们来继续看看序列化中的其他问题。

序列化静态变量

默认实现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 作用 是什么?举个例子说明

Java对象序列化文件追加对象的问题,以及Java的读取多个对象的问题解决方法。

多对象序列化为什么-4个字节

Java中对象序列化与反序列化

点赞

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注