网上关于java对象序列化的博客文档很多,本文是我个人最近学习java对象序列化,查阅资料的一些总结,参考了许多博主的文章,后文有附,希望对大家有所帮助,如发现错误,或有任何不同见解,恳请指出!
Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java对象序列化就能够帮助我们实现该功能。
使用Java对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意的是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。除了在持久化对象时会用到对象序列化之外,当使用RMI(远程方法调用),或在网络中传递对象时,都会用到对象序列化。Java序列化API为处理对象序列化提供了一个标准机制,该API简单易用。
简而言之:
把对象转换为字节序列的过程称为对象的序列化。
把字节序列恢复为对象的过程称为对象的反序列化。
对象的序列化主要有两种用途:
1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
2) 在网络上传送对象的字节序列。
Serializable接口是一个空接口,它的主要作用就是标识该类的对象是可序列化的。在ObjectOutputStream的writeObject0(Object obj, boolean unshared)
方法中可以看到对被写对象类型的判断。若对象不可序列化,将抛出NotSerializableException。
因此,在java中,要想让一个类能够序列化,只需让该类实现Serializable接口即可。下面做一个简单的测试。
首先创建一个枚举类Gender,表示性别。
public enum Gender {
Male,Female
}
下面创建要序列化的Person类。包含三个字段name,age,gender。
public class Person implements Serializable{
private static final long serialVersionUID = 1L;
public static int id;
private String name;
private int age;
private Gender gender;
public Person() {
super();
System.out.println("none-arg constructor");
}
public Person(String name, int age, Gender gender, int id) {
super();
this.name = name;
this.age = age;
this.gender = gender;
Person.id = id;
System.out.println("args constructor");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Gender getGender() {
return gender;
}
public void setGender(Gender gender) {
this.gender = gender;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", Gender=" + gender + ", id=" + id + "]";
}
}
最后测试对象的序列化与反序列化
public class Test{
@Test
public void writeReadObject() throws IOException, ClassNotFoundException{
File file = new File("ObjectSerial.out");
Person p = new Person("张三", 20, Gender.Male, 2017);
System.out.println("序列化的对象:" + p);
//序列化将对象写入文件
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(p);
out.close();
//反序列化从文件中读对象
ObjectInput in = new ObjectInputStream(new FileInputStream(file));
Person obj = (Person) in.readObject();
in.close();
System.out.println("反序列化的对象:" + obj);
System.out.println(obj == p);
}
}
程序运行结果为:
args constructor
序列化的对象:Person [name=张三, age=20, Gender=Male, id=2017]
反序列化的对象:Person [name=张三, age=20, Gender=Male, id=2017]
false
由结果可知,通过序列化成功将对象写入文件,并反序列化从文件中读出了该对象。同时思考问题:
①基本类型、String类默认可以序列化的。枚举类默认继承类java.lang.Enum,而该类实现了Serializable接口,所以枚举类型对象都是默认可以被序列化的。因此,Person类的三个成员变量都是可以序列化,那么如果Person类含有其他引用类型的实例变量呢?————>分析见第二小节。
② obj == p 的值为 false 可知,反序列化出的对象与原对象并不是同一个对象(本地不同,实际应用涉及传输时肯定也不同),但是反序列化创建对象时却没有调用任何构造方法,那么是如何创建的?————>
③序列化不关注类变量,那么static的 id 为什么反序列化时获取到了呢?————>原因:该测试是在本地进行的(而且是同一线程),所以获取的是jvm加载好的类信息,如果是传到另一台机器或者关掉程序重新读入序列化的对象,id就是初始时的信息。
首先创建一个Car类,有两个成员变量brand,price。
public class Car {
private String brand;
private int price;
public Car() {
super();
}
public Car(String brand, int price) {
super();
this.brand = brand;
this.price = price;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
@Override
public String toString() {
return "Car [brand=" + brand + ", price=" + price + "]";
}
}
然后在Person类中加入一个Car类型的成员变量,同时修改构造器和toString。
public class Person implements Serializable{
...
private Car car;
...
}
测试序列化
public class Test{
@Test
public void writeReadObject() throws IOException, ClassNotFoundException{
File file = new File("ObjectSerial.out");
Person p = new Person("张三", 20, Gender.Male, 2017, new Car("BMW", 300000));
System.out.println("序列化的对象:" + p);
//序列化将对象写入文件
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(p);
out.close();
//反序列化从文件中读对象
ObjectInput in = new ObjectInputStream(new FileInputStream(file));
Person obj = (Person) in.readObject();
in.close();
System.out.println("反序列化的对象:" + obj);
}
}
执行结果:
args constructor
序列化的对象:Person [name=张三, age=20, Gender=Male, id=2017, car=Car [brand=BMW, price=300000]]
java.io.NotSerializableException: javalearn.Serialization.Car
可见,在创建Person对象后,程序进行序列化时,抛出了NotSerializableException,序列化失败,说明引用类型的实例变量必须也实现了Serializable接口,否则无法实例化。
仍使用上2.1小节中的Car类,同时修改Person类,增加成员变量List cars,同时修改构造器和toString。
public class Person implements Serializable{
...
private List cars;
...
}
测试序列化
public class Test{
@Test
public void writeReadObject() throws IOException, ClassNotFoundException{
File file = new File("ObjectSerial.out");
List cars = new ArrayList<>();
cars.add(new Car("BWM", 200000));
cars.add(new Car("Audi", 300000));
Person p = new Person("张三", 20, Gender.Male, 2017, cars);
System.out.println("序列化的对象:" + p);
//序列化将对象写入文件
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(p);
out.close();
//反序列化从文件中读对象
ObjectInput in = new ObjectInputStream(new FileInputStream(file));
Person obj = (Person) in.readObject();
in.close();
System.out.println("反序列化的对象:" + obj);
}
}
执行结果为:
args constructor
序列化的对象:Person [name=张三, age=20, Gender=Male, 2017, cars=[Car [brand=BWM, price=200000], Car [brand=Audi, price=300000]]]
java.io.NotSerializableException: javalearn.Serialization.Car
与2.1一样,出现了异常。原因是List容器变量中的实例未实现序列化。
总结:如果仅仅只是让某个类实现Serializable接口,而没有其它任何处理的话,则就是使用默认的序列化机制。使用默认机制,在序列化对象时,不仅会序列化当前对象本身,还会对该对象引用的其它对象也进行序列化,同样地,这些其它对象引用的另外对象也将被序列化,以此类推。所以,如果一个对象包含的成员变量是容器类对象,而这些容器所含有的元素也是容器类对象,那么这个序列化的过程就会较复杂,开销也较大。
那么在实际应用中,怎么样来改变默认的序列化机制,在过程中忽略掉敏感数据,或者简化序列化过程呢?java提供几种解决方法,详细见下节。
transient关键字是变量修饰符,如果用transient声明一个实例变量,当对象进行存储时,它的值不需要维持。换句话来说就是,用transient关键字标记的成员变量不参与序列化过程。当某个字段被声明为transient后,默认序列化机制就会忽略该字段。
将Person类的age属性声明为transient。
public class Person implements Serializable{
...
private transient int age;
...
}
再次执行Test.writeReadObject(),输出为:
args constructor
序列化的对象:Person [name=张三, age=20, Gender=Male]
反序列化的对象:Person [name=张三, age=0, Gender=Male]
可知,声明为transient的age属性未被序列化。那么,如果想让一个声明为transient的属性可以被序列化,除了删除transient关键,还有其他方法吗?答案是有的,见3.2节。
在需要序列化的类中定义writeObject( )和writeObject( )方法。
public class Person implements Serializable{
...
private transient int age;
...
private void writeObject(ObjectOutputStream out) throws IOException{
out.defaultWriteObject();
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException{
in.defaultReadObject();
age = in.readInt();
}
...
}
在writeObject()
方法中会先调用ObjectOutputStream中的defaultWriteObject()
方法,该方法会执行默认的序列化机制,如3.1节所述,此时会忽略掉age字段。然后再调用writeInt()
方法显示地将age字段写入到ObjectOutputStream中。readObject()
的作用则是针对对象的读取,其原理与writeObject()
方法相同。
再次执行Test.writeReadObject(),输出为:
args constructor
序列化的对象:Person [name=张三, age=20, Gender=Male]
反序列化的对象:Person [name=张三, age=20, Gender=Male]
可见,age属性又能够序列化了。
补充说明:
①方法writeObject处理对象的序列化。如果声明该方法,它将会被ObjectOutputStream调用而不是默认的序列化进程。尽管它们被外部类调用但事实上这是两个private的方法。并且它们既不存在于java.lang.Object,也没有在Serializable中声明。那么ObjectOutputStream如何使用它们的呢?ObjectOutputStream使用了反射来寻找是否声明了这两个方法。因为ObjectOutputStream使用getPrivateMethod,所以这些方法必须被声明为private。
②在两个方法的开始处,都调用了defaultWriteObject()和defaultReadObject()。它们做的是默认的序列化进程,来写/读所有的non-transient和 non-static字段(但他们不会去做serialVersionUID的检查),通常来说,所有我们想要自己处理的字段都应该声明为transient。这样的话,默认的序列化机制defaultWriteObject和defaultReadObject便可以专注于其余字段,而我们则可为这些特定的tranient字段进行定制序列化。使用那两个默认的方法并不是强制的,而是给予了处理复杂应用时更多的灵活性。
③write的顺序和read的顺序需要对应,譬如有多个字段都用wirteInt写入流中,那么readInt需要按照顺序将其赋值。
无论是使用transient关键字,还是使用writeObject()
和readObject()
方法,其实质都是基于Serializable接口的序列化。JDK中还提供了另一个序列化接口——Externalizable,使用该接口之后,之前基于Serializable接口的序列化机制就将失效。
Externalizable接口继承于Serializable,在Externalizable中声明了两个方法readExternal()
和writeExternal()
,子类必须实现二者。
public class Person implements Extenalizable{
private String name;
private transient int age;
private Gender gender;
public Person() {
super();
System.out.println("none-arg constructor");
}
public Person(String name, int age, Gender gender) {
super();
this.name = name;
this.age = age;
this.gender = gender;
System.out.println("args constructor");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Gender getGender() {
return gender;
}
public void setGender(Gender gender) {
this.gender = gender;
}
//保留Serializable的默认序列化机制
private void writeObject(ObjectOutputStream out) throws IOException{
out.defaultWriteObject();
}
//保留Serializable的默认序列化机制
private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException{
in.defaultReadObject();
}
//重写writeExternal
@Override
public void writeExternal(ObjectOutput out) throws IOException {
}
//重写readExternal
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", Gender=" + gender + "]";
}
}
执行Test.writeReadObject(),输出为:
args constructor
序列化的对象:Person [name=张三, age=20, Gender=Male]
none-arg constructor
反序列化的对象:Person [name=null, age=0, Gender=null]
从该结果,一方面可以看出Person对象中任何一个字段都没有被序列化,说明Serializable的默认序列化机制失效了。另一方面,还可以发现这次序列化过程调用了Person类的无参构造器。
Externalizable继承于Serializable,当使用该接口时,序列化的细节需要由程序员去完成。如上所示的代码,由于writeExternal()与readExternal()方法未作任何处理,那么该序列化行为将不会保存/读取任何一个字段。因此,输出结果中所有字段的值均为空。
另外,若使用Externalizable进行序列化,当读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。所以,在此次序列化过程中Person类的无参构造器会被调用。由于这个原因,实现Externalizable接口的类必须要提供一个无参的构造器,且它的访问权限为public,否则程序会抛出异常:java.io.InvalidClassException : no valid constructor。
对上述Person类作进一步的修改,使其能够对name,age和gender字段进行序列化,如下代码所示:
public class Person implements Extenalizable{
...
public Person() {
super();
System.out.println("none-arg constructor");
}
...
//重写writeExternal
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
out.writeObject(gender);
}
//重写readExternal
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
gender = (Gender) in.readObject();
}
...
}
再次执行Test.writeReadObject(),输出为:
args constructor
序列化的对象:Person [name=张三, age=20, Gender=Male]
none-arg constructor
反序列化的对象:Person [name=张三, age=20, Gender=Male]
补充说明:
①Serializable是内建支持的也就是直接implement即可,但Externalizable的实现类必须提供readExternal()
和writeExternal()
实现。对于Serializable来说,Java自己建立对象图和字段进行对象序列化,可能会占用更多空间。而Externalizable则完全需要程序员自己控制如何写/读,麻烦但可以有效控制序列化的存储的内容。
由于父类实现的接口,子类不用显式声明,自动实现该接口。因此,父类可以序列化,子类自然也可以序列化。
//父类
public class Animal implements Serializable{
public String name;
public int age;
public Animal() {
super();
System.out.println("none-arg Animal constructor");
}
public Animal(String name, int age) {
super();
this.name = name;
this.age = age;
System.out.println("arg Animal constructor");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Animal [name=" + name + ", age=" + age + "]";
}
}
//子类
public class Dog extends Animal{
private static final long serialVersionUID = -5498857812409621833L;
private String host;
public Dog() {
super();
System.out.println("none-arg Dog constructor");
}
public Dog(String name, int age, String host) {
super(name, age);
this.host = host;
System.out.println("arg Dog constructor");
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
@Override
public String toString() {
return "Dog [host=" + host + ", name=" + name + ", age=" + age + "]";
}
}
//测试序列化
public Test{
@Test
public void writeReadObject() throws IOException, ClassNotFoundException{
File file = new File("ObjectSerial.out");
Dog d = new Dog("black", 3, "张三");
System.out.println("序列化对象为:" + d);
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(d);
out.close();
ObjectInput in = new ObjectInputStream(new FileInputStream(file));
Dog obj = (Dog) in.readObject();
in.close();
System.out.println("反序列化对象为:" + obj);
}
}
输出结果为:
arg Animal constructor
arg Dog constructor
序列化对象为:Dog [host=wang, name=black, age=3]
反序列化对象为:Dog [host=wang, name=black, age=3]
可见子类可以序列化。
测试代码同上,改为让子类实现Serializable。
执行Test.writeReadObject(),输出结果为:
arg Animal constructor
arg Dog constructor
序列化对象为:Dog [host=wang, name=black, age=3]
none-arg Animal constructor
反序列化对象为:Dog [host=wang, name=null, age=0]
由输出可知,反序列化时调用了父类无参的构造函数,且从父类继承的属性无法序列化。
总结:
①父类实现序列化时,子类自然可以序列化。
②只有子类实现序列化时,子类对象可以进行序列化,但是会丢失从父类继承而来的属性,同时要求父类必须有一个无参的构造器(反序列化时需要,若父类没有无参构造器,可写不可读,有兴趣可以自己测试)。
serialVersionUID: 字面意思上是序列化的版本号,凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量。
serialVersionUID可以显式的在类中声明,若不显式声明,则由JVM根据类的内容自动生成一个serialVersionUID,类中的信息的变化会导致serialVersionUID的变化。
(测试发现类信息发生变化时,如增加删除修改成员变量、方法时,会导致serialVersionUID变化,无法识别原先序列化的对象。但是对方法内部的修改,并不会导致serialVersionUID变化,有兴趣可以自己测试)
因此,若一个类没有显式地指定serialVersionUID,而且类信息发生了变化,则读取类变化前存入磁盘中的对象时就会报错,抛出InvalidClassException。
若类信息已经修改较多或者修改成不兼容的模式,导致原来输出到磁盘的内容不应再转换至原对象,此时则应该修改serialVersionUID。
serialVersionUID是为了在序列化时为了保持版本的兼容性,即在版本升级时反序列化仍保持对象的唯一性。
serialVersionUID可以自己指定,也可以让IDE自动生成。在Eclipse中有两种生成方式:
一个是默认的1L,如:private static final long serialVersionUID = 1L;
一个是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,如:private static final long serialVersionUID = -5498857812409621833L。
两种方式并没有什么实质区别,对于第一种,需要了解哪些情况是可兼容的,哪些根本就不兼容。 参考文档:http://Java.sun.com/j2se/1.4/pdf/serial-spec.pdf
在可兼容的前提下,可以保留旧版本号,如果不兼容,或者想让它不兼容,就手工递增版本号。1L–>2L–>3L……
第二种方式,是根据类的结构产生的hash值。增减一个属性、方法等,都可能导致这个值产生变化。若不想继续向下兼容,只需删除原有serialVesionUid声明语句,再自动生成一下。
( 五、readResolve()方法)
参考文档:
http://blog.csdn.net/jediael_lu/article/details/26813153关于serialVesionUid的说明。
http://www.blogjava.net/jiangshachina/archive/2012/02/13/369898.html理解java对象序列化。
http://blog.csdn.net/simon_steve_sun/article/details/8439254
http://blog.csdn.net/u012554102/article/details/51902697
http://www.360doc.cn/article/573136_72970335.html