序列化和反序列化
序列化(Serialization)是将对象的状态信息转化为可以存储或者传输的形式的过程,一般将一个对象存储到一个储存媒介,例如档案或记忆体缓冲等,在网络传输过程中,可以是字节或者XML等格式;而字节或者XML格式的可以还原成完全相等的对象,这个相反的过程又称为反序列化。
Java对象的序列化和反序列化
在Java中,我们可以通过多种方式来创建对象,并且只要对象没有被回收我们都可以复用此对象。但是,我们创建出来的这些对象都存在于JVM中的堆(heap)内存中,只有JVM处于运行状态的时候,这些对象才可能存在。一旦JVM停止,这些对象也就随之消失;但是在真实的应用场景中,我们需要将这些对象持久化下来,并且在需要的时候将对象重新读取出来,Java的序列化可以帮助我们实现该功能。
对象序列化机制(object serialization)是java语言内建的一种对象持久化方式,通过对象序列化,可以将对象的状态信息保存为字节数组,并且可以在有需要的时候将这个字节数组通过反序列化的方式转换成对象,对象的序列化可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。
在JAVA中,对象的序列化和反序列化被广泛的应用到RMI(远程方法调用)及网络传输中。
序列化及反序列化相关接口及类
Java为了方便开发人员将java对象序列化及反序列化提供了一套方便的API来支持,其中包括以下接口和类:
java.io.Serializable |
java.io.Externalizable |
java.io.ObjectOutput |
java.io.ObjectInput |
java.io.ObjectOutputStream |
java.io.ObjectInputStream |
Serialization接口
Java类通过实现java.io.Serialization接口来启用序列化功能,未实现此接口的类将无法将其任何状态或者信息进行序列化或者反序列化。可序列化类的所有子类型都是可以序列化的。序列化接口没有方法或者字段,仅用于标识可序列化的语义。
- 当试图对一个对象进行序列化时,如果遇到一个没有实现java.io.Serialization接口的对象时,将抛出NotSerializationException异常。
- 如果要序列化的类有父类,要想将在父类中定义过的变量序列化下来,那么父类也应该实现java.io.Serialization接口。
首先创建一个Person类,并且未实现java.io.Serialization接口。
@Data
public class Person {
private String name;
}
对未实现java.io.Serialization接口的对象进行序列化
public class SerializationDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person();
person.setName("apocalypse");
// 把对象序列化到文件
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream("person"));
oo.writeObject(person);
oo.close();
}
}
控制台输出结果:
接着创建一个Person类的子类Student,并且子类实现java.io.Serialization接口。
@Data
public class Student extends Person implements Serializable {
private static final long serialVersionUID = 6073686342416510460L;
private String schoolName;
}
对Student对象进行序列化和饭序列化
public class SerializationDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Student student = new Student();
student.setName("apocalypse");
student.setSchoolName("武汉大学");
// 把对象序列化到文件
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream("student"));
oo.writeObject(student);
oo.close();
// 反序列化
ObjectInputStream oi = new ObjectInputStream(new FileInputStream("student"));
student = (Student) oi.readObject();
System.out.println("Hi, My name is " + student.getName() + "," + student.getSchoolName());
oi.close();
}
}
控制台结果:
从结果中可以看到父类中的name属性并没有被序列化。
java.io.Externalizable接口
为了了解Externalizable接口和Serializable接口的区别先来看代码,我们将上面的Person类改为实现java.io.Externalization接口。
@Data
public class Person implements Externalizable {
private static final long serialVersionUID = 1207935110019913345L;
private String name;
@Override
public void writeExternal(ObjectOutput out) throws IOException {
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
}
}
public class SerializationDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person();
person.setName("apocalypse");
// 把对象序列化到文件
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream("person"));
oo.writeObject(person);
oo.close();
// 反序列化
ObjectInputStream oi = new ObjectInputStream(new FileInputStream("person"));
person = (Student) oi.readObject();
System.out.println("Hi, My name is " + person.getName() );
oi.close();
}
}
控制台结果:
通过上面的实例可以发现,对Person进行序列化然后再进行反序列化之后对象的属性都恢复成了默认值,也就是说之前的哪个对象的状态并没有被持久化下来,这就是Externalization和Serialization接口之间的区别;
Externalizable继承了Serializable,该接口中定义了两个抽象方法:writeExternal()与readExternal()。当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()与readExternal()方法。由于上面的代码中,并没有在这两个方法中定义序列化实现细节,所以输出的内容为空。还有一点值得注意:在使用Externalizable进行序列化的时候,在读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。所以,实现Externalizable接口的类必须要提供一个public的无参的构造器。
按照要求修改之后的代码是:
@Data
public class Person implements Externalizable {
private static final long serialVersionUID = 1207935110019913345L;
private String name;
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
}
}
修改后控制台结果:
这样就可以将之前的对象状态保存下来了,如果Person类中没有无参数的构造函数,在运行时会抛出异常:java.io.InvalidClassException;
静态变量的序列化
静态变量序列化
public class StaticVariableSerialization implements Serializable {
private static final long serialVersionUID = -1374037509542418808L;
public static int staticVar = 5;
public static void main(String[] args) {
try {
// 序列化
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("StaticVariable"));
out.writeObject(new StaticVariableSerialization());
out.close();
// 反序列化
ObjectInputStream oinb = new ObjectInputStream(new FileInputStream("StaticVariable"));
StaticVariableSerialization tb = (StaticVariableSerialization) oinb.readObject();
oinb.close();
// 修改前,获取对象静态变量值
System.out.println("修改前,获取对象静态变量值" + tb.staticVar);
// 序列化后修改为10
StaticVariableSerialization.staticVar = 10;
// 反序列化
ObjectInputStream oina = new ObjectInputStream(new FileInputStream("StaticVariable"));
StaticVariableSerialization ta = (StaticVariableSerialization) oina.readObject();
oina.close();
// 修改后,获取对象静态变量值
System.out.println("修改后,获取对象静态变量值" + ta.staticVar);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
main 方法中,将对象序列化后,修改静态变量的数值,再将序列化对象读取出来,然后通过读取出来的对象获得静态变量的数值并打印出来,打印出来的是10还是5?
最后的输出是 10,对于无法理解的读者认为,打印的 staticVar 是从读取的对象里获得的,应该是保存时的状态才对。之所以打印 10 的原因在于序列化时,并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量。
Transient 关键字使用
Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
为何要定义serialVersionUID
其实简单的来理解,serialVersionUID就是每一个类的唯一标识,对象在序列化时会把这个唯一标识保存到字节数组中,等到反序列化时,会将反序列化类中的serialVersionUID与字节数组中保存的serialVersionUID进行比较,只有两个serialVersionUID相等时,才能成功的反序列化。既然现在知道了serialVersionUID的作用,那试着去想一下,为什么在java类文件中需要自己定义serialVersionUID ?这样做有什么作用?
下面考虑一种场景,User类,该类实现了java.io.Serializable接口,但没有定义serialVersionUID的值,对类对象进行序列化。
@Data
public class User implements Serializable {
private String name;
}
User对象进行序列化和反序列化
public class WhySerialVersionUID {
public static void main(String[] args) throws Exception {
User user = new User();
user.setName("apocalypse");
//对象序列化到文件
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream("user"));
oo.writeObject(user);
oo.close();
// 反序列化
ObjectInputStream oi = new ObjectInputStream(new FileInputStream("user"));
user = (User) oi.readObject();
System.out.println("Hi, My name is " + user.getName());
oi.close();
}
}
控制台结果:
从结果中可以看到,序列化和反序列化都顺利完成。但是如果在序列化之后,User这个类发生了改变呢?比如,多了一个成员变量。我们做如下试验,还是先将对象序列化到一个文件中,之后在User这个类中添加一个成员变量,如下:
@Data
public class User implements Serializable {
private static final long serialVersionUID = 2915403099177079124L;
private String name;
// 新添加字段
private String address;
}
接着进行反序列化,就发现运行出错了,会报如下错误:
意思就是说,文件流中的class和本地中的class,也就是修改过后的class,serialVersionUID值不相等,处于安全机制考虑,程序抛出了错误,并且拒绝载入。在我们的例子中,是没有指定serialVersionUID的,那么java编译器会自动给这个class进行一个摘要算法,类似于指纹算法,只要这个文件多一个空格,得到的serialVersionUID就会截然不同的。所以,我们添加了一个字段后,由于没有显指定serialVersionUID,编译器又为我们生成了一个serialVersionUID,当然和前面保存在文件中的那个不一样,于是就出现了2个serialVersionUID不一致的错误。因此,只要我们自己指定了serialVersionUID,就可以在序列化后,去添加一个字段,或者方法,而不会影响到后期的还原,因此如果我们真的有需求要在序列化后添加一个字段或者方法呢?只需要自己去指定serialVersionUID。另外serialVersionUID的生成,Idea 和 Eclipse 都可以自动生成。
参考文章:Java 序列化的高级认识