含义、意义使用场景 序列化:将对象写入到IO流中
反序列化:从IO流中恢复对象
意义:序列化机制允许将实现序列化的Java对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。
使用场景:所有可在网络上传输的对象都必须是可序列化的,比如RMI(remote method invoke,即远程方法调用),传入的参数或返回的对象都是可序列化的,否则会出错;所有需要保存到磁盘的java对象都必须是可序列化的。通常建议:程序创建的每个JavaBean类都实现Serializeable接口。
序列化的实现方式 如果需要将某个对象保存到磁盘上或者通过网络传输,那么这个类应该实现Serializable接口或者Externalizable接口之一。
Serializable 普通序列化 Serializable接口是一个标记接口,不用实现任何方法。一旦实现了此接口,该类的对象就是可序列化的。
创建对象类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Person implements Serializable { private String name; private int age; public Person (String name, int age) { System.out.println("调用有参构造方法" ); this .name = name; this .age = age; } @Override public String toString () { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}' ; } }
序列化步骤 步骤一:创建一个ObjectOutputStream输出流;
步骤二:调用ObjectOutputStream对象的writeObject输出可序列化对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class WriteObject { public static void main (String[] args) { try { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("object.txt" )); Person person = new Person ("9龙" , 23 ); oos.writeObject(person); } catch (Exception e) { e.printStackTrace(); } } } 调用有参构造方法
反序列化步骤 步骤一:创建一个ObjectInputStream输入流;
步骤二:调用ObjectInputStream对象的readObject()得到序列化的对象。
我们将上面序列化到person.txt的person对象反序列化回来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class WriteObject { public static void main (String[] args) { try { ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("person.txt" )); Person brady = (Person) ois.readObject(); System.out.println(brady); } catch (Exception e) { e.printStackTrace(); } } } Person{name='9龙' , age=23 }
输出告诉我们,反序列化并不会调用构造方法。反序列的对象是由JVM自己生成的对象,不通过构造方法生成。
成员为引用的序列化 如果一个可序列化的类的成员不是基本类型,也不是String类型,那这个引用类型也必须是可序列化的 ;否则,会导致此类不能序列化。
看例子,我们新增一个Teacher类。将Person去掉实现Serializable接口代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Person { } public class Teacher implements Serializable { private String leval; private Person person; public Teacher (String leval, Person person) { this .leval = leval; this .person = person; } public static void main (String[] args) throws Exception { try (ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("teacher.txt" ))) { Person person = new Person ("路飞" , 20 ); Teacher teacher = new Teacher ("雷利" , person); oos.writeObject(teacher); } } }
控制台输出:
1 2 3 4 5 6 7 8 9 调用有参构造方法 Exception in thread "main" java.io.NotSerializableException: com.jelly.java.serialize.Person at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184 ) at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548 ) at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509 ) at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432 ) at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178 ) at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348 ) at com.jelly.java.serialize.Teacher.main(Teacher.java:20 )
因为Person类的对象是不可序列化的,这导致了Teacher的对象不可序列化
对同一个对象序列化多次 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public static void writeMutObject () { try { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("object.txt" )); Person p1 = new Person ("1龙" , 23 ); Person p2 = new Person ("9龙" , 30 ); oos.writeObject(p1); oos.writeObject(p2); oos.writeObject(p2); } catch (Exception e) { e.printStackTrace(); } } public static void readMutObject () { try { ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("object.txt" )); Person p1 = (Person) ois.readObject(); Person p2 = (Person) ois.readObject(); Person p3 = (Person) ois.readObject(); System.out.println(p1); System.out.println(p2); System.out.println(p3); System.out.println("p1==p2? :" + p1.equals(p2)); System.out.println("p2==p3? :" + p2.equals(p3)); System.out.println("p1==p3? :" + p1.equals(p3)); } catch (Exception e) { e.printStackTrace(); } } public static void main (String[] args) { readMutObject(); }
输出
1 2 3 4 5 6 Person{name='1龙', age=23} Person{name='9龙', age=30} Person{name='9龙', age=30} p1==p2? :false p2==p3? :true p1==p3? :false
从输出结果可以看出,Java序列化同一对象,并不会将此对象序列化多次得到多个对象 。
序列化算法
所有保存到磁盘的对象都有一个序列化编码号
当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列。
如果此对象已经序列化过,则直接存储对应的编号即可。
序列化算法潜在问题 由于java序利化算法不会重复序列化同一个对象,只会记录已序列化对象的编号。如果序列化一个可变对象(对象内的内容可更改)后,更改了对象内容,再次序列化,并不会再次将此对象转换为字节序列,而只是保存序列化编号,那么就会造成数据丢失。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public static void execp () { try { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("person.txt" )); Person p1 = new Person ("1龙" , 23 ); oos.writeObject(p1); p1.setName("9龙" ); oos.writeObject(p1); oos.close(); oos.flush(); 创建一个ObjectOutputStream输出流 ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("person.txt" )); Person pp1 = (Person) ois.readObject(); Person pp2 = (Person) ois.readObject(); System.out.println(pp1); System.out.println(pp2); } catch (Exception e) { System.out.println("exception " + e); } } public static void main (String[] args) { execp(); } 调用有参构造方法 Person{name='1龙' , age=23 } Person{name='1龙' , age=23 }
定制序列化方式 序列化时忽略字段 (transient) 有些时候,我们有这样的需求,某些属性不需要序列化。使用transient关键字选择不需要序列化的字段。
使用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。
自定义序列化方法 使用transient虽然简单,但将此属性完全隔离在了序列化之外。java提供了可选的。可以进行控制序列化的方式,或者对序列化数据进行编码加密等。
1 2 3 private void writeObject (java.io.ObjectOutputStream out) throws IOException;private void readObject (java.io.ObjectIutputStream in) throws IOException, ClassNotFoundException;private void readObjectNoData () throws ObjectStreamException;
通过重写writeObject与readObject方法,可以自己选择哪些属性需要序列化,哪些属性不需要。如果writeObject使用某种规则序列化,则相应的readObject需要相反的规则反序列化,以便能正确反序列化出对象。
这里展示对名字进行反转加密。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Person implements Serializable { private String name; private int age; private void writeObject (ObjectOutputStream out) throws IOException { out.writeObject(new StringBuffer (this .name).reverse()); out.writeInt(age); } private void readObject (ObjectInputStream ins) throws IOException, ClassNotFoundException { this .name = ((StringBuffer) ins.readObject()).reverse().toString(); this .age = ins.readInt(); } }
当序列化流不完整时,readObjectNoData()方法可以用来正确地初始化反序列化的对象。例如,使用不同类接收反序列化对象,或者序列化流被篡改时,系统都会调用readObjectNoData()方法来初始化反序列化的对象。
Externalizable 通过实现Externalizable接口,必须实现writeExternal、readExternal方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 public interface Externalizable extends java .io.Serializable { void writeExternal (ObjectOutput out) throws IOException; void readExternal (ObjectInput in) throws IOException, ClassNotFoundException; } public class ExPerson implements Externalizable { private String name; private int age; public ExPerson () { } public ExPerson (String name, int age) { this .name = name; this .age = age; } @Override public void writeExternal (ObjectOutput out) throws IOException { StringBuffer reverse = new StringBuffer (name).reverse(); System.out.println(reverse.toString()); out.writeObject(reverse); out.writeInt(age); } @Override public void readExternal (ObjectInput in) throws IOException, ClassNotFoundException { this .name = ((StringBuffer) in.readObject()).reverse().toString(); System.out.println(name); this .age = in.readInt(); } public static void main (String[] args) throws IOException, ClassNotFoundException { try (ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ExPerson.txt" )); ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("ExPerson.txt" ))) { oos.writeObject(new ExPerson ("brady" , 23 )); ExPerson ep = (ExPerson) ois.readObject(); System.out.println(ep); } } } ydarb brady ExPerson{name='brady' , age=23 }
注意:Externalizable接口不同于Serializable接口,实现此接口必须实现接口中的两个方法实现自定义序列化 ,这是强制性的;特别之处是必须提供pulic的无参构造器 ,因为在反序列化的时候需要反射创建对象。
二者对比
实现Serializable接口
实现Externalizable接口
系统自动存储必要的信息
程序员决定存储哪些信息
Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持
必须实现接口内的两个方法
性能略差
性能略好
序列化版本号serialVersionUID 我们知道,反序列化必须拥有class文件,但随着项目的升级,class文件也会升级,序列化怎么保证升级前后的兼容性呢? java序列化提供了一个
1 private static final long serialVersionUID = -81298930239 ;
的序列化版本号,只有版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。
如果反序列化使用的class的版本号与序列化时使用的不一致,反序列化会报InvalidClassException异常。
序列化版本号指定 可以自由指定,如果不指定JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。
什么情况下需要修改serialVersionUID呢? 分三种情况。
如果只是修改了方法,反序列化不影响,则无需修改版本号;
如果只是修改了静态变量,瞬态变量(transient修饰的变量 ),反序列化不受影响,无需修改版本号;
如果修改了非瞬态变量,则可能导致反序列化失败 。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量 。
总结
所有需要网络传输的对象都需要实现序列化接口,通过建议所有的javaBean都实现Serializable接口。
对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
如果想让某个变量不被序列化,使用transient修饰。
序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
反序列化时必须有序列化对象的class文件。
当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。
单例类序列化,需要重写readResolve()方法;否则会破坏单例原则。
同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。