好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

Java回顾之序列化

Java回顾之序列化

Java回顾之序列化

第一篇: Java回顾之I/O

  第二篇: Java回顾之网络通信

  第三篇: Java回顾之多线程

  第四篇: Java回顾之多线程同步

  第五篇: Java回顾之集合

  在这篇文章里,我们关注对象序列化。

  首先,我们来讨论一下什么是序列化以及序列化的原理;然后给出一个简单的示例来演示序列化和反序列化;有时有些信息是不应该被序列化的,我们应该如何控制;我们如何去自定义序列化内容;最后我们讨论一下在继承结构的场景中,序列化需要注意哪些内容。

  序列化概述

  序列化,简单来讲,就是以“流”的方式来保存对象,至于保存的目标地址,可以是文件,可以是数据库,也可以是网络,即通过网络将对象从一个节点传递到另一个节点。

  我们知道在Java的I/O结构中,有ObjectOutputStream和ObjectInputStream,它们可以实现将对象输出为二进制流,并从二进制流中获取对象,那为什么还需要序列化呢?这需要从Java变量的存储结构谈起,我们知道对Java来说,基础类型存储在栈上,复杂类型(引用类型)存储在堆中,对于基础类型来说,上述的操作时可行的,但对复杂类型来说,上述操作过程中,可能会产生重复的对象,造成错误。

  而序列化的工作流程如下:

  1)通过输出流保存的对象都有一个唯一的序列号。

  2)当一个对象需要保存时,先对其序列号进行检查。

  3)当保存的对象中已包含该序列号时,不需要再次保存,否则,进入正常保存的流程。

  正是通过序列号的机制,序列化才可以完整准确的保存对象的各个状态。

  序列化保存的是对象中的各个属性的值,而不是方法或者方法签名之类的信息。对于方法或者方法签名,只要JVM能够找到正确的ClassLoader,那么就可以invoke方法。

  序列化不会保存类的静态变量,因为静态变量是作用于类型,而序列化作用于对象。

  简单的序列化示例

  序列化的完整过程包括两部分:

  1)使用ObjectOutputStream将对象保存为二进制流,这一步叫做“序列化”。

  2)使用ObjectInputStream将二进制流转换成对象,这一步叫做“反序列化”。

  下面我们来演示一个简单的示例,首先定义一个Person对象,它包含name和age两个信息。

定义Person对象

  然后是两个公共方法,用来完成读、写对象的操作:

  1   private   static   void   writeObject(Object obj, String filePath)
   2   {
   3       try 
  4       {
   5          FileOutputStream fos =  new   FileOutputStream(filePath);
   6          ObjectOutputStream os =  new   ObjectOutputStream(fos);
   7           os.writeObject(obj);
   8           os.flush();
   9           fos.flush();
  10           os.close();
  11           fos.close();
  12          System.out.println("序列化成功。" );
  13       }
  14       catch  (Exception ex)
  15       {
  16           ex.printStackTrace();
  17       }
  18   }
  19  
 20   private   static   Object readObject(String filePath)
  21   {
  22       try 
 23       {
  24          FileInputStream fis =  new   FileInputStream(filePath);
  25          ObjectInputStream is =  new   ObjectInputStream(fis);
  26          
 27          Object temp =  is.readObject();
  28          
 29           fis.close();
  30           is.close();
  31          
 32           if  (temp !=  null  )
  33           {
  34              System.out.println("反序列化成功。" );
  35               return   temp;
  36           }
  37       }
  38       catch  (Exception ex)
  39       {
  40           ex.printStackTrace();
  41       }
  42      
 43       return   null  ;
  44  }

  这里,我们将对象保存的二进制流输出到磁盘文件中。

  接下来,我们首先来看“序列化”的方法:

 1   private   static   void   serializeTest1()
  2   {
  3      Person person =  new   Person();
  4      person.setName("Zhang San" );
  5      person.setAge(30 );
  6       System.out.println(person);
  7      writeObject(person, "d:\\temp\\test\\person.obj" );
  8  }

  我们定义了一个Person实例,然后将其保存到d:\temp\test\person.obj中。

  最后,是“反序列化”的方法:

 1   private   static   void   deserializeTest1()
  2   {    
  3      Person temp = (Person)readObject("d:\\temp\\test\\person.obj" );
  4      
 5       if  (temp !=  null  )
  6       {
  7           System.out.println(temp);
  8       }
  9  }

  它从d:\temp\test\person.obj中读取对象,然后进行输出。

  上述两个方法的执行结果如下:

