java Serializable和Externalizable序列化反序列化详解

一、什么是序列化?
 
“对象序列化”(Object Serialization)是 Java1.1就开始有的特性。简单地说,就是可以将一个对象(标志对象的类型)及其状态转换为字节码,保存起来(可以保存在数据库,内存,文件等),然后可以在适当的时候再将其状态恢复(也就是反序列化)。serialization 不但可以在本机做,而且可以经由网络操作。它自动屏蔽了操作系统的差异,字节顺序等。比如,在 Windows 平台生成一个对象并序列化之,然后通过网络传到一台 Unix 机器上,然后可以在这台Unix机器上正确地重构(deserialization)这个对象。不必关心数据在不同机器上如何表示,也不必关心字节的顺序或者其他任何细节。
 
另外,还应明白以下几点:
 
a. java.io.Serializable接口没有任何方法属性域,实现它的类只是从语义上表明自己是可以序列化的。
 
b. 在对一个 Serializable(可序列化)对象进行重新装配的过程中,不会调用任何构建器(甚至默认构建器)。整个对象都是通过从 InputStream 中取得数据恢复的。
 
c. 如是要一个类是可序列化的,那么它的子类也是可序列化的。
 

二、序列化在什么时候用?
 
可提供对 Java 两种主要特性的支持:
 
远程方法调用(RMI):使本来存在于其他机器的对象可以表现出好象就在本地机器上的行为。将消息发给远程对象时,需要通过对象序列化来传输参数和返回值。
 
对象的序列化也是 Java Beans 必需的。使用一个 Bean 时,它的状态信息通常在设计期间配置好。程序启动以后,这种状态信息必须保存下来,以便程序启动以后恢复;具体工作由对象序列化完成。
 
三   序列化过程
 
java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
 
  java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。、
 
  只有实现了Serializable和Externalizable接口的类的对象才能被序列化。Externalizable接口继承自Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式。
 
  对象序列化包括如下步骤:
 
  1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
 
  2) 通过对象输出流的writeObject()方法写对象。
 
  对象反序列化的步骤如下:
 
  1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
 
  2) 通过对象输入流的readObject()方法读取对象。
 
  下面让我们来看一个对应的例子,类的内容如下:
 
     


view plaincopy to clipboardprint?
01.import java.io.*; 
02.import java.util.Date; 
03. 
04. 
09. 
10.public class ObjectSaver { 
11.  
15. 
16.public static void main(String[] args) throws Exception { 
17. ObjectOutputStream out = new ObjectOutputStream 
18.(new FileOutputStream("D:""objectFile.obj")); 
19. 
20. //序列化对象 
21. 
22. Customer customer = new Customer("阿蜜果", 24); 
23. out.writeObject("你好!"); 
24. out.writeObject(new Date()); 
25. out.writeObject(customer); 
26. out.writeInt(123); //写入基本类型数据 
27. out.close(); 
28. //反序列化对象 
29. 
30. ObjectInputStream in = new ObjectInputStream 
31.(new FileInputStream("D:""objectFile.obj")); 
32. 
33. System.out.println("obj1=" + (String) in.readObject()); 
34. System.out.println("obj2=" + (Date) in.readObject()); 
35. Customer obj3 = (Customer) in.readObject(); 
36. System.out.println("obj3=" + obj3); 
37. int obj4 = in.readInt(); 
38. System.out.println("obj4=" + obj4); 
39. in.close(); 
40.} 
41.} 
42. 
43.class Customer implements Serializable { 
44.private String name; 
45.private int age; 
46.public Customer(String name, int age) { 
47.this.name = name; 
48.this.age = age; 
49.} 
50. 
51.public String toString() { 
52.return "name=" + name + ", age=" + age; 
53.} 
54.}  

 
输出结果如下:
 
obj1=你好!
 
obj2=Sat Sep 15 22:02:21 CST 2007
 
obj3=name=阿蜜果, age=24
 
obj4=123

四、如何序列化
 
在Java里,如果要使一个类可以序列化或反序列化,只需要实现 java.io.Serializable 接口。如果类没有实现这个接口,则一般来说不能将他们的状态进行序列化与反序列化。注意,这里我说"一般来说",是因为Java还提供了另外一个接口 java.io.Externalizable,关于这个接口的使用我将会在下面单独说明。
 
1. transient(临时)关键字
 
控制序列化过程时,可能有一个特定的子对象不愿让Java的序列化机制自动保存与恢复。一般地,若那个子对象包含了不想序列化的敏感信息(如密码),就会面临这种情况。即使那种信息在对象中具有“private”(私有)属性,但一旦经序列化处理,人们就可以通过读取一个文件,或者拦截网络传输得到它。
 
