谈谈序列化和反序列化
序列化简介
Java序列化是指将一个Java对象转化为一个二进制流的过程,反序列化是指将二进制流转化为一个Java对象的过程。一般进行序列化的目的有:
- 当程序退出时, 这些对象也就消失了, 而序列化正是为了将这些对象保存起来以便将来使用;
- 通过网络将序列化后的二进制流传输给远程
JVM
使用(RPC
、RMI
的基础)。
所有可能在网络上传输的对象都应该是可以序列化的,比如RMI
过程的中参数和返回值;所有需要保存到磁盘中的对象也应该是可以序列化的,比如说需要保存到HttpSession
或者ServletContext
中的对象。对象要实现序列化,必须实现以下两个接口的其中一个:
- Serializable
- Externalizable
这边简单介绍这两个接口的区别。
Externalizable继承了Serializable,该接口中定义了两个抽象方法:writeExternal()与readExternal()。当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()与readExternal()方法。还有一点值得注意:在使用Externalizable进行序列化的时候,在读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。所以,实现Externalizable接口的类必须要提供一个public的无参的构造器。
一般当我们序列化和反序列化时要实现些自定义逻辑时,会实现Externalizable接口。
对象流
我们要将对象进行序列化,可以使用ObjectOutputStream
和ObjectInputStream
进行序列化。
//使用了自动关闭资源的语法糖
try( ObjectOutputStrem oos = new ObjectOutputStrem(new FileOutputStream("object.txt")) ){
Person p = new Person();
oos.writeObject(p);
}catch(Exception e){
//handle Exception
}
以上代码是将对象序列化到本地文件中,想要将对象反序列化,可以使用ObjectInputStream。使用方式和上面的代码类似。反序列化时仅仅读取的是Java对象的数据,读不到类的数据,因此反序列化时必须要提供这个对象对应的Class文件,不然会报类找不到异常。另外反序列化时不会通过类的构造函数来初始化类。
当一个可序列化的类有多个父类时(包括直接父类和间接父类),这些父类要么有无参数的构造函数,要么也是可以序列化的,否则这个子类进行反序列化时将抛出InvalidClassException
。如果父类是不可以序列化的,只是带有无参数的构造函数,那么子类在进行序列化的时候不会将父类中定义的成员变量序列化到二进制流中。
引用类型成员变量的序列化
如果一个类中包含引用变量,那么只有这个引用变量是可序列化的,这个类本身才是可以序列化的。不然的话无论你是否实现Serializable
,都是不能进行序列化的。
所有对象只会被序列化一次,不然话会存在这样一个问题:对象A和B属于同一个类,他们同时引用了对象C。如果我们序列化A和B时,将对象C序列化两次的话,那么在我们反序列化的时候系统中会有两个C对象。这和我们序列化的初衷不符合。因此Java在序列化的时候采用了下面的序列化算法:
- 所有保存到磁盘的对象都会有一个序列化编号;
- 当程序试图序列化一个对象时,程序会先检查该对象是否已经被序列化过,只有该对象从未被序列化过(本次JVM中),才会将该对象转换成字节序列输出;
- 如果该对象已经序列化过,程序只会输出一个序列化编号,不会对该对象再次序列化。
如果是一个可变对象,我们在将其序列化之后,改变对象的内容,然后再试图对该对象进行序列化,这样是不会生效的。
自定义序列化
当对某个对象进行序列化时,系统会自动把该对象所有实例变量依次进行序列化,如果某个实例引用到另一个对象,则被引用的对象也会被序列化。
如果我们不希望对象的某个属性被序列化,那么我们可以在定义这个成员变量时加上transient
关键字。transient
关键字只能修饰实例变量。
transient
提供的机制过于简单,如果开发者想对某个实例变量进行比较复杂的序列化机制应该怎么做呢?在序列化和反序列化的过程中,如果对象需要特殊的处理逻辑,那么这些对象要提供如下的方法:
//可以通过此方法修改序列化的对象
private Object writeReplace() throws ObjectStreamException;
//方法中调用
private void writeObject(java.io.ObjectOutputStream out) throws IOException;
//使用writeObject的默认的序列化方式,除此之外可以加上一些其他的操作,如添加额外的序列化对象到输出:out.writeObject("XX")
defaultWriteObject()
private void readObject(java.io.ObjectInputStream in) throws Exception;
//可以通过此方法修改返回的对象
private Object readResolve() throws ObjectStreamException;
下面给出一个单例序列化和反序列化的列子:
public class PersonSingleton implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private PersonSingleton(String name) {
this.name = name;
};
private static PersonSingleton person = null;
public static synchronized PersonSingleton getInstance() {
if (person == null)
return person = new PersonSingleton("cgl");
return person;
}
private Object writeReplace() throws ObjectStreamException {
System.out.println("1 write replace start");
return this;//可修改为其他对象
}
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
System.out.println("2 write object start");
out.defaultWriteObject(); //out.writeInt(1);
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
System.out.println("3 read object start");
in.defaultReadObject(); //int i=in.readInt();
}
private Object readResolve() throws ObjectStreamException {
System.out.println("4 read resolve start");
return PersonSingleton.getInstance();//不管序列化的操作是什么,返回的都是本地的单例对象
}
}
上面四个方法的调用顺序依次是writeReplace-->writeObject-->readObject-->readResolve。
另外Java中还提供了另外一种自定义序列化的机制,就是实现Externalizable
接口,这种机制程序员在序列化过程中能有更大的控制权。实现Externalizable
接口需要实现另外两个方法writeExternal
和readExternal
。此时上面的writeObject
和readObject
方法将失效。下面提供一个列子
public class Person implements Externalizable {
private int age;
private String name;
private Person father;
//必须提供默认构造函数
public Person(){
System.out.println("csx-mg");
}
public Person(int age, String name,Person farher) {
this.age = age;
this.name = name;
this.father = farher;
}
//可修改为其他对象
private Object writeReplace() throws ObjectStreamException {
System.out.println("1 write replace start");
return this;
}
//实现Externalizable时,这个方法会失效,不会被调用
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
System.out.println("2 write object start");
out.defaultWriteObject();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("csx-mg");
}
//实现Externalizable时,这个方法会失效,不会被调用
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
System.out.println("3 read object start");
in.defaultReadObject(); //int i=in.readInt();
}
private Object readResolve() throws ObjectStreamException {
System.out.println("4 read resolve start");
return PersonSingleton.getInstance();//不管序列化的操作是什么,返回的都是本地的单例对象
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
System.out.println("csx-mg");
}
}
以上方法的调用顺序是writeReplace-->writeExternal-->readExternal-->readResolve
serialVersionUID 作用
JVM如何判断序列化与反序列化的类文件是否相同呢?
并不是说两个类文件要完全一样, 而是通过类的一个私有属性serialVersionUID来判断的, 如果我们没有显示的指定这个属性, 那么JVM会自动使用该类的hashcode值来设置这个属性, 这个时候如果我们对类进行改变(比如说加一个属性或者删掉一个属性)就会导致serialVersionUID不同, 所以对于准备序列化的类, 一般情况下我们都会显示的设置这个属性, 这样及时以后我们对该类进行了某些改动, 只要这个值保持一样, JVM就还是会认为这个类文件是没有变的。
参考
- 关于 Java 对象序列化您不知道的 5 件事(序列化并不安全)
公众号推荐
欢迎大家关注我的微信公众号「程序员自由之路」