Name:Zhang San; Age:30 
序列化成功。
反序列化成功。
Name:Zhang San; Age: 30

  可以看出,读取的对象和保存的对象是完全一致的。

  隐藏非序列化信息

  有时,我们的业务对象中会包含很多属性,而有些属性是比较隐私的,例如年龄、银行卡号等,这些信息是不太适合进行序列化的,特别是在需要通过网络来传输对象信息时,这些敏感信息很容易被窃取。

  Java使用transient关键字来处理这种情况,针对那些敏感的属性,我们只需使用该关键字进行修饰,那么在序列化时,对应的属性值就不会被保存。

  我们还是看一个实例,这次我们定义一个新的Person2,其中age信息是我们不希望序列化的:

定义Person2对象

  注意age的声明语句:

 1   private   transient   int  age;

  下面是“序列化”和“反序列化”的方法:

  1   private   static   void   serializeTest2()
   2   {
   3      Person2 person =  new   Person2();
   4      person.setName("Zhang San" );
   5      person.setAge(30 );
   6       System.out.println(person);
   7      writeObject(person, "d:\\temp\\test\\person2.obj" );
   8   }
   9  
 10   private   static   void   deserializeTest2()
  11   {    
  12      Person2 temp = (Person2)readObject("d:\\temp\\test\\person2.obj" );
  13      
 14       if  (temp !=  null  )
  15       {
  16           System.out.println(temp);
  17       }
  18  }

  它的输出结果如下:

Name:Zhang San; Age:30 
序列化成功。
反序列化成功。
Name:Zhang San; Age: 0

  可以看到经过反序列化的对象,age的信息变成了Integer的默认值0。

  自定义序列化过程

  我们可以对序列化的过程进行定制,进行更细粒度的控制。

  思路是在业务模型中添加readObject和writeObject方法。下面看一个实例,我们新建一个类型,叫Person3:

  1   class  Person3  implements   Serializable
   2   {
   3       private   String name;
   4       private   transient   int   age;
   5       public   void   setName(String name) {
   6           this .name =  name;
   7       }
   8       public   String getName() {
   9           return   name;
  10       }
  11       public   void  setAge( int   age) {
  12           this .age =  age;
  13       }
  14       public   int   getAge() {
  15           return   age;
  16       }
  17      
 18       public   String toString()
  19       {
  20           return  "Name:" + name + "; Age:" +  age;
  21       }
  22      
 23       private   void   writeObject(ObjectOutputStream os)
  24       {
  25           try 
 26           {
  27               os.defaultWriteObject();
  28              os.writeObject( this  .age);
  29              System.out.println( this  );
  30              System.out.println("序列化成功。" );
  31           }
  32           catch  (Exception ex)
  33           {
  34               ex.printStackTrace();
  35           }
  36       }
  37      
 38       private   void   readObject(ObjectInputStream is)
  39       {
  40           try 
 41           {
  42               is.defaultReadObject();
  43               this .setAge(((Integer)is.readObject()).intValue() - 1 );
  44              System.out.println("反序列化成功。" );
  45              System.out.println( this  );
  46           }
  47           catch  (Exception ex)
  48           {
  49               ex.printStackTrace();
  50           }
  51       }
  52  }

  请注意观察readObject和writeObject方法,它们都是private的,接受的参数是ObjectStream,然后在方法体内调用了defaultReadObject或者defaultWriteObject方法。

  这里age同样是transient的,但是在保存对象的过程中,我们单独对其进行了保存,在读取时,我们将age信息读取出来,并进行了减1处理。

  下面是测试方法:

  1   private   static   void   serializeTest3()
   2   {
   3      Person3 person =  new   Person3();
   4      person.setName("Zhang San" );
   5      person.setAge(30 );
   6       System.out.println(person);
   7       try 
  8       {
   9          FileOutputStream fos =  new  FileOutputStream("d:\\temp\\test\\person3.obj" );
  10          ObjectOutputStream os =  new   ObjectOutputStream(fos);
  11           os.writeObject(person);
  12           fos.close();
  13           os.close();
  14       }
  15       catch  (Exception ex)
  16       {
  17           ex.printStackTrace();
  18       }
  19   }
  20  
 21   private   static   void   deserializeTest3()
  22   {    
  23       try 
 24       {
  25          FileInputStream fis =  new  FileInputStream("d:\\temp\\test\\person3.obj" );
  26          ObjectInputStream is =  new   ObjectInputStream(fis);
  27           is.readObject();
  28           fis.close();
  29           is.close();
  30       }
  31       catch  (Exception ex)
  32       {
  33           ex.printStackTrace();
  34       }
  35  }

  输出结果如下:

