Java Object Serialization

1. Overview
  Java中的序列化就是将Java对象的状态转化为字节序列,以便存储和传输的机制,在未来的某个时间,可以通过字节序列重新构造对象。把Java对象转换为字节序列的过程称为对象的序列化。把字节序列恢复为Java对象的过程称为对象的反序列化。这一切都归功于java.io包下的ObjectInputStream和ObjectOutputStream这两个类。

2. Serializable
  要想实现序列化,类必须实现Serializable接口,这是一个标记接口,没有定义任何方法。如果一个类实现了Serializable接口,那么一旦这个类发布,“改变这个类的实现”的灵活性将大大降低。以下是一个序列化的小例子:
class Message implements Serializable{

	private static final long serialVersionUID = 1L;
	
	private String id;
	
	private String content;
	
	public Message(String id, String content){
		this.id = id;
		this.content = content;
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public String getContent() {
		return content;
	}

	public void setContent(String content) {
		this.content = content;
	}
	
	public String toString(){
		return "id = " + id + " content = " + content;
	}
}

public class Test{
	
	public static void main(String[] args) {
		serialize();
		deserialize();
	}
	
	private static void serialize(){
		Message message = new Message("1", "serializable test");
		try {
			ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Message"));
			oos.writeObject(message);
			oos.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		System.out.println("over");
	}
	
	private static void deserialize(){
		try {
			ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Message"));
			Message message = (Message)ois.readObject();
			System.out.println(message.toString());
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
	
}

  需要注意,序列化机制只保存对象的类型信息,属性的类型以及属性值,与方法没有关系。对于静态的变量,序列化机制也是不保存的。因为,静态变量属于类变量,而不是对象变量。而且,并不是所有的Java对象都可以被序列化,例如:Thread,Socket。内部类很少甚至没有实现Serializable接口的。关于容器类的序列化,可以遵循Hashtable的实现方式,即存储键和值的形式,而非一个大的哈希表的数据结构类型。

3. Serial Version UID
  每一个可序列化的类都有一个与之关联的唯一的序列化版本UID。其有两种生成策略,一种是固定的1L(private static final long serialVersionUID = 1L),另一种是依据类名、它实现的接口的名字、以及所有公共和受保护的成员的名字,利用JDK提供的工具随机的生成一个不重复的long类型数据。因此,假如你在已经序列化的类中,添加了新方法或属性,其随机UID的值也许会改变,如果此时在反序列化时,将会抛出java.io.InvalidClassException。因此建议,如果没有特殊需求,就是用默认的 1L 就可以,这样可以确保代码一致时反序列化成功。

4. Serialization Storage Rules
  我们修改上面列子的代码,将message对象写入Message.obj文件中两遍,然后我们查看文件大小,以及从文件中反序列化出两个对象,比较是否相等,代码如下:
public class Test{
	
	public static void main(String[] args) {
		try {
			Message message = new Message("1", "serializable test");
			ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Message.obj"));
			oos.writeObject(message);
			oos.flush();
			System.out.println(new File("Message.obj").length());
			oos.writeObject(message);
			System.out.println(new File("Message.obj").length());
			
			//
			ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Message.obj"));
			Message message1 = (Message)ois.readObject();
			Message message2 = (Message)ois.readObject();
			System.out.println(message1 == message2);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
}

  输出结果:
115
120
true

  从结果,我们可以看出,对相同对象的第二次序列化后,文件的大小只增加了5字节,且反序列化出来的两个对象相等。原因在于,Java序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的5字节的存储空间就是新增引用和一些控制信息的空间。反序列化时,恢复引用关系,使得message1和message2指向唯一的对象,二者相等,输出 true。该存储规则极大的节省了存储空间。
  我们修改上面main方法中的代码,使第一次序列化id值为2,第二次序列化id值为3,然后反序列化出两个对象,并打印id值:
public static void main(String[] args) {
		try {
			Message message = new Message("1", "serializable test");
			ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Message.obj"));
			message.setId("2");
			oos.writeObject(message);
			oos.flush();
			System.out.println(new File("Message.obj").length());
                        message.setId("3");
			oos.writeObject(message);
			System.out.println(new File("Message.obj").length());
			
			//
			ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Message.obj"));
			Message message1 = (Message)ois.readObject();
			Message message2 = (Message)ois.readObject();
			System.out.println(message1 == message2);
			System.out.println("message1.id = " + message1.getId());
			System.out.println("message2.id = " + message2.getId());
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}

  输出结果:
115
120
true
message1.id = 2
message2.id = 2

  结果两次输出,id值都为2。原因就是第一次写入对象后,第二次再试图写的时候,JVM根据引用关系知道已经有一个相同对象已经写入文件,因此只保存第二次写的引用。所以读取时都是第一次保存的对象。因此,我们必须要注意,在同一个对象进行多次序列化到相同文件中时所产生的这个问题。
  
5. Serialization and Inheritance
  假设有如下情形: 一个父类实现了Serializable接口,子类继承父类同时也实现了Serializable接口。那么在反序列化的时候,结果如何呢?请看下面这个小例子:
public class Base implements Serializable{

	private static final long serialVersionUID = 1L;
	
	private int x;
	
	public Base(){
		System.out.println("Base Class : no-arg constructor");
	}
	
	public Base(int x){
		this.x = x;
		System.out.println("Base Class : one-arg constructor");
	}
	
	public int getX(){
		return x;
	}
	
	public String toString(){
		return "x = " + this.x;
	}
}

public class Child extends Base implements Serializable{

	private static final long serialVersionUID = 1L;
	
	private int y;
	
	public Child(){
		System.out.println("Child Class : default no-arg constructor");
	}
	
	public Child(int x, int y){
		super(x);
		this.y = y;
		System.out.println("Child Class : two-args constructor");
	}
	
	public String toString(){
		return "x = " + getX() + " , y = " + this.y;
	}
	
	public static void main(String[] args) {
		try {
			Child child =  new Child(1, 2);
			ObjectOutputStream oos =  new ObjectOutputStream(new FileOutputStream("BaseAndChild"));
			oos.writeObject(child);
			oos.close();
			System.out.println("over");
			
			//
			ObjectInputStream ois = new ObjectInputStream(new FileInputStream("BaseAndChild"));
			Child child2 = (Child)ois.readObject();
			ois.close();
			System.out.println(child2);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
}

  输出结果为:
Base Class : one-arg constructor
Child Class : two-args constructor
over
x = 1 , y = 2

  从输出的结果,我们可以看出,当父子类同时实现Serializable接口,反序列化时,不调用构造函数,且子类中的x,y值都被正确的设置。当父类没有实现Serializable接口时,输出结果变为:
Base Class : two-args constructor
Child Class : one-arg constructor
over
Base Class : no-arg constructor
x = 0 , y = 2

  说明,在反序列化时,调用了父类的无参构造函数,且子类从父类继承的x也没有被正确的设置。我们可以这样理解,一个Java对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值,所以我们x的取值为0。如果在反序列化时,父类没有提供可访问的,无参的构造函数,将抛出java.io.InvalidClassException异常。

6. Serialization and Proxy
  序列化允许将代理放在流中。看下面这个列子:
public class Product implements Serializable {

	private static final long serialVersionUID = 1L;
	
	private String description;
	
	public Product(String description){
		this.description = description;
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}
}

public class ProductProxyFactory implements Serializable, MethodInterceptor{

	private static final long serialVersionUID = 1L;

	public Object intercept(Object obj, Method m, Object[] args,
			MethodProxy mp) throws Throwable {
		
		System.out.println("before interception");
		try {
			return mp.invokeSuper(obj, args);
		} finally {
			System.out.println("after interception");
		}
	}
	
	public static Product getProxy(String description) throws Exception{
		Enhancer enhancer = new Enhancer();
		enhancer.setSuperclass(Product.class);
		enhancer.setCallback(new ProductProxyFactory());
		return (Product)enhancer.create(new Class[]{String.class}, new Object[]{description});
	}
}

public class Serialization {
	
	public static void main(String[] args) throws Exception {
		Product p = ProductProxyFactory.getProxy("first product");
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Product"));
		oos.writeObject(p);
		oos.flush();
		oos.close();
		System.out.println("over");
	}
}

public class Deserialization {
	
	public static void main(String[] args) throws Exception {
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Product"));
		Product p1 = (Product)ois.readObject();
		ois.close();
		System.out.println("deserialize: " + p1.getDescription());
	}
}

先运行Serialization类,然后运行Deserialization类(也就是说在不同的JVM间序列化/反序列化用cglib生成的代理对象),会抛出java.lang.ClassNotFoundException。原因在于,使用cglib生成的代理类是Product子类,而这些子类在不同的JVM间是不同的,因此抛出异常。解决方法是在Product类中加入以下两个方法:
public Object writeReplace() throws ObjectStreamException {
	return new Product(getDescription());
}
public Object readResolve() throws ObjectStreamException {
	return ProductProxyFactory.getProxy(getDescription());
}

  在对代理类进行序列化时,父类(Product类)中的writeReplace()方法会被调用。writeReplace()方法返回的是个Product类的对象,因此实际序列化的对象不是代理类的对象;在对代理类进行反序列化时,父类(Product类)中的readResolve方法会被调用。readResolve方法返回了个新的代理类的对象。
需要注意的是,使用Java 动态代理(Dynamic Proxy)创建代理类有特殊的类标识符,因此在ObjectInputStream对其进行反序列化时,会识别这个特殊的标识符,并调用ObjectInputStream.resolveProxyClass方法对其进行处理,因此会自动返回一个新的代理类的对象,也就是说如果使用动态代理创建代理类,那么不必添加writeReplace和readResolve方法。在另一方面,使用cglib创建的代理类只有普通的类标识符,ObjectInputStream对其进行反序列化时只是调用ObjectInputStream.resolveClass方法对其进行处理,因此需要以上的技巧。
  如果一个类同时改写了这两个方法,以及writeObject()和readObject()方法,那么在序列化时的调用顺序是:
1.writeReplace()
2.writeObject()
3.readObject()
4.readResolve()

7. Externalizable
  Externalizable接口继承自Serializable接口,实现Externalizable接口的类进行序列化时,只保存对象的标识符信息,因此序列化的速度更快,序列化后的字节流更小。Externalizable接口的定义如下:
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

  子类必须提供读取和写出方法的实现。在序列化的时候,通过writerExternal方法序列化对象,通过readExternal方法反序列化一个对象。跟实现了Serializable接口的类不同,在反序列化时,会调用类的无参构造函数,所以该实现类必须提供一个可访问的无参构造函数。当一个类同时实现了Externalizable接口和Serializable接口时,那么实际的序列化形式是由Externalizable接口中声明的方法决定。以下是一个列子:
public class Person implements Externalizable {
	
	private static final long serialVersionUID = 1L;

	private int age;
	
	public Person(){
	}
	
	public Person(int age){
		this.age = age;
	}
	
	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
	
	public String toString(){
		return "age = " + this.age;
	}
	
	public void readExternal(ObjectInput in) throws IOException,
			ClassNotFoundException {
		setAge(in.readInt());

	}

	public void writeExternal(ObjectOutput out) throws IOException {
		out.writeInt(getAge());
	}

	public static void main(String[] args) {
		try {
			Person p = new Person(20);
			ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Person.obj"));
			oos.writeObject(p);
			oos.close();
			System.out.println("over");
			
			//
			ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Person.obj"));
			Person p1 = (Person)ois.readObject();
			System.out.println(p1);
			ois.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

8. Transient关键字
  Transient关键字的作用就是控制变量的序列化,在变量声明前加上transient关键字,可以阻止变量被序列化到文件中,在被反序列化后,transient关键字修饰的变量的值被设置为初始值,如int型为0,对象型为null。以下是一个例子:
public class Person implements Externalizable {
	
	private static final long serialVersionUID = 1L;

	private int age;
	
	private transient String name;
	
	public Person(){
	}
	
	public Person(int age, String name){
		this.age = age;
		this.name = name;
	}
	
	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
	
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String toString(){
		return "age = " + this.age + " , name = " + this.name;
	}
	
	public void readExternal(ObjectInput in) throws IOException,
			ClassNotFoundException {
		setAge(in.readInt());
	}

	public void writeExternal(ObjectOutput out) throws IOException {
		out.writeInt(getAge());
	}

	public static void main(String[] args) {
		try {
			Person p = new Person(20, "remy");
			ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Person.obj"));
			oos.writeObject(p);
			oos.close();
			System.out.println("over");
			
			//
			ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Person.obj"));
			Person p1 = (Person)ois.readObject();
			System.out.println(p1);
			ois.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

  输出结果:
over
age = 20 , name = null

9. ObjectInputValidation
  我们可以使用ObjectInputValidation接口验证序列化流中的数据是否与最初写到流中的数据一致。我们需要覆盖validateObject()方法,如果调用该方法时发现某处有错误,则抛出一个 InvalidObjectException。

猜你喜欢

转载自technoboy.iteye.com/blog/1068200
今日推荐