可序列化理解(转载)

Posted 豆腐全家

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了可序列化理解(转载)相关的知识,希望对你有一定的参考价值。

转载地址:http://rabbitsfish-163-com.iteye.com/blog/1121318

什么是序列化 
java中的序列化(serialization)机制能够将一个实例对象的状态信息写入到一个字节流中,使其可以通过socket进行传输、或者持久化存储到数据库或文件系统中;然后在需要的时候,可以根据字节流中的信息来重构一个相同的对象。序列化机制在java中有着广泛的应用,EJB、 RMI等技术都是以此为基础的。 
Java的序列化机制只序列化对象的属性值,而不会去序列化什么所谓的方法。其实这个问题简单思考一下就可以搞清楚,方法是不带状态的,就是一些指令,指令是不需要序列化的,只要你的JVM classloader可以load到这个类,那么类方法指令自然就可以获得。序列化真正需要保存的只是对象属性的值,和对象的类型。 

正确使用序列化机制 
一般而言,要使得一个类可以序列化,只需简单实现java.io.Serializable接口即可。该接口是一个标记式接口,它本身不包含任何内容,实现了该接口则表示这个类准备支持序列化的功能。 
利用对象序列化可以进行对象的“深复制”。 
类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。 
要允许不可序列化类的子类型序列化,可以假定该子类型负责保存和恢复超类型的公用 (public)、受保护的 (protected) 和(如果可访问)包 (package) 字段的状态。仅在子类型扩展的类有一个可访问的无参数构造方法来初始化该类的状态时,才可以假定子类型有此职责。如果不是这种情况,则声明一个类为可序列化类是错误的。该错误将在运行时检测到。 
对象的默认序列化机制写入的内容是:对象的类,类签名,以及非瞬态和非静态字段的值。其他对象的引用(瞬态和静态字段除外)也会导致写入那些对象。可使用引用共享机制对单个对象的多个引用进行编码,这样即可将对象的图形恢复为最初写入它们时的形状。 
序列化操作不写出没有实现 java.io.Serializable 接口的任何对象的字段。不可序列化的 Object 的子类可以是可序列化的。在此情况下,不可序列化的类必须有一个无参数构造方法,以便允许初始化其字段。在此情况下,子类负责保存和恢复不可序列化的类的状态。经常出现的情况是,该类的字段是可访问的(public、package 或 protected),或者存在可用来恢复状态的 get 和 set 方法。 

一个类是可序列化的,意味着这个类的所有成员变量都必须是可以序列化的。一定注意是所有的成员变量,static变量,还有瞬态transient的除外。 

例如: 
public class SerializableB { 

    int b =10 ; 
    
    public SerializableB(int b){ 
        this.b = b ; 
    } 


public class SerializableA extends SerializableB implements Serializable { 

    public SerializableA(int b) { 
        super(b); 
    } 
    /** 
     * 
     */ 
    private static final long serialVersionUID = 1L; 
    public int a = 1 ; 
      



public class TestSerial { 
    
    

