java序列化

来源:互联网 发布:今日头条 招聘 java 编辑:程序博客网 时间:2024/06/05 09:56

序列化概述

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

  我们知道在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     try23     {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         try26         {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         try41         {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     try24     {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 Serializable31 {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

  这正是我们期望的结果。

实现序列化的第二种方式为实现接口Externalizable

Externlizable接口继承了java的序列化接口,并增加了两个方法:

     void writeExternal(ObjectOutput out) throws IOException;

     void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

     首先,我们在序列化对象的时候,由于这个类实现了Externalizable 接口,在writeExternal()方法里定义了哪些属性可以序列化,

哪些不可以序列化,所以,对象在经过这里就把规定能被序列化的序列化保存文件,不能序列化的不处理,然后在反序列的时候自动调

用readExternal()方法,根据序列顺序挨个读取进行反序列,并自动封装成对象返回,然后在测试类接收,就完成了反序列。

     所以说Exterinable的是Serializable的一个扩展。

     为了更好的理解相关内容,请看下面的例子:

   

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
package com.xiaohao.test;
 
import java.io.Externalizable;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
 
 
/**
 * 测试实体类
 * @author 小浩
 * @创建日期 2015-3-12
 */
class Person implements Externalizable{
        private static final long serialVersionUID = 1L;<br>    String userName;
    String password;
    String age;
     
   
    public Person(String userName, String password, String age) {
        super();
        this.userName = userName;
        this.password = password;
        this.age = age;
    }
     
     
    public Person() {
        super();
    }
 
 
    public String getAge() {
        return age;
    }
    public void setAge(String age) {
        this.age = age;
    }
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
     
    /**
     * 序列化操作的扩展类
     */
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        //增加一个新的对象
        Date date=new Date();
        out.writeObject(userName);
        out.writeObject(password);
        out.writeObject(date);
    }
     
    /**
     * 反序列化的扩展类
     */
    @Override
    public void readExternal(ObjectInput in) throws IOException,
            ClassNotFoundException {
        //注意这里的接受顺序是有限制的哦,否则的话会出错的
        // 例如上面先write的是A对象的话,那么下面先接受的也一定是A对象...
        userName=(String) in.readObject();
        password=(String) in.readObject();
        SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");
        Date date=(Date)in.readObject();       
        System.out.println("反序列化后的日期为:"+sdf.format(date));
         
    }
    @Override
    public String toString() {
        //注意这里的年龄是不会被序列化的,所以在反序列化的时候是读取不到数据的
        return "用户名:"+userName+"密 码:"+password+"年龄:"+age;
    }
}
 
 
/**
 * 序列化和反序列化的相关操作类
 * @author 小浩
 * @创建日期 2015-3-12
 */
class Operate{
    /**
     * 序列化方法
     * @throws IOException
     * @throws FileNotFoundException
     */
    public void serializable(Person person) throws FileNotFoundException, IOException{
        ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream("a.txt"));
        outputStream.writeObject(person);      
    }
     
    /**
     * 反序列化的方法
     * @throws IOException
     * @throws FileNotFoundException
     * @throws ClassNotFoundException
     */
    public Person deSerializable() throws FileNotFoundException, IOException, ClassNotFoundException{
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream("a.txt"));
        return (Person) ois.readObject();
    }
     
 
     
}
/**
 * 测试实体主类
 * @author 小浩
 * @创建日期 2015-3-12
 */
public class Test{
    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
       Operate operate=new Operate();
       Person person=new Person("小浩","123456","20");
       System.out.println("为序列化之前的相关数据如下:\n"+person.toString());
       operate.serializable(person);
       Person newPerson=operate.deSerializable();
       System.out.println("-------------------------------------------------------");
       System.out.println("序列化之后的相关数据如下:\n"+newPerson.toString());
    }
     
     
}

        

  首先,我们在序列化UserInfo对象的时候,由于这个类实现了Externalizable 接口,在writeExternal()方法里定义了哪些属性可

以序列化,哪些不可以序列化,所以,对象在经过这里就把规定能被序列化的序列化保存文件,不能序列化的不处理,然后在反序列

的时候自动调用readExternal()方法,根据序列顺序挨个读取进行反序列,并自动封装成对象返回,然后在测试类接收,就完成了反

序列。

5.4 readResolve()方法

当我们使用Singleton模式时,应该是期望某个类的实例应该是唯一的,但如果该类是可序列化的,那么情况可能略有不同。此时对第2节使用的Person类进行修改,使其实现Singleton模式,如下所示:

  1. public class Person implements Serializable {
  2. private static class InstanceHolder {
  3. private static final Person instatnce = new Person("John", 31, Gender.MALE);
  4. }
  5. public static Person getInstance() {
  6. return InstanceHolder.instatnce;
  7. }
  8. private String name = null;
  9. private Integer age = null;
  10. private Gender gender = null;
  11. private Person() {
  12. System.out.println("none-arg constructor");
  13. }
  14. private Person(String name, Integer age, Gender gender) {
  15. System.out.println("arg constructor");
  16. this.name = name;
  17. this.age = age;
  18. this.gender = gender;
  19. }
  20. ...
  21. }

同时要修改SimpleSerial应用,使得能够保存/获取上述单例对象,并进行对象相等性比较,如下代码所示:

  1. public class SimpleSerial {
  2. public static void main(String[] args) throws Exception {
  3. File file = new File("person.out");
  4. ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
  5. oout.writeObject(Person.getInstance()); // 保存单例对象
  6. oout.close();
  7. ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
  8. Object newPerson = oin.readObject();
  9. oin.close();
  10. System.out.println(newPerson);
  11. System.out.println(Person.getInstance() == newPerson); // 将获取的对象与Person类中的单例对象进行相等性比较
  12. }
  13. }

执行上述应用程序后会得到如下结果:

  1. arg constructor
  2. [John, 31, MALE]
  3. false

值得注意的是,从文件person.out中获取的Person对象与Person类中的单例对象并不相等。为了能在序列化过程仍能保持单例的特性,可以在Person类中添加一个readResolve()方法,在该方法中直接返回Person的单例对象,如下所示:

  1. public class Person implements Serializable {
  2. private static class InstanceHolder {
  3. private static final Person instatnce = new Person("John", 31, Gender.MALE);
  4. }
  5. public static Person getInstance() {
  6. return InstanceHolder.instatnce;
  7. }
  8. private String name = null;
  9. private Integer age = null;
  10. private Gender gender = null;
  11. private Person() {
  12. System.out.println("none-arg constructor");
  13. }
  14. private Person(String name, Integer age, Gender gender) {
  15. System.out.println("arg constructor");
  16. this.name = name;
  17. this.age = age;
  18. this.gender = gender;
  19. }
  20. private Object readResolve() throws ObjectStreamException {
  21. return InstanceHolder.instatnce;
  22. }
  23. ...
  24. }

再次执行本节的SimpleSerial应用后将如下输出:

  1. arg constructor
  2. [John, 31, MALE]
  3. true

无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象。


0 0
原创粉丝点击