• 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 class Person 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     
    18     public String toString()
    19     {
    20         return "Name:" + name + "; Age:" + age;
    21     }
    22 }

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

     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对象
     1 class Person2 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 }

      注意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

      这正是我们期望的结果。

  • 相关阅读:
    2020阿里最新出品的泰山版Java开发手册,告别垃圾代码
    freecplus框架-目录操作
    freecplus框架简介
    freecplus框架-加载参数文件
    freecplus框架-xml解析
    freecplus框架-tcp网络通信
    freecplus框架-PostgreSQL数据库操作
    freecplus框架-Oracle数据库操作
    freecplus框架-MySQL数据库操作
    freecplus框架-ftp客户端
  • 原文地址:https://www.cnblogs.com/wing011203/p/3066606.html
Copyright © 2020-2023  润新知