    public static void main(String[] args) throws IOException, ClassNotFoundException { 
        SerializableA a = new SerializableA(100); 
        String file = "C:/b.out" ; 
        FileOutputStream out = new FileOutputStream(file) ; 
        ObjectOutputStream objOut = new ObjectOutputStream(out); 
        System.out.println(a.b) ; 
        System.out.println(a.a) ; 
        objOut.writeObject(a); 
        objOut.flush(); 
        objOut.close(); 
        
//        FileInputStream in = new FileInputStream("C:/b.out"); 
//        ObjectInputStream objIn = new ObjectInputStream(in); 
//        SerializableA b =  (SerializableA)objIn.readObject(); 
//        System.out.println(b.b) ; 
//        System.out.println(b.a) ; 
        
    } 



这时候你要是序列化时,由于父类不可序列化,故序列化没有问题,但是反序列化出问题了。 
Exception in thread "main" java.io.InvalidClassException: com.steven.serializable.SerializableA; com.steven.serializable.SerializableA; no valid constructor 
    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 com.steven.serializable.TestSerial.main(TestSerial.java:27) 
Caused by: java.io.InvalidClassException: com.steven.serializable.SerializableA; no valid constructor 
    at java.io.ObjectStreamClass.<init>(Unknown Source) 
    at java.io.ObjectStreamClass.lookup(Unknown Source) 
    at java.io.ObjectStreamClass.initNonProxy(Unknown Source) 
    at java.io.ObjectInputStream.readNonProxyDesc(Unknown Source) 
    at java.io.ObjectInputStream.readClassDesc(Unknown Source) 
    ... 4 more 
首先SerializableB  不可序列化。其次反序列化时,由于SerializableB  没有提供一个默认的无参构造器,故报错。你只需要加一个无参构造器构就是正确的。子类会负责调用父类的无参构造器来恢复不可序列化的类的状态。 

Enum 常量的序列化不同于普通的 serializable 或 externalizable 对象。enum 常量的序列化形式只包含其名称;常量的字段值不被传送。为了序列化 enum 常量,ObjectOutputStream 需要写入由常量的名称方法返回的字符串。与其他 serializable 或 externalizable 对象一样,enum 常量可以作为序列化流中后续出现的 back 引用的目标。用于序列化 enum 常量的进程不可定制;在序列化期间,由 enum 类型定义的所有类特定的 writeObject 和 writeReplace 方法都将被忽略。类似地,任何 serialPersistentFields 或 serialVersionUID 字段声明也将被忽略,所有 enum 类型都有一个 0L 的固定的 serialVersionUID。 
基本数据(不包括 serializable 字段和 externalizable 数据)以块数据记录的形式写入 ObjectOutputStream 中。块数据记录由头部和数据组成。块数据部分包括标记和跟在部分后面的字节数。连续的基本写入数据被合并在一个块数据记录中。块数据记录的分块因子为 1024 字节。每个块数据记录都将填满 1024 字节,或者在终止块数据模式时被写入。调用 ObjectOutputStream 方法 writeObject、defaultWriteObject 和 writeFields 最初只是终止所有现有块数据记录。 
Enum 常量的反序列化不同于普通的 serializable 或 externalizable 对象。Enum 常量的序列化形式只包含其名称;不传送常量的字段值。要反序列化 enum 常量,ObjectInputStream 需要从流中读取常量的名称;然后将 enum 常量的基本类型和接收到的常量名称作为参数,调用静态方法 Enum.valueOf(Class, String) 获取反序列化的常量。与其他 serializable 或 externalizable 对象一样,enum 常量可以作为序列化流中随后出现的反向引用的目标。不可以自定义 enum 常量的反序列化进程:在反序列化期间,enum 类型所定义的任何与类有关的 readObject、readObjectNoData 和 readResolve 方法都将被忽略。类似地,任何 serialPersistentFields 或 serialVersionUID 字段声明也将被忽略(所有 enum 类型都有一个固定的 0L 的 serialVersionUID)。 

如下例定义了类Person,并声明其可以序列化。 


Java 代码 1.public class Person implements java.io.Serializable {}  
public class Person implements java.io.Serializable {} 


序列化机制是通过java.io.ObjectOutputStream类和java.io.ObjectInputStream类来实现的。在序列化 (serialize)一个对象的时候,会先实例化一个ObjectOutputStream对象,然后调用其writeObject()方法;在反序列化(deserialize)的时候,则会实例化一个ObjectInputStream对象,然后调用其readObject()方法。下例说明了这一过程。 

Java 代码 1.public void serializeObject(){  
2.     String fileName = "ser.out";  
3.     FileOutputStream fos = new FileOutputStream(fileName);  
4.     ObjectOutputStream oos = new ObjectOutputStream(fos);  
5.     oos.writeObject(new Person());  
6.     oos.flush();  
7.}  
8.  
9.public void deserializeObject(){  
10.     String fileName = "ser.out";  
11.     FileInputStream fos = new FileInputStream(fileName);  
12.     ObjectInputStream oos = new ObjectInputStream(fos);  
13.     Person p = oos.readObject();  
14.}  
public void serializeObject(){ 
     String fileName = "ser.out"; 
     FileOutputStream fos = new FileOutputStream(fileName); 
     ObjectOutputStream oos = new ObjectOutputStream(fos); 
     oos.writeObject(new Person()); 
     oos.flush(); 


public void deserializeObject(){ 
     String fileName = "ser.out"; 
     FileInputStream fos = new FileInputStream(fileName); 
     ObjectInputStream oos = new ObjectInputStream(fos); 
     Person p = oos.readObject(); 


上例中我们对一个Person对象定义了序列化和反序列化的操作。但如果Person类是不能序列化的话,即对不能序列化的类进行序列化操作,则会抛出 java.io.NotSerializableException异常。 
JVM中有一个预定义的序列化实现机制,即默认调用 ObjectOutputStream.defaultWriteObject() 和 ObjectInputStream.defaultReadObject() 来执行序列化操作。如果想自定义序列化的实现,则必须在声明了可序列化的类中实现 writeObject()和readObject()方法。 

几种使用情况 
一般在序列化一个类A的时候,有以下三种情况: 
[list=3] 
•类A没有父类,自己实现了Serializable接口 
•类A有父类B,且父类实现了Serializable接口 
•类A有父类B,但父类没有实现Serializable接口,自己实现了Serializable接口 
[/list] 
对于第一种情况,直接实现Serializable接口即可。 
对于第二种情况,因为父类B已经实现了Serializable接口,故类A无需实现此接口;如果父类实现了writeObject()和 readObject(),则使用此方法,否则直接使用默认的机制。 
对于第三种情况,有一点要特别注意,在父类B中一定要有一个无参的构造函数,这是因为在反序列化的过程中并不会使用声明为可序列化的类A的任何构造函数,而是会调用其没有申明为可序列化的父类B的无参构造函数。 

序列化机制的一些问题 

      •性能问题 

 

      为了序列化类A一个实例对象,所需保存的全部信息如下: 

 

      1. 与此实例对象相关的全部类的元数据(metadata)信息;因为继承关系,类A的实例对象也是其任一父类的对象。因而,需要将整个继承链上的每一个类的元数据信息,按照从父到子的顺序依次保存起来。 

 

      2. 类A的描述信息。此描述信息中可能包含有如下这些信息:类的版本ID(version ID)、表示是否自定义了序列化实现机制的标志、可序列化的属性的数目、每个属性的名字和值、及其可序列化的父类的描述信息。 

 

      3. 将实例对象作为其每一个超类的实例对象,并将这些数据信息都保存起来。 

 

      在RMI等远程调用的应用中,每调用一个方法,都需要传递如此多的信息量;久而久之,会对系统的性能照成很大的影响。 

 

      •版本信息 

 

      当用readObject()方法读取一个序列化对象的byte流信息时,会从中得到所有相关类的描述信息以及示例对象的状态数据;然后将此描述信息与其本地要构造的类的描述信息进行比较,如果相同则会创建一个新的实例并恢复其状态,否则会抛出异常。这就是序列化对象的版本检测。JVM中默认的描述信息是使用一个长整型的哈希码(hashcode)值来表示,这个值与类的各个方面的信息有关,如类名、类修饰符、所实现的接口名、方法和构造函数的信息、属性的信息等。因而,一个类作一些微小的变动都有可能导致不同的哈希码值。例如开始对一个实例对象进行了序列化,接着对类增加了一个方法,或者更改了某个属性的名称,当再想根据序列化信息来重构以前那个对象的时候,此时两个类的版本信息已经不匹配,不可能再恢复此对象的状态了。要解决这个问题,可能在类中显示定义一个值,如下所示: 



      Java 代码 1.private static final long serialVersionUID = ALongValue;  

 

      private static final long serialVersionUID = ALongValue; 



      这样,序列化机制会使用这个值来作为类的版本标识符,从而可以解决不兼容的问题。但是它却引入了一个新的问题,即使一个类作了实质性的改变,如增加或删除了一些可序列化的属性,在这种机制下仍然会认为这两个类是相等的。 



一种更好的选择 
作为实现Serializable接口的一种替代方案,实现java.io.Externalizable接口同样可以标识一个类为可序列化。 
Externalizable接口中定义了以下两个方法: 

Java 代码 1.public void readExternal(ObjectInput in);  
2.public void writeExternal(ObjectOutput out);  
public void readExternal(ObjectInput in); 
public void writeExternal(ObjectOutput out); 

这两个方法的功能与 readObject()和writeObject()方法相同,任何实现了Externalizable接口的类都需要这实现两个函数来定义其序列化机制。 
使用Externalizable比使用Serializable有着性能上的提高。前者序列化一个对象,所需保存的信息比后者要小,对于后者所需保存的第3个方面的信息,前者不需要访问每一个父类并使其保存相关的状态信息,而只需简单地调用类中实现的writeExternal()方法即可。 

问题总结: 
1、实现Serializable回导致发布的API难以更改,并且使得package-private和private 
这两个本来封装的较好的咚咚也不能得到保障了 
2、Serializable会为每个类生成一个序列号,生成依据是类名、类实现的接口名、 
public和protected方法,所以只要你一不小心改了一个已经publish的API,并且没有自 
己定义一个long类型的叫做serialVersionUID的field,哪怕只是添加一个getXX,就会 
让你读原来的序列化到文件中的东西读不出来(不知道为什么要把方法名算进去?) 
3、不用构造函数用Serializable就可以构造对象,看起来不大合理,这被称为 
extralinguistic mechanism,所以当实现Serializable时应该注意维持构造函数中所维 
持的那些不变状态 
4、增加了发布新版本的类时的测试负担 
5、1.4版本后,JavaBeans的持久化采用基于XML的机制,不再需要Serializable 
6、设计用来被继承的类时,尽量不实现Serializable,用来被继承的interface也不要 
继承Serializable。但是如果父类不实现Serializable接口,子类很难实现它,特别是 
对于父类没有可以访问的不含参数的构造函数的时候。所以,一旦你决定不实现 
Serializable接口并且类被用来继承的时候记得提供一个无参数的构造函数 
7、内部类还是不要实现Serializable好了,除非是static的,(偶也觉得内部类不适合 
用来干这类活的) 
8、使用一个自定义的序列化方法 
9、不管你选择什么序列化形式,声明一个显式的UID: 

private static final long serialVersionUID = randomLongValue; 

10、不需要序列化的东西使用transient注掉它吧,别什么都留着 

11、writeObject/readObject重载以完成更好的序列化 


最近在看web sevice 方面的东西,顺便看了下序列化,懂了不少啊 : 

从MarshalByRefObject派生的类和有[Serializable]的类都可以跨越应用程序域作为参数传递。 
从MarshalByRefObject派生的类按引用封送,有[Serializable]标志的类,按值封送。 
如果此类即从MarshalByRefObject派生,也有[Serializable]标志也是按引用封送。 


MarshalByRefObject和Serializable 
序列化有3种情况: 

1.序列化为XML格式: 
在webservice里,写个web method,传个自定义类做参数,就是这种情况。系统会帮你搞定,把自定义的类转换为默认XML格式。 
2.序列化为2进制: 
要加[Serializable]标志,可以把私有变量和公共变量都序列化。 
3.序列化为soap格式: 
需要实现ISerializable接口,定义序列化函数ISerializable.GetObjectData,和还原序列化的构造函数。 


Java的序列化算法                    序列化算法一般会按步骤做如下事情:                    ◆将对象实例相关的类元数据输出。                    ◆递归地输出类的超类描述直到不再有超类。                    ◆类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。                    ◆从上至下递归输出实例的数据                    我们用另一个更完整覆盖所有可能出现的情况的例子来说明:                    1.class parent implements Serializable {  2. 3.       int parentVersion = 10;  4. 5.}  6. 7.   8. 9.class contain implements Serializable{  10. 11.       int containVersion = 11;  12. 13.}  14. 15.public class SerialTest extends parent implements Serializable {  16. 17.       int version = 66;  18. 19.       contain con = new contain();  20. 21.   22. 23.       public int getVersion() {  24. 25.              return version;  26. 27.       }  28. 29.       public static void main(String args[]) throws IOException {  30. 31.              FileOutputStream fos = new FileOutputStream("temp.out");  32. 33.              ObjectOutputStream oos = new ObjectOutputStream(fos);  34. 35.              SerialTest st = new SerialTest();  36. 37.              oos.writeObject(st);  38. 39.              oos.flush();  40. 41.              oos.close();  42. 43.       }  44. 45.}                                                           这个例子是相当的直白啦。SerialTest类实现了Parent超类,内部还持有一个Container对象。 

序列化后的格式如下: 

AC ED 00 05 73 72 00 0A 53 65 72 69 61 6C 54 65 

73 74 05 52 81 5A AC 66 02 F6 02 00 02 49 00 07 

76 65 72 73 69 6F 6E 4C 00 03 63 6F 6E 74 00 09 

4C 63 6F 6E 74 61 69 6E 3B 78 72 00 06 70 61 72 

65 6E 74 0E DB D2 BD 85 EE 63 7A 02 00 01 49 00 

0D 70 61 72 65 6E 74 56 65 72 73 69 6F 6E 78 70 

00 00 00 0A 00 00 00 42 73 72 00 07 63 6F 6E 74 

61 69 6E FC BB E6 0E FB CB 60 C7 02 00 01 49 00 

0E 63 6F 6E 74 61 69 6E 56 65 72 73 69 6F 6E 78 

70 00 00 00 0B 

我们来仔细看看这些字节都代表了啥。开头部分,见颜色: 

1.AC ED: STREAM_MAGIC. 声明使用了序列化协议. 
2.00 05: STREAM_VERSION. 序列化协议版本. 
3.0x73: TC_OBJECT. 声明这是一个新的对象.  
序列化算法的第一步就是输出对象相关类的描述。例子所示对象为SerialTest类实例, 
因此接下来输出SerialTest类的描述。见颜色: 

1.0x72: TC_CLASSDESC. 声明这里开始一个新Class。 
2.00 0A: Class名字的长度. 
3.53 65 72 69 61 6c 54 65 73 74: SerialTest,Class类名. 
4.05 52 81 5A AC 66 02 F6: SerialVersionUID, 序列化ID,如果没有指定, 
则会由算法随机生成一个8byte的ID. 
5.0x02: 标记号. 该值声明该对象支持序列化。 
6.00 02: 该类所包含的域个数。 
接下来,算法输出其中的一个域,int version=66;见颜色: 

1.0x49: 域类型. 49 代表"I", 也就是Int. 
2.00 07: 域名字的长度. 
3.76 65 72 73 69 6F 6E: version,域名字描述. 
然后,算法输出下一个域,contain con = new contain();这个有点特殊,是个对象。 
描述对象类型引用时需要使用JVM的标准对象签名表示法,见颜色: 

1.0x4C: 域的类型. 
2.00 03: 域名字长度. 
3.63 6F 6E: 域名字描述,con 
4.0x74: TC_STRING. 代表一个new String.用String来引用对象。 
5.00 09: 该String长度. 
6.4C 63 6F 6E 74 61 69 6E 3B: Lcontain;, JVM的标准对象签名表示法. 
7.0x78: TC_ENDBLOCKDATA,对象数据块结束的标志 
.接下来算法就会输出超类也就是Parent类描述了,见颜色: 

1.0x72: TC_CLASSDESC. 声明这个是个新类. 
2.00 06: 类名长度. 
3.70 61 72 65 6E 74: parent,类名描述。 
4.0E DB D2 BD 85 EE 63 7A: SerialVersionUID, 序列化ID. 
5.0x02: 标记号. 该值声明该对象支持序列化. 
6.00 01: 类中域的个数. 
下一步,输出parent类的域描述,int parentVersion=100;同见颜色: 

1.0x49: 域类型. 49 代表"I", 也就是Int. 
2.00 0D: 域名字长度. 
3.70 61 72 65 6E 74 56 65 72 73 69 6F 6E: parentVersion,域名字描述。 
4.0x78: TC_ENDBLOCKDATA,对象块结束的标志。 
5.0x70: TC_NULL, 说明没有其他超类的标志。. 
到此为止,算法已经对所有的类的描述都做了输出。下一步就是把实例对象的实际值输出了。这时候是从parent Class的域开始的,见颜色: 

1.00 00 00 0A: 10, parentVersion域的值. 
还有SerialTest类的域: 

1.00 00 00 42: 66, version域的值. 
再往后的bytes比较有意思,算法需要描述contain类的信息,要记住, 
现在还没有对contain类进行过描述,见颜色: 

1.0x73: TC_OBJECT, 声明这是一个新的对象. 
2.0x72: TC_CLASSDESC声明这里开始一个新Class. 
3.00 07: 类名的长度. 
4.63 6F 6E 74 61 69 6E: contain,类名描述. 
5.FC BB E6 0E FB CB 60 C7: SerialVersionUID, 序列化ID. 
6.0x02: Various flags. 标记号. 该值声明该对象支持序列化 
7.00 01: 类内的域个数。 
.输出contain的唯一的域描述,int containVersion=11; 

1.0x49: 域类型. 49 代表"I", 也就是Int.. 
2.00 0E: 域名字长度. 
3.63 6F 6E 74 61 69 6E 56 65 72 73 69 6F 6E: containVersion, 域名字描述. 
4.0x78: TC_ENDBLOCKDATA对象块结束的标志. 
这时,序列化算法会检查contain是否有超类,如果有的话会接着输出。 

1.0x70:TC_NULL,没有超类了。 
最后,将contain类实际域值输出。 

1.00 00 00 0B: 11, containVersion的值. 

以上是关于可序列化理解(转载)的主要内容,如果未能解决你的问题,请参考以下文章

转载序列化和反序列化

理解php反序列化漏洞

转载:UML学习-----序列图(silent)

序列化:基本理解和文档

java序列化对象简单理解

反射和可序列化