Name:Zhang San; Age:30 
序列化成功。
反序列化成功。
Name:Zhang San; Age: 29

  可以看到,经过反序列化得到的对象,其age属性已经减1。

  探讨serialVersionUID

  在上文中,我们描述序列化原理时,曾经提及每个对象都会有一个唯一的序列号,这个序列号,就是serialVersionUID。

  当我们的对象实现Serializable接口时,该接口可以为我们生成serialVersionUID。

  有两种方式来生成serialVersionUID,一种是固定值:1L,一种是经过JVM计算,不同的JVM采取的计算算法可能不同。

  下面就是两个serialVersionUID的示例:

 1   private   static   final   long  serialVersionUID = 1L ;
  2   private   static   final   long  serialVersionUID = -2380764581294638541L;

  第一行是采用固定值生成的;第二行是JVM经过计算得出的。

  那么serialVersionUID还有其他用途吗?

  我们可以使用它来控制版本兼容。如果采用JVM生成的方式,我们可以看到,当我们业务对象的代码保持不变时,多次生成的serialVersionUID也是不变的,当我们对属性进行修改时,重新生成的serialVersionUID会发生变化,当我们对方法进行修改时,serialVersionUID不变。这也从另一个侧面说明,序列化是作用于对象属性上的。

  当我们先定义了业务对象,然后对其示例进行了“序列化”,这时根据业务需求,我们修改了业务对象,那么之前“序列化”后的内容还能经过“反序列化”返回到系统中吗?这取决于业务对象是否定义了serialVersionUID,如果定义了,那么是可以返回的,如果没有定义,会抛出异常。

  来看下面的示例,定义新的类型Person4:

  1   class  Person4  implements   Serializable
   2   {
   3       private   String name;
   4       private   int   age;
   5       public   void   setName(String name) {
   6           this .name =  name;
   7       }
   8       public   String getName() {
   9           return   name;
  10       }
  11       public   void  setAge( int   age) {
  12           this .age =  age;
  13       }
  14       public   int   getAge() {
  15           return   age;
  16       }
  17       private   void   xxx(){}
  18      
 19       public   String toString()
  20       {
  21           return  "Name:" + name + "; Age:" +  age;
  22       }
  23  }

  然后运行下面的方法:

 1   private   static   void   serializeTest4()
  2   {
  3      Person4 person =  new   Person4();
  4      person.setName("Zhang San" );
  5      person.setAge(30 );
  6      
 7      writeObject(person, "d:\\temp\\test\\person4.obj" );
  8  }

  接下来修改Person4,追加address属性:

  1   class  Person4  implements   Serializable
   2   {
   3       private   String name;
   4       private   int   age;
   5       private   String address;
   6       public   void   setName(String name) {
   7           this .name =  name;
   8       }
   9       public   String getName() {
  10           return   name;
  11       }
  12       public   void  setAge( int   age) {
  13           this .age =  age;
  14       }
  15       public   int   getAge() {
  16           return   age;
  17       }
  18       private   void   xxx(){}
  19      
 20       public   String toString()
  21       {
  22           return  "Name:" + name + "; Age:" +  age;
  23       }
  24       public   void   setAddress(String address) {
  25           this .address =  address;
  26       }
  27       public   String getAddress() {
  28           return   address;
  29       }
  30  }

  然后运行“反序列化”方法:

 1   private   static   void   deserializeTest4()
  2   {    
  3      Person4 temp = (Person4)readObject("d:\\temp\\test\\person4.obj" );
  4      
 5       if  (temp !=  null  )
  6       {
  7           System.out.println(temp);
  8       }
  9  }

  可以看到,运行结果如下:

