Java序列化Serializable的那些事儿
说到Java的序列化,有个问题就是为什么需要序列化,更优先的一个问题是什么是序列化。
序列化的含义
《Java编程思想》中这么解释,Java的对象序列化是将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象。
换句话说就是,主要将对象的可变信息以字节序列,保存在硬盘文件、数据库或者通过网络传输到另一个JVM中等等,等到需要在内存中恢复该对象当时的状态时,也就是当别的机器或程序运行时需要该对象的状态时,可以反序列化这些字节序列,将此对象还原出来使用(也说明了序列化是可以跨操作系统和JVM的)。这种机制就叫做序列化。
序列化的用途
明白了序列化的含义,也不难清楚序列化的用途了。
- 当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
- 当你想用套接字在网络上传送对象的时候;
- 当你想通过RMI传输对象的时候。
明白了序列化的含义及用途,接下来需要了解序列化的使用。
序列化的使用
package com.szh.serializable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class TestSerializable {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person p1 = new Person();
Dog d1 = new Dog();
d1.setName("Dog1");
p1.setId(1);
p1.setName("Tom");
p1.setDog(d1);
// serializable
if (!new File("E:/tests").exists()) {
new File("E:/tests").mkdirs();
}
FileOutputStream fos = new FileOutputStream(new File("E:/tests/serializable_file.ser"));
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(p1);
oos.close();
// deserializable
FileInputStream fis = new FileInputStream(new File("E:/tests/serializable_file.ser"));
ObjectInputStream ois = new ObjectInputStream(fis);
Object obj = ois.readObject();
ois.close();
Person p = (Person) obj;
System.out.println(p);
}
public static class Person implements Serializable {
private static final long serialVersionUID = -1891426275960796136L;
private transient int id;
private String name;
private transient Dog dog;
public Dog getDog() {
return dog;
}
public void setDog(Dog dog) {
this.dog = dog;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person [id=" + id + ", name=" + name + ", dog=" + dog + "]";
}
}
public static class Dog {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog [name=" + name + "]";
}
}
}
在序列化的使用中,主要有以下几个问题:
- transient关键字的作用;
- serialVersionUID的作用;
- 序列化警告的处理方式。
transient关键字的作用
transient,意为短暂的,临时的。在Java中,transient声明一个实例变量,当对象存储时,它的值不需要维持。换句话来说就是,用transient关键字标记的成员变量不参与序列化过程。
如示例代码中Person拥有一个transient修饰的id和Dog,程序运行后,经过序列化和反序列化,结果如下:
Person [id=0, name=Tom, dog=null]
可以发现,id和Dog可以认为分别为各自类型的缺省值,id=0,dog=null。明显这两个变量未经过序列化过程。
注意:不需要经过序列化的类型的成员变量,使用transient修饰后可以不实现Serializable接口。如Person的Dog不需要序列化。当然,非要给Dog实现Serializable接口也不影响结果。有意思的是,在eclipse中自动生成toString()方法,发现使用了transient修饰的成员变量默认不被选择参与打印对象字符串。
serialVersionUID的作用
当没有显式地定义long类型的serialVersionUID变量时,Java序列化机制会根据编译的class(它通过类名,方法名等诸多因素经过计算而得,理论上是一一映射的关系,也就是唯一的)自动生成一个serialVersionUID作序列化版本比较来使用。
解析serialVersionUID的jdk代码,可调试以下代码:
因此,也可验证没有显式添加serialVersionUID也同样拥有序列号,所以在序列化的时候必然也有jdk代码根据各种因素来生成serialVersionUID。
这种情况下,如果class文件(主要是类名、方法名等)没有发生变化(增加空格、换行、注释等等不会产生变化),就算再编译多次,serialVersionUID也不会变化。但是一旦变化,如给类增加了方法、属性等,那么在反序列化时,就会出现序列化版本不一致的异常(InvalidCastException)!如下:
Exception in thread "main" java.io.InvalidClassException: com.szh.serializable.TestSerializable$Person; local class incompatible: stream classdesc serialVersionUID = -5579678255079137242, local class serialVersionUID = -1463584727334114570
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:617)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1622)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1517)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1771)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1350)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:370)
at com.szh.serializable.TestSerializable.main(TestSerializable.java:30)
因此,serialVersionUID即代表了对应类的版本号,目的是为了保证序列化和反序列化时,类信息的一致性和安全性。
序列化警告的处理方式
在Java中,我们对警告不应无视,每一条警告都应该做合适的处理。那对于序列化时,我们实现了Serializable接口,却未显示定义serialVersionUID时,IDE会提示出三种处理方式:
- Add default serial version ID;
- Add generated serial version ID;
- Add @SuppressWarnings 'serial' to 'Person'。
当我们选择第一种处理方式时,相当于显式地手动定义了类的当前版本(自己去维护)。当我们以后需要此类时,需要同步修改serialVersionUID。
当我们选择第二种处理方式时,相当于显式地自动定义了类的当前版本(根据类信息等内容自动生成)。当我们以后需要此类时,需要重新生成serialVersionUID。
当我们选择第三种处理方式时,相当于告诉编译器忽略此警告。那么问题来了,第三种方式貌似可有可无,毕竟相当于上面哪种都未选择。答案是否定的。
在Java中,任何警告我们都不应不处理(选择使用第三种注解方式忽略警告也认为是一种处理方式,但是三种方式都不选择的话,则认为是不处理),虽然使用@SuppressWarnings去忽略也是一样的运行效果。因为,这是一种很好地编码习惯,有时警告也会造成一些意想不到的问题,所以我们应当处理代码中所有的警告,对于确实可以忽略掉的警告,jdk提供了这个注解来标记代码警告行,这样便可以使我们的代码逻辑更加严密,因为我们处理了每一条警告。
那么我们如何在编译后的class文件中,找到类的版本信息呢?
序列化与字节码class文件
class文件,存放了十六进制字节码,其中包含了类编译时的序列版本号。我们可以使用如下命令进行解析(反编译):
javap -v class文件名 > 输出文件名
如:javap -v E:/J2SE_workspace/MyTest/bin/com/szh/serializable/TestSerializable$Person.class > E:\test.txt
解析后可以看出来,对于第一第二种显式地添加版本号的类来说,能够明显找到serialVersionUID,而对于使用注解忽略或直接忽略的方式,则不能找到serialVersionUID。
不论三种中哪种处理警告的方式,在序列化到文件的这个过程中,此时类的版本号serialVersionUID必然已保存在了字节序列中。