序列化

Posted 这一辈子也只有那么几个人

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了序列化相关的知识,希望对你有一定的参考价值。

Java 对象只有在虚拟机运行的情况下才存在,而虚拟机关闭了以后,这个对象也随着内存回收被释放掉,这种状态称为“瞬态”。如何把这种瞬态转换为持久态就是序列化要解决的问题。除了持久化需要用到序列化以外,把一个对象在网络上进行传输也是序列化的一个重要功能。在网络上,数据以字节的形式进行传输,序列化可以把一个对象作为整体在网络上传输,在网络的另一端,对这个整体进行还原。这样就实现了以对象为单位的传输。

 

1如何实现序列化

1.1默认的序列化

Java最简单的序列化可以通过对一个类实现Serializable接口实现。Serializable接口是一个标识接口,并没有定义任何方法。JDK中一些常见的类都实现了这个接口。如果想要对对象进行序列化,这个对象的类必须实现了Serilizable接口,否则将会抛出异常。下面是一个最简单的实例,用于说明如何进行序列化以及反序列化。

(1)定义了一个类  实现了Serializable接口

 

  1. class Sout implements Serializable  
  2. {  
  3.     public static  int  staticInt=1000;  
  4.     public int  paramA=1000;  
  5.     private String  paramB="abc";  
  6.   
  7.   
  8. }  

(2) 序列化与反序列化,序列化使用ObjectOutputStream类的writeObject方法,反序列化使用ObjectInputStream的readObject方法

 

  1. Sout sout=new Sout();  
  2.         sout.paramA=2500;  
  3.           
  4.         //序列化  
  5.         ObjectOutputStream objectOutputStream=new ObjectOutputStream(new FileOutputStream("d:/a.dat"));  
  6.         objectOutputStream.writeObject(sout);  
  7.         objectOutputStream.close();  
  8.           
  9.         //反序列化  
  10.         ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream("d:/a.dat"));  
  11.         Sout sout2=(Sout)  
  12.         objectInputStream.readObject();  


以上两个过程就完成了最简单,也是最常见的序列化方式。这种序列化将会对目标对象的所有属性进行序列化,包括属性是另一个对象的引用的情况,也将对其进行序列化。如果这个引用的对象没有实现Serializable接口,将会抛出异常。这种序列化显然不是最佳的,如果一个目标对象包含很多引用对象,这样的序列化过程将会是十分消耗资源的,而且也是不安全的。

另外还需要注意的是,这种序列化方式只对当前类的属性进行序列化,而不会对它的父类进行序列化,下面的例子将说明这一点

(1) 定义一个Father和一个Son类,子类Son类实现了Serializable接口

 

  1. class Father  
  2. {  
  3.     public String name;  
  4.     public int age;  
  5.   
  6. }  
  7.   
  8. class Son extends Father implements Serializable  
  9. {  
  10.     public String sex = "male";  
  11.   
  12. }  


(2) 序列化的结果是 属于Father类的属性 name为null

 

  1. public static void main(String[] args) throws Throwable, IOException  
  2.     {  
  3.         Son son = new Son();  
  4.         son.name = "xiaoming";  
  5.         son.age = 20;  
  6.   
  7.         // 序列化  
  8.         ObjectOutputStream objectOutputStream = new ObjectOutputStream(  
  9.                 new FileOutputStream("d:/a.dat"));  
  10.         objectOutputStream.writeObject(son);  
  11.         objectOutputStream.close();  
  12.   
  13.         // 反序列化  
  14.         ObjectInputStream objectInputStream = new ObjectInputStream(  
  15.                 new FileInputStream("d:/a.dat"));  
  16.         Son son2 = (Son) objectInputStream.readObject();  
  17.   
  18.         System.out.println(son2.sex);  
  19.         System.out.println(son2.name);  
  20.   
  21.     }  


对于这种需要序列化父类的情况,它的解决办法之一是让父类实现Serializable接口。


