Java序列化总结

昨天读到了Hollis的一篇关于序列化的文章,以及文章里所关联的之前几篇文章,让我对序列化有了一个比较深入的了解,所以在这里做个总结,加深理解。

原文地址:https://mp.weixin.qq.com/s/5xcDDtsVYdgzUebF3_Mg4g

以下是总结,基本都是自己的理解,大白话。如有不对的地方,请指正。

1、什么是java序列化,为什么要序列化。

因为java对象是存活在jvm里的,一旦jvm停止,那么java对象也就灭亡了。java序列化就是将java对象按照一个规则以二进制的方式到存储硬盘中,保证对象的持久化。在网络传输中,以及RPC调用中,需要通过二进制方式传输,所以也需要将对象进行序列化。在需要使用对象,或调用方接收到返回的二进制信息后,可以通过既定规则将二进制信息转成java对象。由于在反序列化过程中,采用的是反射方式构造对象,所以会破坏单例模式,下面会谈到。

另外,还搜到一些文章说,由于序列化存在比较大的安全风险,oracle正计划摒弃序列化,这个就不细写了。

2、如果实现序列化。

只要一个对象实现了java.io.Serializable或者java.io.Externalizable这两个接口,就表示这个对象可以被序列化。其中Externalizable接口继承自Serializable接口。

3、Serializable为什么是一个空接口。

Serializable是一个标记接口,标记一个对象为可序列化的,然后在序列化过程中,会判断一个对象是否可序列化,如果不是,则会抛出异常:NotSerializableException。

如果一个父类实现了Serializable接口,那么它的所有子类都可以被序列化。

4、如何进行序列化。

建一个User类,实现Serializable,里面包含两个参数name和age,并生成get/set方法,这个就不写了。下面是序列化与反序列化过程

User u = new User();
u.setAge(12);
u.setName("张三");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("SerializableTest"));
objectOutputStream.writeObject(u);
IOUtils.closeQuietly(objectOutputStream);
File file = new File("SerializableTest");
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
User user = (User) objectInputStream.readObject();
IOUtils.closeQuietly(objectInputStream);
System.out.println(user.getName());//打印张三
System.out.println(user.getAge());//打印12

可以看到,新的User对象被创建了,并且属性值也可以还原。顺便说一下,这也是面试经常被问到的不使用new创建对象的方式之一。

5、Serializable和Externalizable的区别。

同样那个User类,这次改为实现Externalizable接口。在更改后,会发现编辑器提示必须实现两个方法:writeExternal和readExternal,先不用管。其他代码不变,再跑一次,会发现这次打印的name是null,age是0。

所以可以得出第一个区别:

区别1:Serializable在反序列化后,默认将所有成员变量的值还原,而Externalizable会默认将所有的成员变量置为初始值,String为null,int为0。如果想要让Externalizable能将成员变量值进行序列化,就需要自己实现writeExternal和readExternal这两个方法。

这样的好处是,我们可以自定义在序列化过程中,允许暴露哪些变量值,也可以对被序列化的值做加密操作。如一个用户的密码,当我们重写writeExternal方法时,可以将密码进行加密再进行序列化,取出时再进行解密,保证信息安全。

然后来看第二个区别:我们给这个User类中新增一个构造方法:

public User(int age) {
     this.age=age;
}

然后分别调用一次,会发现Serializable可以正常运行,而Externalizable会报错: java.io.InvalidClassException: no valid constructor。

区别2:Serializable在反序列化中,采用了反射方式直接创建对象,而Externalizable采用了调用无参构造方法创建对象。

而Java在没有写构造方法时,会默认生成无参构造方法,但是如果自定义了有参构造方法后,就不会再生成了。

6、Serializable怎么实现某些字段不被序列化,transient的作用。

transient就是标识在序列化过程中,某一个变量不参与序列化。所以当某个变量被transient修饰,那么在反序列化后,该变量会显示初始值,就和Externalizable效果一样了。所以transient在Externalizable无实际意义。

7、如何打破transient的限制,以及如何重写Externalizable的writeExternal和readExternal方法。

先说如何打破transient的限制。需要在User类中,增加两个方法:

private void readObject(java.io.ObjectInputStream s)
和
private void writeObject(java.io.ObjectOutputStream s) 

