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

来源:互联网 发布:生物多样性 数据 调查 编辑:程序博客网 时间:2024/05/01 16:20
本文转自:http://blog.sina.com.cn/s/blog_4e345ce70100rt86.html

一、什么是序列化?

“对象序列化”(Object Serialization)是 Java1.1就开始有的特性。 简单地说,就是可以将一个对象(标志对象的类型)及其状态转换为字节码,保存起来(可以保存在数据库,内存,文件等),然后可以在适当的时候再将其状态恢复(也就是反序列化)。serialization 不但可以在本机做,而且可以经由网络操作。它自动屏蔽了操作系统的差异,字节顺序等。比如,在 Windows 平台生成一个对象并序列化之,然后通过网络传到一台 Unix 机器上,然后可以在这台Unix机器上正确地重构(deserialization)这个对象。 不必关心数据在不同机器上如何表示,也不必关心字节的顺序或者其他任何细节。

另外,还应明白以下几点:
a. java.io.Serializable接口没有任何方法属性域,实现它的类只是从语义上表明自己是可以序列化的。
b. 在对一个 Serializable(可序列化)对象进行重新装配的过程中,不会调用任何构建器(甚至默认构建器)。整个对象都是通过从 InputStream 中取得数据恢复的。
c. 如是要一个类是可序列化的,那么它的子类也是可序列化的。

二、序列化在什么时候用?
a)当你想把的内存中的对象保存到一个文件中或者数据库中时候;
b)当你想用套接字在网络上传送对象的时候;
c)当你想通过RMI传输对象的时候;

三 序列化过程
java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
  只有实现了Serializable和Externalizable接口的类的对象才能被序列化。Externalizable接口继承自Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式 。

  对象序列化包括如下步骤:
  1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
  2) 通过对象输出流的writeObject()方法写对象。
  
  对象反序列化的步骤如下:
  1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
  2) 通过对象输入流的readObject()方法读取对象。

  下面让我们来看一个对应的例子,类的内容如下:

package com.company;import java.io.*;import java.util.Date;public class ObjectSaver {    public static void main(String[] args) throws Exception {        ObjectOutputStream out = new ObjectOutputStream               (new FileOutputStream("D:""objectFile.obj"));        //序列化对象        Customer customer = new Customer("阿蜜果", 24);        out.writeObject("你好!");        out.writeObject(new Date());        out.writeObject(customer);        out.writeInt(123); //写入基本类型数据        out.close();        //反序列化对象        ObjectInputStream in = new ObjectInputStream               (new FileInputStream("D:""objectFile.obj"));        System.out.println("obj1=" + (String) in.readObject());        System.out.println("obj2=" + (Date) in.readObject());        Customer obj3 = (Customer) in.readObject();        System.out.println("obj3=" + obj3);        int obj4 = in.readInt();        System.out.println("obj4=" + obj4);        in.close();    }}class Customer implements Serializable {    private String name;    private int age;    public Customer(String name, int age) {        this.name = name;        this.age = age;    }    public String toString() {        return "name=" + name + ", age=" + age;    }}

输出结果如下:

obj1=你好!

obj2=Sat Sep 15 22:02:21 CST 2007

obj3=name=阿蜜果, age=24

obj4=123

四、如何序列化
在Java里,如果要使一个类可以序列化或反序列化,只需要实现 java.io.Serializable 接口。如果类没有实现这个接口,则一般来说不能将他们的状态进行序列化与反序列化。注意,这里我说”一般来说”,是因为Java还提供了另外一个接口 java.io.Externalizable,关于这个接口的使用我将会在下面单独说明。

  • 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手册就可以查到,我以为是不应该犯这种基本概念错误的。
我们可以做一个简单的小试验,来证实一下:

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的两个属性以及相关方法删除掉:

import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectOutputStream;import java.io.Serializable;public class Main {    public class DomainObject implements Serializable {    }    public static void main(String[] args) throws Exception {        DomainObject obj = new DomainObject();        FileOutputStream fos = new FileOutputStream("DomainObject");        ObjectOutputStream oos = new ObjectOutputStream(fos);        try {            oos.writeObject(obj);        } catch (IOException e) {            e.printStackTrace();        }        oos.close();        fos.close();    }}

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

robbin@linux:~> strings DomainObject
com.javaeye.DomainObject

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

请记住序列化机制只保存对象的类型信息,属性的类型信息和属性值,和方法没有什么关系,你就是给这个类增加10000个方法,序列化内容也不会增加任何东西,不要想当然的臆测自己不了解的知识,动手去做!

序列化在 Effective Java 中讲得很清楚啊, 一般认为只声明实现 implements 接口, 不提供自定义的序列化形式是不负责任的做法, 这样可能导致比较多的问题比如类的不同版本之间的兼容性, 看看 Effective Java 中的条目吧

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

0 0
原创粉丝点击