java.io.InvalidClassException: sample.serialization.Person4; local  class  incompatible: stream classdesc serialVersionUID = -2380764581294638541, local  class  serialVersionUID = -473458100724786987 
    at java.io.ObjectStreamClass.initNonProxy(Unknown Source)
    at java.io.ObjectInputStream.readNonProxyDesc(Unknown Source)
    at java.io.ObjectInputStream.readClassDesc(Unknown Source)
    at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
    at java.io.ObjectInputStream.readObject0(Unknown Source)
    at java.io.ObjectInputStream.readObject(Unknown Source)
    at sample.serialization.Sample.readObject(Sample.java: 158 )
    at sample.serialization.Sample.deserializeTest4(Sample.java: 105 )
    at sample.serialization.Sample.main(Sample.java: 16)

  但是当我们在Person4中添加serialVersionUID后,再次执行上述各步骤,得出的运行结果如下:

 反序列化成功。
Name:Zhang San; Age: 30

  有继承结构的序列化

  业务对象会产生继承,这在管理系统中是经常看到的,如果我们有下面的业务对象:

  1   class   Person5
   2   {
   3       private   String name;
   4       private   int   age;
   5       public   void   setName(String name) {
   6           this .name =  name;
   7       }
   8       public   String getName() {
   9           return   name;
  10       }
  11       public   void  setAge( int   age) {
  12           this .age =  age;
  13       }
  14       public   int   getAge() {
  15           return   age;
  16       }
  17      
 18       public   String toString()
  19       {
  20           return  "Name:" + name + "; Age:" +  age;
  21       }
  22      
 23       public  Person5(String name,  int   age)
  24       {
  25           this .name =  name;
  26           this .age =  age;
  27       }
  28   }
  29  
 30   class  Employee  extends  Person5  implements   Serializable
  31   {
  32       public  Employee(String name,  int   age) {
  33           super  (name, age);
  34       }
  35  
 36       private   String companyName;
  37  
 38       public   void   setCompanyName(String companyName) {
  39           this .companyName =  companyName;
  40       }
  41  
 42       public   String getCompanyName() {
  43           return   companyName;
  44       }
  45      
 46       public   String toString()
  47       {
  48           return  "Name:" +  super .getName() + "; Age:" +  super .getAge() + "; Company:" +  this  .companyName;
  49       }
  50  }

  Employee继承在Person5,Employee实现了Serializable接口,Person5没有实现,那么运行下面的方法:

  1   private   static   void   serializeTest5()
   2   {
   3      Employee emp =  new  Employee("Zhang San", 30 );
   4      emp.setCompanyName("XXX" );
   5      
  6      writeObject(emp, "d:\\temp\\test\\employee.obj" );
   7   }
   8  
  9   private   static   void   deserializeTest5()
  10   {    
  11      Employee temp = (Employee)readObject("d:\\temp\\test\\employee.obj" );
  12      
 13       if  (temp !=  null  )
  14       {
  15           System.out.println(temp);
  16       }
  17  }

  会正常运行吗?事实上不会,它会抛出如下异常:

 java.io.InvalidClassException: sample.serialization.Employee; no valid constructor
    at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(Unknown Source)
    at java.io.ObjectStreamClass.checkDeserialize(Unknown Source)
    at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
    at java.io.ObjectInputStream.readObject0(Unknown Source)
    at java.io.ObjectInputStream.readObject(Unknown Source)
    at sample.serialization.Sample.readObject(Sample.java: 158 )
    at sample.serialization.Sample.deserializeTest5(Sample.java: 123 )
    at sample.serialization.Sample.main(Sample.java: 18)

  原因: 在有继承层次的业务对象,进行序列化时,如果父类没有实现Serializable接口,那么父类必须提供默认构造函数 。

  我们为Person5添加如下默认构造函数:

 1   public   Person5()
  2   {
  3       this .name = "Test" ;
  4       this .age = 1 ;
  5  }

  再次运行上述代码,结果如下:

Name:Zhang San; Age:30 ; Company:XXX
序列化成功。
反序列化成功。
Name:Test; Age: 1; Company:XXX

  可以看到,反序列化后的结果, 父类中的属性,已经被父类构造函数中的赋值代替了!

  因此,我们推荐 在有继承层次的业务对象进行序列化时,父类也应该实现Serializable接口 。我们对Person5进行修改,使其实现Serializable接口,执行结果如下:

Name:Zhang San; Age:30 ; Company:XXX
序列化成功。
反序列化成功。
Name:Zhang San; Age: 30; Company:XXX

  这正是我们期望的结果。

    

作者: 李胜攀

    

出处: http://wing011203.cnblogs.com/

    

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

 

分类:  Java相关

标签:  序列化 ,  面试 ,  Java ,  Serializable

好文收藏系列(二) 2013-05-08 15:14 by 轩脉刃, 454 阅读,  1  评论,  收藏 ,  编辑

分析redis性能的项目

https://github.com/Instagram/redis-faina

DB性能分析思路之全量query分析

http://txyey.org/?p=60

如何创建自适应系统来增强用户体验

http://www.alibuybuy.com/posts/81152.html#jtss-tsina

PS: 自适应系统的概念

Golang的坑之http读取大文件必须读完

http://wendal.net/2013/0316.html

PS:golang的一个坑。。。so,坑还是要一个个踩过才行

深入分析Linux内核源码

http://www.kerneltravel.net/kernel-book/%E6%B7%B1%E5%85%A5%E5%88%86%E6%9E%90Linux%E5%86%85%E6%A0%B8%E6%BA%90%E7%A0%81.html

PS:纯收藏

被遗忘的Logrotate

http://huoding.com/2013/04/21/246

PS:你还在用crontab来删除日志吗?out了

每个程序员都应该了解的 CPU 高速缓存

http://blog.jobbole.com/36263/

PS:这个是一个系列的文章,都是精品

每个程序员都应该了解的内存知识

http://blog.jobbole.com/34303/

PS:这个是一个系列的文章,都是精品

开发者需要了解的WebKit

http://www.infoq.com/cn/articles/webkit-for-developers

PS:个人觉得slice说得更明白

Network Autoconfiguration with Go and ?MQ

http://kyleisom.net/blog/2013/02/26/network-autoconfiguration-with-go-and-zmq/

PS: go使用在zeromq上

来自Google、Amazon和Facebook等7大知名互联网的系统扩展经验

http://www.csdn.net/article/2013-04-01/2814733-scalability-lessons-from-google-7-compannies

PS:自动化一切很帅,但是没说到怎么做,纲纲

linux lsof详解

http://blog.csdn.net/guoguo1980/article/details/2324454

PS:关于lsof命令

我的学习方法

http://blog.csdn.net/absurd/article/details/1497463

PS:大牛说学习方法。。。

PHP程序下如何取得APK签名以及验证是否一致

http://www.oophp.cn/article/view/id/430

PS:实际上还是使用toolkey

黑客是怎样入侵你的网站的

http://www.freebuf.com/articles/web/7359.html

PS:潘多拉的宝盒。。。强烈推荐

《深入理解并行编程》中文版

http://ifeve.com/perfbook/

snoopy(强大的PHP采集类) 实例应用

http://www.4wei.cn/archives/396

PS:snoopy如其名一样可爱

[翻译]绝妙的 channel

http://www.mikespook.com/2013/05/%E7%BF%BB%E8%AF%91%E7%BB%9D%E5%A6%99%E7%9A%84-channel/comment-page-1/#comment-6007

PS:golang的channel的深入研究

本文基于 署名-非商业性使用 3.0 许可协议发布,欢迎转载,演绎,但是必须保留本文的署名 叶剑峰 (包含链接http://www.cnblogs.com/yjf512/),且不得用于商业目的。如您有任何疑问或者授权方面的协商,请 与我联系 。

 

分类:  开源研究

作者: Leo_wl

    

出处: http://www.cnblogs.com/Leo_wl/

    

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

版权信息

查看更多关于Java回顾之序列化的详细内容...

  阅读:40次