是的,很奇怪的两个方法,没有继承,没有实现,Object中也不存在。这是因为在Serializable的序列化过程中,全部通过反射实现的,反射过程中,会检查对象是否具有这两个方法,如果有的话,会主动调用;如果没有,就采用Java默认的方式进行处理。

并且writeObject和writeExternal写法基本类似,只是writeObject中要增加一句话:s.defaultWriteObject();这是因为Serializable是有默认的序列化方法的,如果你只想重定义某个变量的序列化,其他变量依然按照默认的方式进行,就需要靠这句话进行声明。否则,只会序列化自定义的那个变量了。read同理。

说明:因为Serializable和Externalizable自定义实现的都是以read和write开头的,所以后面就统称read和write了。

private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
    s.defaultReadObject();//Serializable接口需要,Externalizable接口不需要
    setAge(s.readInt());//或者直接age=s.readInt()
    setName(s.readUTF());
}

private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
    s.defaultWriteObject();//Serializable接口需要,Externalizable接口不需要
    s.writeInt(age);
    s.writeUTF(name);
}

我们看到在自定义序列化中,直接使用了readInt和writeInt,而没有指定这个int到底是那个参数。这是因为在序列化过程中,产生的二进制文件会按照定义顺序依次写入变量及其对应的值,所以在读取时,必须需要按照写入顺序来进行读取了。

举例说明:如果只有一个String name和int age,因为这两个变量是两个类型,所以以下两种读取顺序,并不影响反序列化

//简写代码
s.writeInt(age);//先序列化int,如:12
s.writeUTF(name);//后序列化String,如:张三
//第一种
setAge(s.readInt());//先读取int,结果为12
setName(s.readUTF());//后读取String,结果为张三
//第二种
setName(s.readUTF());//先读取String,结果为张三
setAge(s.readInt());//后读取int,结果为720902

反过来,先序列化String,后序列化int,然后先读int,后读String则会报错。

以上出现错误int数据和报错的原因是因为,反序列化的过程,姑且用“翻译”二字来描述,它是一个线性过程。以代码中的错误例子说,因为首先要读取的是String,但是翻译过程中首先遇到了int数据,会被抛弃,接着翻译第二个,是String数据,然后将其赋值给变量name,接着需要读int了,但是下一段要翻译的,已经不是变量了,所以会读取这一块数据所对应的数字,来进行赋值。String报错也同理,因为那块数据无法被转为UTF格式。

所以就要求,在自定义序列化与反序列化的过程中,读写操作的顺序必须一致!

还有一点要注意:write和read方法必须成对实现,不能单独实现其中一个方法。

如果只有write,没有read,Externalizable类型的对象的变量值依然为初始值。Serializable类型的对象,会出现反序列化的结果和实际不符。如下:

private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
	s.defaultWriteObject();
	s.writeUTF(name);
	s.writeInt(age+1);//或者是s.write(encrypt(passwd));这样的加密
}

我们序列化的时候假设age=10,这时使age+1,但是没有重写read方法,这时会调用java默认反序列化方式读取这个age,如果它不是transient的,那么这个age依然会是10。而如果它是transient的,这个age会是0。想要让它等于11,就必须在read方法中主动获取。

而只有read,没有write,程序会直接抛错。具体原因后面说明。

8、为什么要打破transient的限制,或者说,为什么既定义一个变量为transient,却还要自定义实现它的序列化。

这一点,Hollis在文中举了ArrayList的例子来说明,大家可以去看,我直接把结论拿过来就好了。

ArrayList实际上是动态数组,每次在放满以后自动增长设定的长度值,如果数组自动增长长度设为100,而实际只放了一个元素,那就会序列化99个null元素。为了保证在序列化的时候不会将这么多null同时进行序列化,ArrayList把元素数组设置为transient,然后通过自定义序列化的方式,每次只把数组中具体的值进行序列化。

道理是懂了,但是研究到这里,我反而产生一个疑问,加了transient的Serializable,在序列化的思想上和Externalizable岂不没区别了?不知道这么想到底对不对。

9、序列化的调用过程,为什么实现了Serializable接口就可以被序列化。

依然取自原文,序列化调用流程如下:

