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。