1.2 实现writeObject和readObject方法

这是1.1的升级版,对于1.1中介绍的默认方法来说,它是不可控制的,完全由JDK封装的方法对类进行序列化。这种方式的缺陷不仅仅如上面提到的,而且如果类中的属性有被transient关键字修饰的时候,这种方式可以破坏这种约束。

来看下面的例子

(1)定义一个Person类,其中age字段被transient修饰,表示不可以被序列化。同时定义了writeObject和readObject方法

 

  1. class Person implements Serializable  
  2. {  
  3.     public transient int age;  
  4.     public String name;  
  5.   
  6.     private void writeObject(ObjectOutputStream out) throws IOException  
  7.     {  
  8.         out.defaultWriteObject();  
  9. //      out.writeInt(age);  
  10.     }  
  11.   
  12.     private void readObject(ObjectInputStream in) throws IOException,  
  13.             ClassNotFoundException  
  14.     {  
  15.         in.defaultReadObject();  
  16. //      age = in.readInt();  
  17.     }  
  18.   
  19. }  


(2) 序列化的结果

如果去掉上面的注释,age字段将同样被序列化。

对于这种方式实现序列化,这其中的defaultWriteObject和defaultReadObject两个方法实际上就是1.1中的默认序列化方法。如果一个类既实现了Serializable接口,又定义了writeObject和readObject方法的话,在序列化时就调用这两个方法进行序列化。而且这两个方法可以在默认的序列化方式基础上进行拓展,即可以实现transient修饰的属性的序列化,又可以对父类的属性进行序列化。

1.3 实现Externalizable接口

这种方式与第二种方式很类似,只不过它实现的Externalizable接口,而且需要实现writeExternal和readExternal这两个方法。这是一种更加随意的序列化的方式,想要序列化那些属性,完成由自己决定。这种方式需要注意一点是,序列化的类需要提供一个空的构造方法。

示例:

 

  1. class Person implements Externalizable  
  2. {  
  3.     public Person()  
  4.   
  5.     {  
  6.   
  7.     }  
  8.   
  9.     public transient int age;  
  10.     public String name;  
  11.   
  12.     @Override  
  13.     public void writeExternal(ObjectOutput out) throws IOException  
  14.     {  
  15.         out.writeInt(age);  
  16.         out.writeUTF(name);  
  17.   
  18.     }  
  19.   
  20.     @Override  
  21.     public void readExternal(ObjectInput in) throws IOException,  
  22.             ClassNotFoundException  
  23.     {  
  24.         age = in.readInt();  
  25.         name = in.readUTF();  
  26.     }  
  27.   
  28. }  

注意:

writeObject readObject是私有的,只能被序列化机制调用,与此不同给的是,writeExternal和readExternal是公有的方法,而且还存在允许修改对象现有的状态。

2 对单例的序列化

在对单例的类进行序列化的时候,如果按照上面的方式,那么尽管得到了一致的结果,但并不满足单例的要求。如下:


