对象序列化
对象序列化的含义和意义
对象序列化的目标是将对象保存到磁盘中,或允许在网络中直接传输对象。兑现序列化机制允许把内存中的JAVA对象转换成平台无关的二进制流,从而把这种二进制流永久地保存在磁盘上。通过网络可以将这种二进制流传输到另一个网络节点。其它程序一旦获得了这种二进制流,都可以讲这种二进制流回复成原来的JAVA对象。
序列化机制使得对象可以脱离程序的运行单独存在。
对象的序列化是指将一个JAVA对象写入IO流,其反序列化则指从IO流中恢复该对象。
如果想让某个对象支持序列化机制,则必须让它的类是可序列化的,也就是说这个类必须实现Serializable或是Externalizable接口中的一个。
其中Serializable接口是一个标记接口,实现该接口无需实现任何抽象方法,它只是表面这个类是可序列化的。
所有可能在网络上传输的对象都应该是可序列化的,否则程序将会出现异常。
使用对象流实现序列化
一旦某个类实现了Serializable接口,那么就可以通过以下两步实现序列化:
1.创建一个ObjectOutputStream,该输出流是一个处理流,所以必须建立在其它节点流的基础上
2.调用ObjectOutputStream对象的writeObject()方法输出可序列化对象
举个例子
package 对象序列化;
public class Person implements java.io.Serializable{
private String name;
private int age;
public Person(String name,int age)
{
this.name=name;
this.age=age;
}
public void setName(String name)
{
this.name=name;
}
public String getName()
{
return this.name;
}
public void setAge(int age)
{
this.age=age;
}
public int getAge()
{
return this.age ;
}
}
package 对象序列化;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class WriteObject {
public static void main(String []args)
{
try(ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("object.txt")))
{
Person p=new Person("孙悟空",500);
oos.writeObject(p);
}
catch(IOException e)
{
e.printStackTrace();
}
}
}
如果需要从二进制流中恢复JAVA对象,则需要使用反序列化。反序列化的步骤如下:
1.创建一个ObjectInputStream
2.调用ObjectInputStream对象的readObject()方法读取流中的对象,该方法返回了一个Object型对象,如果知道该对象的类型,可以使用强制类型转换,将其转换为真实的类型。
package 对象序列化;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
public class ReadObject {
public static void main(String []args)
{
try(ObjectInputStream ois=new ObjectInputStream(new FileInputStream("object.txt")))
{
Person p=(Person)ois.readObject();
System.out.println("名字 "+p.getName()+" 年龄 "+p.getAge());
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
输出结果为:
反序列化读取的仅仅是JAVA对象,而不是JAVA类,因此采用反序列化恢复JAVA对象时,必须给出该JAVA对象所属类的class文件,否则将引发ClassNotFoundException异常。
如果使用序列化机制向文件中写入多个JAVA对象,使用反序列化机制恢复对象时必须按照实际写入的顺序读取。
当一个可序列化类有多个父类的时候(包括直接和间接父类),这些父类要么有无参构造器,要么也是可以序列化的——否则反序列化时会排出InvalidClassException。如果父类只是带有无参构造器而不是可序列化的,那么该父类中定义的成员变量值不会序列化到二进制中。
对象引用的序列化
如果某个类的成员变量的类型不是基本类型或String类型,而是另一个引用类型,那么这个引用类必须是可序列化的,否则拥有该类型成员变量的类也是不可序列化的。
为了避免当一个对象作为多个类的成员变量在反序列化后被认为是多个不同的对象,JAVA序列化机制采取了一种特殊的算法:
所有保留在磁盘里的对象都有一个序列化编号
当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有未被序列化,才会将其转化成字节序列并输出。
如果某个对象已经被序列化过,程序将至少直接输出一个序列化编号,而不是重新序列化该对象。
也就是说,当多次序列化同一个JAVA对象时,只有第一次序列化才会把该JAVA对象转化成字节序列并输出,这就引发了一个问题——当程序序列化一个可变对象时,只有第一次使用writeObject()方法输出时才会把对象转化为字节码序列并输出,之后即使该可变对象已改变,再调用writeObject()方法程序只是输出序列化编号,并不会再次输出实例。
自定义序列化
当某个对象进行序列化时,系统会自动把该对象的所有实例变量一次进行序列化,如果某个实例变量引用到另一个对象,则被引用的对象也会被序列化;如果被引用的对象的实例变量也引用了其它对象,那么依旧会被序列化,这就是所谓的递归序列化。
通过在实例变量前面使用transient关键字修饰,可以知道Java序列化时无需理会该实例变量。
不过这会导致某些副作用,比如:
package 自定义序列化;
public class Person implements java.io.Serializable{
private String name;
private transient int age;
public Person(String name,int age)
{
this.name=name;
this.age=age;
}
public void setName(String name)
{
this.name=name;
}
public String getName()
{
return this.name;
}
public void setAge(int age)
{
this.age=age;
}
public int getAge()
{
return this.age ;
}
}
package 自定义序列化;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class TransientTest {
public static void main(String []args)
{
try
(ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("transient.txt"));
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("transient.txt")))
{
Person p=new Person("孙悟空",500);
oos.writeObject(p);
Person ps=(Person)ois.readObject();
System.out.println("姓名 "+ps.getName()+" 年龄 "+ps.getAge());
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
结果如下:
如图所示,由于age变量被transient关键字修饰,所以实际输出为0.
使用transient关键字修饰实例变量虽然简单,但被transient修饰的实例变量将被完全隔离在序列化机制之外,这样导致在反序列化恢复Java对象时无法取得该实例变量的值。
在序列化和反序列化的过程中,可以通过重写writeObject和readObject方法来实现对序列化机制的完全控制。(P693)
在默认条件下,该writeObject()方法会调用out.defaultWriteObject来保存JAVA对象的非瞬态实例变量。
而readObject()方法会调用in.defaultReadObject来恢复JAVA对象的非瞬态实例变量。
当序列化流不完整时(接收方使用的反序列化类的版本不同于发送方,或序列化流被篡改等),readObjectNoData()方法可以用来正确地初始化反序列化的对象。
还有一种更为彻底的自定义机制,它甚至可以在序列化该对象时将该对象替换成其它对象,如果需要替换,那个要重写Object writeReplace()throws ObjectStreamException方法。
package 自定义序列化替换;
import java.io.ObjectStreamException;
import java.util.ArrayList;
public class Person implements java.io.Serializable{
private String name;
private int age;
public Person(String name,int age)
{
this.name=name;
this.age=age;
}
public void setName(String name)
{
this.name=name;
}
public String getName()
{
return this.name;
}
public void setAge(int age)
{
this.age=age;
}
public int getAge()
{
return this.age ;
}
private Object writeReplace()throws ObjectStreamException
{
ArrayList<Object> list=new ArrayList<Object>();
list.add(name);
list.add(age);
return list;
}
}
package 自定义序列化替换;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
public class ReplaceTest {
public static void main(String []args)
{
try(
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("replace.txt"));
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("replace.txt")))
{
Person p=new Person("孙悟空",500);
oos.writeObject(p);
ArrayList l=(ArrayList)ois.readObject();
System.out.println(l);
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
输出结果为:
可以看到,确实是以List的形式输出的
在系统序列化某个对象之前,会先调用该对象的writeReplace()方法和writeObject()方法。系统总会先调用writeReplace()方法,如果该方法返回另一个对象,那么再调用另一个对象的writeReplace()方法。。。直到不返回另一个对象,程序会调用该对象的writeObject()方法来保存该对象的状态
与writeReplace()方法相对,序列化机制中还有一个特殊的方法,它可以实现保护性复制整个对象。
Object readResolve()throws ObjectStreamException
该方法在序列化单例类、枚举类时特别有用
如果父类包含一个protected或public的readResolve()方法且子类没有重写该方法,将会使子类反序列化时得到一个父类的对象,这显然不正确,且这种错误河南发现,
如果父类包含一个protected或public的readResolve()方法且子类没有重写该方法,将会使子类反序列化时得到一个父类的对象,这显然不正确,且这种错误河南发现,
对于final类重写readResolve()方法不会有任何问题,否则,重写readResolve()方法时应尽量使用private修饰。
另一种自定义序列化机制
P697
通过继承Externalizable接口,该接口定义了如下两个方法
void readExteranl(ObjectInput in):需要序列化的类实现readExternal()方法来实现反序列化
void writeExternal(ObjectOutput out):需要序列化的类实现writeExternal()方法来保存对象的现状
当使用Externalizable机制反序列化对象时,程序会先使用public的无参数构造器创建实例,然后才执行readExternal()方法进行反序列化,因此实现Externalizable的序列化必须实现public的无参构造器。
虽然实现Externalizable接口可以带来性能上的提升,但是这样会导致编程复杂度增加,所有大部分都是采用实现Serializable接口的方式来实现序列化。
关于对象序列化,需要注意以下几点:
对象的类名、实例变量(包括基本类型、数组、对其它对象的引用)都会被序列化;方法、类变量、transient实例变量(也成为瞬态实例变量)都不会被序列化。
实现Serializable接口的类如果需要让某个实例变量不被序列化,则可在该实例变量前加transient,而不是static。虽然static也能达到相同效果,但static不是这么用的。
要保证序列化对象的实例变量也是可序列化的,否则要用transient关键字来修饰该实例变量。否则该对象不可序列化。
反序列化对象时必须有序列化对象的class文件。
当文件、网络来读取序列化后的对象时,必须按实际写入的顺序读写。
如果修改类时修改了非瞬态的实例变量,则可能导致序列化版本不见人。如果对象流中的对象和新类中包含同名的实例变量,而实例变量类型不同,则反序列化失败。