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

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

  序列化概述

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

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

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

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

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

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

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

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

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

  简单的序列化示例

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

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

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

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

 


 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信息是我们不希望序列化的:

 


 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