为防止对象的敏感部分被序列化,一个办法是将自己的类实现为Externalizable,这样一来,没有任何东西可以自动序列化,只能在 writeExternal() 明确序列化那些需要的部分。
 
然而,若操作的是一个 Serializable 对象,所有序列化操作都会自动进行。为解决这个问题,可以用transient(临时)逐个字段地关闭序列化。
 
五、特殊情况-父类不可序列化而子类可序列化
 
我们看到,对于B的属性b1,其可以正确的序列化,但对于其从A继承过来的属性a,b则没有正确的序列化。为什么呢?我们再看上面的运行结果,可以发现:反序列化的时候,由于B实现了Serializable,所在以反序列化的时候,它并不会调用它自己的构造器,但是,在反序列化B的时候,却调用了它的超类的构造器(实际上不仅仅是构造器,A的所有的初始化过程都会正常进行)。这正是上面结果中a,b的值没有正确反序列化的原因。
 
于是,对于这种父类非序列化而子类可序列化的类,子类应该自己对超类的public,protected,以及 friedly 属性进行单独处理。
 
六.可序列化类的不同版本的序列化兼容性
 
  凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量:
 
private static final long serialVersionUID;

  以上serialVersionUID的取值是Java运行时环境根据类的内部细节自动生成的。如果对类的源代码作了修改,再重新编译,新生成的类文件的serialVersionUID的取值有可能也会发生变化。
类的serialVersionUID的默认值完全依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的serialVersionUID,也有可能相同。为了提高哦啊serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显示的定义serialVersionUID,为它赋予明确的值。显式地定义serialVersionUID有两种用途:
 
  1) 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;
 
  2) 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。
 
七、关于 writeReplace()与readResolve()
 
对于实现 Serializable 或 Externalizable 接口的类来说,writeReplace() 方法可以使对象被写入流以前,用一个对象来替换自己。当序列化时,可序列化的类要将对象写入流,如果我们想要另一个对象来替换当前对象来写入流,则可以要实现下面这个方法,方法的签名也要完全一致:ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
writeReplace()方法在 ObjectOutputStream 准备将对象写入流以前调用, ObjectOutputStream 会首先检查序列化的类是否定义了 writeReplace()方法,如果定义了这个方法,则会通过调用它,用另一个对象替换它写入流中。方法返回的对象要么与它替换的对象类型相同,要么与其兼容,否则,会抛出 ClassCastException 。
 
同理,当反序列化时,要将一个对象从流中读出来,我们如果想将读出来的对象用另一个对象实例替换,则要实现跟下面的方法的签名完全一致的方法。ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
 
readResolve 方法在对象从流中读取出来的时候调用, ObjectInputStream 会检查反序列化的对象是否已经定义了这个方法,如果定义了,则读出来的对象返回一个替代对象。同 writeReplace()方法,返回的对象也必须是与它替换的对象兼容,否则抛出 ClassCastException。如果序列化的类中有这些方法,那么它们的执行顺序是这样的:
a. writeReplace()
b. writeObject()
c. readObject()
d. readResolve()
 
下面是 java doc 中关于 readResolve() 与 writeReplace()方法的英文描述:
 
Serializable classes that need to designate an alternative object to be used when writing an object to the stream should implement this special method with the exact signature:


 ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
 This writeReplace method is invoked by serialization if the method exists and it would be accessible from a method defined within the class of the object being serialized. Thus, the method can have private, protected and package-private access. Subclass access to this method follows java accessibility rules.

Classes that need to designate a replacement when an instance of it is read from the stream should implement this special method with the exact signature.
 
 ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
 This readResolve method follows the same invocation rules and accessibility rules as writeReplace.
 
八. 

其实这个问题简单思考一下就可以搞清楚,方法是不带状态的,就是一些指令,指令是不需要序列化的,只要你的JVM classloader可以load到这个类,那么类方法指令自然就可以获得。序列化真正需要保存的只是对象属性的值,和对象的类型。
 
这些知识找一本Java基础编程的书,或者Java手册就可以查到,我以为是不应该犯这种基本概念错误的。

我们可以做一个简单的小试验,来证实一下:

 

view plaincopy to clipboardprint?

package com.javaeye; 
 
import java.io.Serializable; 
 
public class DomainObject  implements Serializable { 
 
    private String name; 
     
    private int age ; 
 