(1)对于下面的程序,虽然利用序列化产生了新的实例,但是它已经破坏了单例的特性。

 

  1. SingleLady singleLady=SingleLady.getInstance();  
  2.           
  3.         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();  
  4.         // 序列化  
  5.         ObjectOutputStream objectOutputStream = new ObjectOutputStream(  
  6.                 outputStream);  
  7.         objectOutputStream.writeObject(singleLady);  
  8.         objectOutputStream.close();  
  9.   
  10.         outputStream.toByteArray();  
  11.   
  12.         // 反序列化  
  13.   
  14.         ObjectInputStream objectInputStream = new ObjectInputStream(  
  15.                 new ByteArrayInputStream(outputStream.toByteArray()));  
  16.   
  17.         SingleLady singleLady2 = (SingleLady) objectInputStream.readObject();  
  18.         System.out.println(singleLady==singleLady2);  
  1. class SingleLady implements Serializable {  
  2. private static SingleLady singleLady;  
  3.   
  4. private SingleLady() {  
  5.   
  6. }  
  7.   
  8. public static SingleLady getInstance() {  
  9.     if (singleLady == null) {  
  10.         singleLady = new SingleLady();  
  11.         return singleLady;  
  12.     } else {  
  13.         return singleLady;  
  14.     }  
  15. }  
  16.   
  17. public String getName() {  
  18.     return name;  
  19. }  
  20.   
  21. public void setName(String name) {  
  22.     this.name = name;  
  23. }  
  24.   
  25. private String name;  



(2)这就需要一种特别的方式去实现单例的序列化。需要定义另一种称为readResolve的特殊序列化方法仅需在单例类中定义这个方法。

 

  1. /** 
  2.      * 单例的序列化方法 
  3.      * @return 
  4.      * @throws ObjectStreamException 
  5.      */  
  6.     private Object readResolve() throws ObjectStreamException {  
  7.         return SingleLady.getInstance();  
  8.     }  




3SerialVersionUID与版本管理

在定义一个类实现了Serializable接口的时候,编译器通常会给出这么一个警告,提示我们需要生成一个 serial version ID。

技术分享

这个ID到底有什么用?简单的说,它相当于类的版本号,这样它就可以进行反序列化。当一个类进行反序列化的时候,首先要检验SerialVersionUID,如果在反序列化的过程中,SerialVersionUID不匹配的话,这个反序列化的过程就会失败,同时会抛出java.io.InvalidClassException异常,并打印出类的名字以及对应的SerialVersionUID。

当一个类升级的时候,它的所有较新版本都必须把serialVersionUID常量定义为与最初的版本的指纹相同。一旦这个静态成员被置于类中,那么序列化系统就可以读入这个类的不同版本。如果这个类只有方法发生了变化,那么在读入新的对象时不会产生任何问题。但是如果数据域发生了改变,这个时候可能会存在一些问题,对象流也会尽力地把这个对象转换到当前版本上:

  • 对象流会将对象与当前版本进行对比,而且仅仅对比非瞬时和非静态的数据域。如果两个数据域名字相同而类型却不同的话,那么对象流不会进行转换;

  • 如果对象流中的数据域在当前类中不存在,那么对象流将会忽略掉这些数据

  • 如果当前版本存在对象流中不存在的数据,那么这些数据将会被设置成默认值。

  • 如果使用序列化来保存对象,尤其是把一个对象保存到文件中,就需要考虑程序的演化对这个对象的影响。主要的影响就是对属性的增加与删除。



4重复引用的序列化

在实际中我们可能遇到这种情况,两个对象都持有另一个对象的引用,那么在序列化的时候,显然不应该把这个被引用的对象序列化两次。对于这种情况,Java利用了序列号的机制来解决这个问题:

  • 对遇到的每一个引用都关联一个序列号;

  • 每个对象 第一次遇到的时候,保存其对象到数据流中;

  • 如果某个对象已经保存过,那么只写出“与之前保存过的序列号为x的对象相同”。在读对象时,整个过程反过来

  • 对于流中的对象,第一次遇到它的序列号时,构建它,并使用流中数据来初始化它,然后记录这个序列号和新对象之间的关联;

  • 当遇到“与之前保存过的序列号为x的对象相同”标记时,获取与这个顺序号相关联的对象引用;

通过下面的示例可以看出Java是如何实现对重复引用的序列化的

(1) 定义两个类  common和hold


  1. class Common implements Serializable  
  2. {  
  3.     /** 
  4.      *  
  5.      */  
  6.     //private static final long serialVersionUID = -393917310072427835L;  
  7.     private static int count=0;  
  8.     private void writeObject(ObjectOutputStream out) throws IOException {   
  9.         System.out.println( "common write invoked  " +(count++));  
  10.         out.defaultWriteObject();    
  11.          
  12.     }    
  13.    
  14.     private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {   
  15.         System.out.println(" common  read invoked");  
  16.         in.defaultReadObject();    
  17.           
  18.     }    


  1. class Hold implements Serializable  
  2. {  
  3.     private Common common;  
  4.   
  5.     public Common getCommon() {  
  6.         return common;  
  7.     }  
  8.   
  9.     public void setCommon(Common common) {  
  10.         this.common = common;  
  11.     }  
  12.       
  13.   
  14. }  


(2) 序列化比较结果


  1. public static void main(String[] args) throws Throwable {  
  2.         Common common=new Common();  
  3.         Hold hold1=new Hold();  
  4.         Hold hold2=new Hold();  
  5.           
  6.         hold1.setCommon(common);  
  7.         hold2.setCommon(common);  
  8.           
  9.           
  10.         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();  
  11.         // 序列化  
  12.         ObjectOutputStream objectOutputStream = new ObjectOutputStream(  
  13.                 outputStream);  
  14.         objectOutputStream.writeObject(hold1);  
  15.         objectOutputStream.writeObject(hold2);  
  16.         objectOutputStream.close();  
  17.   
  18.         outputStream.toByteArray();  
  19.   
  20.         // 反序列化  
  21.   
  22.         ObjectInputStream objectInputStream = new ObjectInputStream(  
  23.                 new ByteArrayInputStream(outputStream.toByteArray()));  
  24.   
  25.         Hold hold3 = (Hold) objectInputStream.readObject();  
  26.         Hold hold4 = (Hold) objectInputStream.readObject();  
  27.           
  28.         System.out.println(hold3.getCommon()==hold4.getCommon());  
  29.     }  


结果显示序列化过程仅仅调用了一次 ,而且反序列化得到的结果也是指向同一个引用的。证明了上面说的Java序列化对于相同引用的机制。

5 clone

在Object类中,定义了protected的Clone方法,当需要进行clone时,这个方法必须被重写。这样很麻烦,而且某种程度上说也不太安全。序列化机制可以巧妙地从另外一个角度去解决这个问题,做法很简单。把一个对象序列化到流,然后进行反序列化。这样就产生了一个深拷贝。

 

    1. class StudentNew implements Serializable, Cloneable {  
    2.     public String name;  
    3.     public ClassMate classMate;  
    4.   
    5.     public  StudentNew clone() throws CloneNotSupportedException {  
    6.         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();  
    7.         try {  
    8.             // 序列化  
    9.             ObjectOutputStream objectOutputStream = new ObjectOutputStream(  
    10.                     outputStream);  
    11.             objectOutputStream.writeObject(this);  
    12.             objectOutputStream.close();  
    13.   
    14.             outputStream.toByteArray();  
    15.   
    16.             // 反序列化  
    17.   
    18.             ObjectInputStream objectInputStream = new ObjectInputStream(  
    19.                     new ByteArrayInputStream(outputStream.toByteArray()));  
    20.   
    21.             StudentNew cloneStudent = (StudentNew) objectInputStream.readObject();  
    22.             return cloneStudent;  
    23.         } catch (Exception e) {  
    24.             return null;  
    25.         }  
    26.   
    27.     }  






















以上是关于序列化的主要内容,如果未能解决你的问题,请参考以下文章

什么是序列化? 如何实现(反)序列化 序列化的应用

什么是序列化? 如何实现(反)序列化 序列化的应用

如何序列化/反序列化的ArrayList

什么是java的序列化和反序列化?

Netty_05_六种序列化方式(JavaIO序列化 XML序列化 Hessian序列化 JSON序列化 Protobuf序列化 AVRO序列化)(实践类)

Netty_05_六种序列化方式(JavaIO序列化 XML序列化 Hessian序列化 JSON序列化 Protobuf序列化 AVRO序列化)(实践类)