序列化与反序列化
一、对象与序列化
Java对象在创建后,只要程序还需要,它就会存在,但随着程序的终结,它一定会消失。Java的对象序列化是将实现了Serializable接口的对象转化为一个字节序列,并在以后这个字节序列能完全恢复成原来的对象。这里Serializable只是一个标记接口,其中并没有方法,若一个类没有实现Serializable接口,不是枚举、数组或字符串类型,则它的对象不能被序列化。
1 if (obj instanceof String) { 2 writeString((String) obj, unshared); 3 } else if (cl.isArray()) { 4 writeArray(obj, desc, unshared); 5 } else if (obj instanceof Enum) { 6 writeEnum((Enum<?>) obj, desc, unshared); 7 } else if (obj instanceof Serializable) { 8 writeOrdinaryObject(obj, desc, unshared); 9 } else { 10 if (extendedDebugInfo) { 11 throw new NotSerializableException( 12 cl.getName() + "\n" + debugInfoStack.toString()); 13 } else { 14 throw new NotSerializableException(cl.getName()); 15 } 16 }
序列化不仅仅实现了对对象的持久化,还突破了网络、操作系统限制。比如在windows系统上创建的一个对象A,序列化后通过网络在另一台Linux系统计算机也能重新组装使用,而不用担心数据在不同系统上的表示不同。这个过程需要注意的是,在反序列化过程中,打开和读取A对象时,必须保证虚拟机能找到相关的.class文件,如果找不到A对应的Class对象,就会抛出ClassNotFoundException异常。
二、序列化使用
序列化一个对象,先要创建一个OutputStream对象,将其封装在一个ObjectOutputStream中,然后调用writeObject( )方法即可将对象序列化;反序列化就需要用到InputStream和ObjectInputStream。
1 public class Person implements Serializable{ 2 private String name; 3 private static String ID = "0001"; 4 private transient int age; 5 private int weight; 6 private Pet pet; 7 8 public Person(){ 9 System.out.println("我是父类默认构造"); 10 } 11 12 public Person(String name, int age, int weight,Pet pet){ 13 this.name = name; 14 this.age = age; 15 this.weight = weight; 16 this.pet = pet; 17 } 18 19 @Override 20 public String toString() { 21 return "Person("+name+", "+ID+", "+age+", "+weight+", "+pet+")"; 22 } 23 } 24
1 public class Pet implements Serializable { 2 private String name; 3 private int age; 4 5 public Pet(String name, int age) { 6 this.name = name; 7 this.age = age; 8 } 9 10 @Override 11 public String toString() { 12 return "pet("+name+", "+age+")"; 13 } 14 }
1 public class TestObjectStream { 2 public static void main(String[] args) throws IOException, ClassNotFoundException { 3 File file = new File(".\\file\\person.txt"); 4 5 Pet pet = new Pet("小白",2); 6 Person person = new Person("Aidan", 22, 62,pet); 7 8 write(file,person); 9 System.out.println(person); 10 11 Object o = read(file); 12 System.out.println((Person)o); 13 } 14 15 private static void write(File file, Object o) throws IOException { 16 ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(file)); 17 os.writeObject(o); 18 os.close(); 19 } 20 21 private static Object read(File file) throws IOException, ClassNotFoundException { 22 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); 23 Object o = ois.readObject(); 24 ois.close(); 25 return o; 26 } 27 } 28 29 //Person(Aidan, 0001, 22, 62, pet(小白, 2)) 30 //Person(Aidan, 0001, 0, 62, pet(小白, 2)) 31
值得注意的是,序列化过程保存了对象的全部字段,包括对象中的引用字段,如果这个引用字段中也有引用字段,同样会被追踪保存,其中引用字段也必须实现Serializable接口。示例中people对象中有引用字段pet,在序列化过程中,pet对象也被保存下来,pet也需要实现Serializable接口。
另外在Person中,有两个字段被transient和static修饰,transient表示该字段是瞬态的字段,而被这两个关键字修饰的字段是不会被序列化的,反序列化得到的值是空(代码打印结果,static修饰的字段未被序列化,反序列化后的该字段值是在常量池中得到的,并不是序列化保存的)。因此,如对象中可能存在如密码等敏感性信息,或者不希望被自动序列化的字段可以用transient修饰。
2.1 对象引用问题
如果两个对象都具有对第三个对象的引用,那么反序列化得到的对象,第三个对象的引用是同一个吗?
1 //对以上代码测试 2 public static void main(String[] args) throws IOException, ClassNotFoundException { 3 ArrayList<Person> list = new ArrayList<>(); 4 Pet pet = new Pet("小白",2); 5 Person person = new Person("Aidan", 22, 62,pet); 6 Person person1 = new Person("chen",18,66,pet); 7 list.add(person); 8 list.add(person1); 9 10 ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(file)); 11 ObjectOutputStream os1 = new ObjectOutputStream(new FileOutputStream(file1)); 12 os.writeObject(list); 13 os.writeObject(list); 14 os1.writeObject(list); 15 16 ObjectInputStream in = new ObjectInputStream(new FileInputStream(file)); 17 ObjectInputStream in1 = new ObjectInputStream(new FileInputStream(file1)); 18 19 Object o1 = in.readObject(); 20 Object o2 = in.readObject(); 21 Object o3 = in1.readObject(); 22 23 System.out.println((ArrayList<Person>)o1); 24 System.out.println((ArrayList<Person>)o2); 25 System.out.println((ArrayList<Person>)o3); 26 } 27 28 //[person = 5300014 pet = day3.Pet@181eb93, person = 9552271 pet = day3.Pet@181eb93] 29 //[person = 5300014 pet = day3.Pet@181eb93, person = 9552271 pet = day3.Pet@181eb93] 30 //[person = 10634667 pet = day3.Pet@c355be, person = 9237702 pet = day3.Pet@c355be]
反序列化得到的是一个新的对象,示例中两个person对象对pet有同一个引用,我们用同一流os将保存两对象的list写入两次,再用另一个流os1写入一次。反序列化后,两个对象对pet对象的引用仍然相同,并且同一流中的o1和o2地址相同。而o3恢复时,系统无法知道另一个流内的对象是第一个流内的对象的别名,因此产生了一个新的对象。整个过程可以描述为:
- 对你遇到的每一个对象引用都关联一个序列号。
- 对于每个对象,当第一次遇到时,保存其对象数据到流中。
- 如果某个对象之前已经被保存过,那么只写出”与之前保存过的序列号为x的对象相同“。在读回对象时,整个过程是反过来的。
- 对于流中的对象,在第一次遇到其序列号时,构建它,并使用流中数据来初始化它,然后记录这个顺序号和新对象之间的关联。
- 当遇到”与之前保存过的序列号为x的对象相同“标记时,获取与这个顺序号相关联的对象引用。
三、Externalizable实现序列化控制
对于某些有字段被transient、static修饰的类,或需要进行对类中的数据进行安全校验等需求,则默认的序列化机制显然不能实现,这时可以用到Externalizable接口,它继承Serializable接口,添加了两个方法:writeExternal( )和readExternal( )方法,这两个方法会在序列化和反序列化过程中被自动调用
1 public class Mountain implements Externalizable { 2 private String name; 3 private transient int height; 4 private int width; 5 private static String location = "陕西"; 6 7 public Mountain(String name, int height, int width) { 8 this.name = name; 9 this.height = height; 10 this.width = width; 11 } 12 13 public Mountain(){ 14 System.out.println("我是默认构造器"); 15 } 16 17 @Override 18 public void writeExternal(ObjectOutput out) throws IOException { 19 out.writeObject(name); 20 out.writeInt(height); 21 out.writeInt(width); 22 out.writeObject(location); 23 } 24 25 @Override 26 public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { 27 name = (String)in.readObject(); 28 height = in.readInt(); 29 int w = in.readInt(); 30 location = (String)in.readObject(); 31 if (w>1000) 32 width = w; 33 else 34 try { 35 throw new Exception("error data"); 36 } catch (Exception e) { 37 e.printStackTrace(); 38 } 39 } 40 41 @Override 42 public String toString() { 43 return "Mountain("+name+", "+height+", "+width+", "+location+")"; 44 } 45 }
1 public class SerializeTest { 2 public static void main(String[] args) throws IOException, ClassNotFoundException { 3 //测试实现Externalizable 4 Mountain mountain = new Mountain("华山",8000 ,3000); 5 6 ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(".\\file\\Mountain.txt")); 7 os.writeObject(mountain); 8 os.close(); 9 ObjectInputStream is = new ObjectInputStream(new FileInputStream(".\\file\\Mountain.txt")); 10 Object o = is.readObject(); 11 is.close(); 12 System.out.println((Mountain)o); 13 } 14 } 15 //结果: 16 //我是默认构造器 17 //Mountain(华山, 8000, 3000, 陕西) 18 19 //若反序列化时不按照序列化顺序读取,如交互width和height,则两者值被交换
需要注意的是当我们注释掉类中的默认构造参数,则会抛出InvalidClassException异常。恢复实现Serializable的对象时,对象完全以它存储的二进制位为基础来构造,不调用构造器;而Externalizable对象,普通的默认构造器会被调用,然后在调用readExternal( )方法,才会使Externalize对象产生正确的行为。同时对于显示的控制序列化过程要注意,通过文件网络读取序列化对象的时候,必需按写入的顺序来读取。
对于需要特殊处理的类,除了通过实现Externalizable接口显示的序列化控制,还可以通过使用准确签名的特殊方法。
四、序列化中特殊签名的方法
- private void writeObject(java.io.ObjectOutputStream out)
throws IOException; - private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException;
这两个方法不是Serializable的方法,在调用ObjectOutputStream.writeObject( )时,会检测Serializable对象,如果对象中实现了它自己的writeObject( )方法,就会跳过默认序列化过程,调用它自己的方法,readObject( )方法同样如此。在方法中,可以通过调用out.defaultWriteObject ( )方法执行默认的序列化机制。与实现Externalizable接口不同,该方法本身不需要涉及属于其超类或子类的状态,也就是它的序列化过程不需要调用构造器;相同的一点是,序列化和反序列化时,写读的字段顺序要保持一致。
1 public class Animal implements Serializable { 2 private String name; 3 private String type; 4 private transient int life; 5 private static String location = "sea"; 6 7 public Animal(){ 8 System.out.println("我是默认构造器"); 9 } 10 11 public Animal(String name, String type, int life) { 12 this.name = name; 13 this.type = type; 14 this.life = life; 15 } 16 17 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { 18 in.defaultReadObject(); 19 life = in.readInt(); 20 location = (String)in.readObject(); 21 } 22 23 private void writeObject(ObjectOutputStream out) throws IOException { 24 out.defaultWriteObject(); 25 out.writeInt(life); 26 out.writeObject(location); 27 } 28 29 @Override 30 public String toString() { 31 return "Animal("+name+", "+type+", "+life+", "+location+")"; 32 } 33 } 34
1 public class SerializeTest { 2 public static void main(String[] args) throws IOException, ClassNotFoundException { 3 //测试具有特定签名方法的实现Serializable接口 4 Animal animal = new Animal("Tiger", "Cats", 20); 5 ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(".\\file\\Animal.txt")); 6 os.writeObject(animal); 7 os.close(); 8 9 ObjectInputStream is = new ObjectInputStream(new FileInputStream(".\\file\\Animal.txt")); 10 Object o = is.readObject(); 11 is.close(); 12 System.out.println((Animal)o); 13 } 14 } 15 16 //Animal(Tiger, Cats, 20, sea)
- private void readObjectNoData()
throws ObjectStreamException;
在序列化流不列出给定类作为将被反序列化对象的超类的情况下,readObjectNoData 方法负责初始化特定类的对象状态。这在接收方使用的反序列化实例类的版本不同于发送方,并且接收者版本扩展的类不是发送者版本扩展的类时发生。在序列化流已经被篡改时也将发生;因此,不管源流是“敌意的”还是不完整的,readObjectNoData 方法都可以用来正确地初始化反序列化的对象。
- ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
- ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
writeReplace( )将对象写入流时需要指定要使用的替代对象的可序列化类,即若实现了该方法,序列化时会序列化该方法返回的对象,序列化机制也会调用该返回对象实现的序列化机制;readResolve( )在从流中读取类的一个实例时需要指定替代的类,即若实现了该方法,它会返回一个对象替代反序列化重建的对象。
1 //在Animal方法中实现此方法 2 public Object writeReplace() throws ObjectStreamException { 3 return new Mountain("华山",2000,3000); 4 } 5 6 public class SerializeTest { 7 public static void main(String[] args) throws IOException, ClassNotFoundException { 8 Animal animal = new Animal("Tiger", "Cats", 20); 9 ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(".\\file\\Animal.txt")); 10 os.writeObject(animal); 11 os.close(); 12 13 ObjectInputStream is = new ObjectInputStream(new FileInputStream(".\\file\\Animal.txt")); 14 Object o = is.readObject(); 15 is.close(); 16 System.out.println((Mountain)o); 17 } 18 } 19 20 //我是默认构造器 21 //Mountain(华山, 2000, 3000, 陕西) 22 23 //结果调用了Mountain对象中的序列化机制
4.1 单例模式中的序列化
1 public class Singleton implements Serializable { 2 private Singleton(){ 3 4 } 5 6 private static class Holder{ 7 private static final Singleton INSTANCE = new Singleton(); 8 } 9 10 public static Singleton getInstance(){ 11 return Holder.INSTANCE; 12 } 13 14 /*private Object readResolve() throws ObjectStreamException { 15 return getInstance(); 16 }*/ 17 }
1 public class Test { 2 public static void main(String[] args) throws IOException, ClassNotFoundException { 3 Singleton singleton = Singleton.getInstance(); 4 ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(".\\file\\singleton.txt")); 5 os.writeObject(singleton); 6 os.close(); 7 8 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(".\\file\\singleton.txt")); 9 Object o = ois.readObject(); 10 ois.close(); 11 12 System.out.println(singleton); 13 System.out.println((Singleton)o); 14 } 15 } 16 17 //day.Singleton@15fbaa4 18 //day.Singleton@1d0d5ae
可以看到序列化后返回的对象不是原来的对象,破坏了单例模式,此时可以实现readResolve( )方法,让其返回原来对象
五、版本管理
序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。如果接收者加载的该对象的类的 serialVersionUID 与对应的发送者的类的版本号不同,则反序列化将会导致 InvalidClassException。可序列化类可以通过声明名为 "serialVersionUID" 的字段(该字段必须是静态 (static)、最终 (final) 的 long 型字段)显式声明其自己的 serialVersionUID:
如果可序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID 值。不过,建议 所有可序列化类都显式声明 serialVersionUID 值,原因是计算默认的 serialVersionUID 对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,这样在反序列化过程中可能会导致意外的 InvalidClassException。因此,为保证 serialVersionUID 值跨不同 java 编译器实现的一致性,序列化类必须声明一个明确的 serialVersionUID 值。还强烈建议使用 private 修饰符显示声明 serialVersionUID(如果可能),原因是这种声明仅应用于直接声明类 -- serialVersionUID 字段作为继承成员没有用处。数组类不能声明一个明确的 serialVersionUID,因此它们总是具有默认的计算值,但是数组类没有匹配 serialVersionUID 值的要求。
如果类进行了更新,并且没有显示指定serialVersionUID,那么新版本的类的serialVersionUID势必会发生变化,无法反序列化;若显示指定了serialVersionUID,则如果这个类只有方法产生了变化,那么在读入新对象数据时是不会有任何问题的。但是,如果数据域产生了变化,那么就可能会有问题。例如,旧文件对象可能比程序中的对象具有更多或更少的数据域,或者数据域的类型可能有所不同。在这些情况中,对象流将尽力将流对象转换成这个类当前的版本。
对象流会将这个类当前版本的数据域与流中版本的数据域进行比较,当然,对象流只会考虑非瞬时和非静态的数据域。如果这两部分数据域之间名字匹配而类型不匹配,那么对象流不会尝试将一种类型转换成另一种类型,因为这两个对象不兼容;如果流中的对象具有在当前版本中所没有的数据域,那么对象流会忽略这些额外的数据;如果当前版本具有在流化对象中所没有的数据域,那么这些新添加的域将被设置成它们的默认值。
六、继承关系中的序列化
可序列化类的所有子类型本身都是可序列化的,但父类未实现Serializable接口,而子类实现了,则反序列化过程中会调用父类的默认构造器。
官方文档中说明,要允许不可序列化类的子类型序列化,可以假定该子类型负责保存和恢复超类型的公用 (public)、受保护的 (protected) 和(如果可访问)包 (package) 字段的状态。仅在子类型扩展的类有一个可访问的无参数构造方法来初始化该类的状态时,才可以假定子类型有此职责。如果不是这种情况,则声明一个类为可序列化类是错误的。该错误将在运行时检测到。
在反序列化过程中,将使用该类的公用或受保护的无参数构造方法初始化不可序列化类的字段。可序列化的子类必须能够访问无参数构造方法。可序列化子类的字段将从该流中恢复。
1 //父类Person不实现Serializable接口 2 public class Student extends Person implements Serializable { 3 private String school; 4 5 public String getSchool() { 6 return school; 7 } 8 9 public void setSchool(String school) { 10 this.school = school; 11 } 12 13 public Student(String name, int age, int weight,String school,Pet pet) { 14 super(name, age, weight,pet); 15 this.school = school; 16 } 17 18 public Student(){ 19 System.out.println("我是子类默认构造器"); 20 } 21 22 @Override 23 public String toString() { 24 return "Student("+school+")"; 25 } 26 }
1 public class TestObjectStream { 2 public static void main(String[] args) throws IOException, ClassNotFoundException { 3 File file = new File(".\\file\\person.txt"); 4 Student student = new Student("天天",19,60,"2中",new Pet("小白", 2)); 5 6 write(file,student ); 7 Object o = read(file); 8 System.out.println((Student)o); 9 } 10 11 private static void write(File file, Object o) throws IOException { 12 ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(file)); 13 os.writeObject(o); 14 os.close(); 15 } 16 17 private static Object read(File file) throws IOException, ClassNotFoundException { 18 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); 19 Object o = ois.readObject(); 20 ois.close(); 21 return o; 22 } 23 } 24 25 //我是父类默认构造 26 //Student(2中) 27
七、为克隆使用序列化
由于反序列化返回的是一个新对象,因此只需要实现Cloneable和Serializable接口,就可以通过序列化便捷实现对一个对象的深拷贝。
1 public class CloneSeria implements Cloneable, Serializable { 2 @Override 3 protected Object clone() throws CloneNotSupportedException { 4 Object o = null; 5 try { 6 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 7 ObjectOutputStream out = new ObjectOutputStream(bos); 8 out.writeObject(this); 9 out.close(); 10 11 ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); 12 ObjectInputStream in = new ObjectInputStream(bis); 13 o = in.readObject(); 14 in.close(); 15 } catch (IOException e) { 16 e.printStackTrace(); 17 } catch (ClassNotFoundException e) { 18 e.printStackTrace(); 19 } 20 return o; 21 } 22 23 public static void main(String[] args) throws CloneNotSupportedException { 24 CloneSeria cloneSeria = new CloneSeria(); 25 Object clone = cloneSeria.clone(); 26 System.out.println(cloneSeria); 27 System.out.println((CloneSeria)clone); 28 } 29 } 30 31 //day5.CloneSeria@16f6e28 32 //day5.CloneSeria@18bf509