writeObject ---> writeObject0 --->writeOrdinaryObject--->writeSerialData--->invokeWriteObject

在writeObject0方法中,我们会看到如下一段代码:

 if (obj instanceof String) {
    writeString((String) obj, unshared);
} else if (cl.isArray()) {
    writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
    writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
    writeOrdinaryObject(obj, desc, unshared);
} else {
    if (extendedDebugInfo) {
        throw new NotSerializableException(
            cl.getName() + "\n" + debugInfoStack.toString());
    } else {
        throw new NotSerializableException(cl.getName());
    }
}

也就是通过instanceof判断一个对象是否实现了Serializable,没有就会抛错。

至于为什么会有4个if语句,是因为其他三个,String,Array和Enum的底层代码,其实也是实现了Serializable的,不过他们的序列化过程和对象不一样,需要走不同的处理逻辑。

下面就不贴源码了,只说明一下。

在writeOrdinaryObject中,会看到里面有判断这个类是否实现了Externalizable,然后有一个分支writeExternalData去处理。因为大部分都是实现了Serializable,所以就会走writeSerialData方法。

在writeSerialData中,会看到一句判断:slotDesc.hasWriteObjectMethod(),这就是在判断这个对象是否有重写的WriteObject方法,有的话,就通过反射的方式调用自定义的方法,没有才会走默认的方法去处理。

同样读取也是这样的流程。

10、serialVersionUID的作用。

serialVersionUID主要是标识一个序列化后的对象,当它进行反序列化时,判断两者的这个ID是否一致,一致则认为是同一个对象,允许被序列化,不一致则不被序列化,同时抛出一个错误:java.io.InvalidClassException。

这也就是为什么所有的serialVersionUID都可以被编辑器生成为1L,不是为了区分每一个对象,而是标识一个对象序列化与反序列化前后是否一致。

当我们没有给出一个serialVersionUID时,jvm会根据类名、接口名、成员方法及属性等生成一个默认的。所以只要一个类的这些内容没有变更过,那么在反序列化时,不会报错。

11、当变量是一个对象时,如何序列化。

对于这一点,没有做深入研究。只说一个结论吧:

如果一个类A中的成员变量为对象B时,如果要对A进行序列化,则B必须也是可以被序列化的,除非它被transient修饰。

12、如何防止反序列化破坏单例模式

之前提到,由于反序列化是通过反射调用无参数的构造方法创建一个新的对象,所以会导致创建多个类。为了避免这个问题,就需要重写readResolve方法了。

private Object readResolve(){
    return singleton;
}

可以看到,我们是直接返回了这个类的实例。然后再看源码的readOrdinaryObject方法中,会有对这个方法的判断

if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
{
    Object rep = desc.invokeReadResolve(obj);
    if (unshared && rep.getClass().isArray()) {
        rep = cloneArray(rep);
    }
    if (rep != obj) {
        // Filter the replacement object
        if (rep != null) {
            if (rep.getClass().isArray()) {
                filterCheck(rep.getClass(), Array.getLength(rep));
            } else {
                filterCheck(rep.getClass(), -1);
            }
        }
        handles.setObject(passHandle, obj = rep);
    }
}

13、序列化后的二进制文件分析

我把这一点放在最后说,是因为我其实不太会分析这个,所以就推荐一篇文章,让大家去看吧:https://www.toutiao.com/i6579019730017845763/

我只把个人对这篇文章的总结列一下就好,也能回答一下上面遗留的问题。

a、默认实现的序列化方式,产生的二进制文件中,每个变量的值会跟随在变量后面。而自定义实现的序列化方式,是在将整个类都进行序列化完毕后,在后面补充每个变量的值。上面也说过,他是一个线性“翻译”的过程,所以在翻译完整个类后,读取到变量值再进行复制。所以上面第7点最后说到的只有read,没有write,程序会直接抛错。当处理read方法时,就是在读取这个类最后的数据,但是没有write去写。则读到的要么是空,要么是乱码,所以会报错

b、如果一次性序列化的多个类中,同时有对另一个类的引用。比如User1和User2这两个类中,都引用了Company这个类,那么在序列化时,Company只会被序列化一次,然后生成一个序号,让两个User指向它。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值