    public int getAge(); { 
        return age; 
   
 
    public void setAge(int age); { 
        this.age = age; 
   
 
    public String getName(); { 
        return name; 
   
 
    public void setName(String name); { 
        this.name = name; 
   
     
     
 
  
package com.javaeye; 
 
import java.io.FileOutputStream; 
import java.io.ObjectOutputStream; 
 
public class Main { 
 
    public static void main(String[] args); throws Exception { 
        DomainObject obj = new DomainObject();; 
        obj.setAge(29);; 
        obj.setName("fankai");; 
        FileOutputStream fos = new FileOutputStream("DomainObject");; 
        ObjectOutputStream oos = new ObjectOutputStream(fos);; 
        oos.writeObject(obj);; 
        oos.close();; 
        fos.close();; 
   
 


DomainObject是我们准备序列化的类,在Main里面,我们new一个DomainObject的对象,然后赋值,最后把该对象序列化到一个硬盘文件中。

然后使用一种支持二进制编辑器,例如UltraEdit打开这个文件,看看Java都对DomainObject序列化了哪些信息,你就什么都明白了。

为了更方便观察,我使用Linux下面的strings去提取文本信息,输出为:

robbin@linux:~> strings DomainObject
com.javaeye.DomainObject
ageL
namet
Ljava/lang/String;xp
fankai

这些信息很直观的告诉我们序列化都保存了些什么内容:
1)对象的类型
2)对象属性的类型
3)对象属性的值

并没有什么方法签名的信息,更不要说什么序列化方法了。

然后我们再做一个试验,给DomainObject增加两个方法:

 

我们增加了toString方法和doSomeWork方法,按照你的理论,如果序列化方法的话,产生的文件体积必然增大。记录一下文件体积,92Byte,好了,删除,运行程序,生成了新的文件,看一下体积,还是92Byte!
 
拿到Linux下面再提取一下字符串:

robbin@linux:~> strings DomainObject
com.javaeye.DomainObject
ageL
namet
Ljava/lang/String;xp
fankai

完全一模一样!

然后我们再做第三个试验,这次把DomainObject的两个属性以及相关方法删除掉:
 

 

view plaincopy to clipboardprint?
01.package com.javaeye;    
02.   
03.import java.io.Serializable;    
04.   
05.public class DomainObject  implements Serializable {    
06.   
07.    public String toString(); {    
08.        return "This is a serializable test!";    
09.      
10.        
11.    public void doSomeWork(); {    
12.        System.out.println("hello");;    
13.      
14.}   
29. 
30. 
31.修改Main类如下:  
32. 
33.package com.javaeye;    
34.   
35.import java.io.FileOutputStream;    
36.import java.io.ObjectOutputStream;    
37.   
38.public class Main {    
39.   
40.    public static void main(String[] args); throws Exception {    
41.        DomainObject obj = new DomainObject();;    
42.   
43.        FileOutputStream fos = new FileOutputStream("DomainObject");;    
44.        ObjectOutputStream oos = new ObjectOutputStream(fos);;    
45.        oos.writeObject(obj);;    
46.        oos.close();;    
47.        fos.close();;    
48.      
49.   
50.}   
51.


按照你的理论,如果序列化方法的话,我们必然应该在文件里面发现方法的签名信息,甚至方法里面包含的字符串,好了,再运行一遍,然后打开看一下吧!文件现在体积变成了45Byte,拿到Linux下面提取一下信息:
 
robbin@linux:~> strings DomainObject
com.javaeye.DomainObject

只有对象的类型信息,再无其它东西了!

请记住序列化机制只保存对象的类型信息,属性的类型信息和属性值,和方法没有什么关系,你就是给这个类增加10000个方法,序列化内容也不会增加任何东西,不要想当然的臆测自己不了解的知识,动手去做!
 
序列化在 Effective Java 中讲得很清楚啊, 一般认为只声明实现 implements 接口, 不提供自定义的序列化形式是不负责任的做法, 这样可能导致比较多的问题比如类的不同版本之间的兼容性, 看看 Effective Java 中的条目吧

    谨慎地实现 Serialiable
   
    为了继承而设计的类应该很少实现 Serialiable, 接口也应该很少会扩展它. 如果违反了这条规则, 则扩展这个类或者实现这个接口的程序员会背上沉重的负担.
   
     若没有认真考虑默认序列化形式是否合适, 则不要接受这种形式
  
    即使你确定了默认序列化形式是合适的, 通常你仍然要提供一个 readObject方法以保证约束关系和约束性
  
    不管你选择了哪种序列化形式, 你都要为自己编写的每个可序列化的类声明一个显式的序列版本 UID (serialVersionUID)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值