JAVA序列化机制的深入研究

Posted zhaozheng7758

tags:

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

1、java序列化简介

序列化就是指对象通过写出描述自己状态的数值来记录自己的过程,即将对象表示成一系列有序字节,java提供了将对象写入流和从流中恢复对象的方法。对象能包含其它的对象,而其它的对象又可以包含另外的对象。JAVA序列化能够自动的处理嵌套的对象。对于一个对象的简单域,writeObject()直接将其值写入流中。当遇到一个对象域时,writeObject()被再次调用,如果这个对象内嵌另一个对象,那么,writeObject()又被调用,直到对象能被直接写入流为止。程序员所需要做的是将对象传入ObjectOutputStreamwriteObject()方法,剩下的将有系统自动完成。

要实现序列化的类必须实现的java.io.Serializablejava.io.Externalizable接口,否则将产生一个NotSerializableException。该接口内部并没有任何方法,它只是一个"tagging interface",仅仅"tags"它自己的对象是一个特殊的类型。类通过实现 java.io.Serializable接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。Java"对象序列化"能让你将一个实现了Serializable接口的对象转换成一组byte,这样日后要用这个对象时候,你就能把这些byte数据恢复出来,并据此重新构建那个对象了。

序列化图示


反序列化图示


在序列化的时候,writeObjectreadObject之间是有先后顺序的。readObject将最先writeobject read出来。用数据结构的术语来讲就称之为先进先出!

2、序列化的必要性及目的

Java中,一切都是对象,在分布式环境中经常需要将Object从这一端网络或设备传递到另一端。这就需要有一种可以在两端传输数据的协议。Java序列化机制就是为了解决这个问题而产生。

Java序列化支持的两种主要特性:

  • Java RMI使本来存在于其他机器的对象可以表现出就象本地机器上的行为。
  • 将消息发给远程对象时,需要通过对象序列化来传输参数和返回值.

Java序列化的目的:

  • 支持运行在不同虚拟机上不同版本类之间的双向通讯;
  • 定义允许JAVA类读取用相同类较老版本写入的数据流的机制;
  • 定义允许JAVA类写用相同类较老版本读取的数据流的机制;
  • 提供对持久性和RMI的序列化;
  • 产生压缩流且运行良好以使RMI能序列化;
  • 辨别写入的是否是本地流;
  • 保持非版本化类的低负载;

3、序列化异常

    序列化对象期间可能抛出6种异常:

  •  InvalidClassException 通常在重序列化流无法确定类型时或返回的类无法在取得对象的系统中表示时抛出此异常。异常也在恢复的类不声明为public时或没有public缺省(无变元)构造器时抛出。
  •  NotSerializableException 通常由具体化对象(负责自身的重序列化)探测到输入流错误时抛出。错误通常由意外不变量值指示,或者表示要序列化的对象不可序列化。
  • StreamCorruptedException 在存放对象的头或控制数据无效时抛出。
  • OptionalDataException 流中应包含对象但实际只包含原型数据时抛出。
  • ClassNotFoundException 流的读取端找不到反序列化对象的类时抛出。
  • IOException  要读取或写入的对象发生与流有关的错误时抛出。

4、序列化一个对象

序列化一个对象,以及对序列化后的对象进行操作,需要遵循以下3点:

1、 一个对象能够序列化的前提是实现Serializable接口或Externalizable接口,Serializable接口没有方法,更像是个标记。有了这个标记的Class就能被序列化机制处理。

2、 写个程序将对象序列化并输出。ObjectOutputStream能把Object输出成Byte流。

3、 要从持久的文件中读取Bytes重建对象,我们可以使用ObjectInputStream。 

在序列化时,有几点要注意的:

  •  当一个对象被序列化时,只序列化对象的非静态成员变量,不能序列化任何成员方法和静态成员变量。
  •  如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存。
  • 如果一个可序列化的对象包含对某个不可序列化的对象的引用,那么整个序列化操作将会失败,并且会抛出一个NotSerializableException。可以通过将这个引用标记为transient,那么对象仍然可以序列化。对于一些比较敏感的不想序列化的数据,也可以采用该标识进行修饰。

5、对象的序列化格式

5.1 简单对象的序列化介绍

package com.asc.alibaba.base;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
publicclassTestSerialimplements Serializable
    publicbyteversion = 100;
    publicbytecount = 0;
    publicstaticvoid main(String[] args)throws IOException, ClassNotFoundException 
        FileOutputStream fos = new FileOutputStream("temp.out");
        ObjectOutputStream oos =new ObjectOutputStream(fos);
        TestSerialize testSerialize =new TestSerialize();
        oos.writeObject(testSerialize);
        oos.flush();
        oos.close();
        
    

将一个对象序列化后是什么样子呢?打开刚才将对象序列化输出的temp.out文件,以16进制方式显示。内容应该如下:

AC ED 00 05 73 72 00 0A 53 65 72 69 61 6C 54 65
73 74 A0 0C 34 00 FE B1 DD F9 02 00 02 42 00 05
63 6F 75 6E 74 42 00 07 76 65 72 73 69 6F 6E 78
70 00 64
这一堆字节就是用来描述序列化以后的TestSerial对象的,我们注意到TestSerial类中只有两个域:

public byte version = 100;

public byte count = 0;

且都是byte型,理论上存储这两个域只需要2个byte,但是实际上temp.out占据空间为51bytes,也就是说除了数据以外,还包括了对序列化对象的其他描述。

开头部分,见颜色

²  AC ED: STREAM_MAGIC. 声明使用了序列化协议.

²  00 05: STREAM_VERSION. 序列化协议版本.

²  0x73: TC_OBJECT. 声明这是一个新的对象.  

输出TestSerial类的描述。见颜色:

²  0x72: TC_CLASSDESC. 声明这里开始一个新Class。

²  00 0A: Class名字的长度.

²  53 65 72 69 61 6c 54 65 73 74: TestSerial,Class类名.

²  05 52 81 5A AC 66 02 F6: SerialVersionUID, 序列化ID,如果没有指定,
则会由算法随机生成一个8byte的ID.

²  0x02: 标记号. 该值声明该对象支持序列化。

²  00 02: 该类所包含的域个数。

输出域的信息,见颜色

²  0x42: 域类型. 42 代表"B", 也就是byte;

²  00 05: 域名字的长度;

²  636F 75 6E 74: count,域名字描述count;

²  0x42: 域类型. 42 代表"B", 也就是byte;

²  00 07: 域名字的长度;

²  76 65 72 73 69 6F 6E 78 70: version,域名字描述version;
块的结束标记:见颜色
0x78: TC_ENDBLOCKDATA,对象块结束的标志。

0x70:TC_NULL,没有超类了。

输出域的值信息,见颜色:
²  00: 域值为00;
²  64: 域值为100;

5.2 复杂对象的序列化介绍

package com.asc.alibaba.base;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;

classContain implements Serializable 
    publicintcontainVersion = 11;


classParent implements Serializable 
    publicintparentVersion = 10;


publicclassSerialTest extends Parent implements Serializable 
    publicint      version   = 66;
    public Containcon = new Contain();

    publicint getVersion() 
        returnversion;
    

    publicstaticvoid main(String[] args)throws IOException 
        FileOutputStream fos = new FileOutputStream("temp1.out");
        ObjectOutputStream oos =new ObjectOutputStream(fos);
        SerialTest testSerialize =new SerialTest();
        oos.writeObject(testSerialize);
        oos.flush();
        oos.close();
        FileInputStream fis = new FileInputStream("temp1.out");
        byte[] bb =newbyte[200];
        while (fis.read(bb) != -1) 
            for (byte b : bb)                System.out.print(Integer.toHexString(b));
                System.out.print(" ");
            
        
    

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 7000 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

开头部分,见颜色

²  ACED: STREAM_MAGIC. 声明使用了序列化协议;

²  0005: STREAM_VERSION. 序列化协议版本;

²  0x73: TC_OBJECT. 声明这是一个新的对象;

输出TestSerial类的描述。见颜色:

²  0x72: TC_CLASSDESC. 声明这里开始一个新Class;

²  000A: Class名字的长度;

²  5365 72 69 61 6c 54 65 73 74: SerialTest,Class类名;

²  0552 81 5A AC 66 02 F6: SerialVersionUID, 序列化ID,如果没有指定,则会由算法随机生成一个8byte的ID;

²  0x02: 标记号. 该值声明该对象支持序列化;

²  0002: 该类所包含的域个数;

输出域的信息,见颜色

²  0x49: 域类型. 49 代表"I", 也就是Int.

²  00 07: 域名字的长度.

²  76 65 72 73 69 6F 6E: version,域名字描述.

算法输出下一个域,contain con = new contain();这个有点特殊,是个对象。描述对象类型引用时需要使用JVM的标准对象签名表示法,见颜色

²  0x4C: 域的类型;

²  0003: 域名字长度;

²  636F 6E: 域名字描述,con;

²  0x74: TC_STRING. 代表一个new String.用String来引用对象;

²  0009: 该String长度;

²  4C63 6F 6E 74 61 69 6E 3B: Lcontain;,JVM的标准对象签名表示法;

²  0x78: TC_ENDBLOCKDATA,对象数据块结束的标志;

算法就会输出超类也就是Parent类描述了,见颜色

²  0x72: TC_CLASSDESC. 声明这个是个新类;

²  00 06: 类名长度;

²  70 6172 65 6E 74: parent,类名描述;

²  0E DBD2 BD 85 EE 63 7A: SerialVersionUID, 序列化ID;

²  0x02: 标记号. 该值声明该对象支持序列化;

²  00 01: 类中域的个数;

输出parent类的域描述,int parentVersion=100;见颜色

²  0x49: 域类型. 49 代表"I", 也就是Int;

²  00 0D: 域名字长度;

²  70 6172 65 6E 74 56 65 72 73 69 6F 6E: parentVersion,域名字描述;

²  0x78: TC_ENDBLOCKDATA,对象块结束的标志;

²  0x70: TC_NULL, 说明没有其他超类的标志;

到此为止,算法已经对所有的类的描述都做了输出。下一步就是把实例对象的实际值输出了。这时候是从parent Class的域开始的,见颜色

²  00 00 00 0A: 10, parentVersion域的值.

²  还有SerialTest类的域:

²  00 00 00 42: 66, version域的值.

再往后的bytes比较有意思,算法需要描述contain类的信息,要记住,
现在还没有对contain类进行过描述,见颜色

²  0x73: TC_OBJECT, 声明这是一个新的对象;

²  0x72: TC_CLASSDESC声明这里开始一个新Class;

²  0007: 类名的长度;

²  636F 6E 74 61 69 6E:contain,类名描述;

²  FCBB E6 0E FB CB 60 C7: SerialVersionUID, 序列化ID;

²  0x02: Various flags. 标记号. 该值声明该对象支持序列化;

²  0001: 类内的域个数;

输出contain的唯一的域描述,int containVersion=11:

²  0x49: 域类型. 49 代表"I", 也就是Int;

²  000E: 域名字长度;

²  636F 6E 74 61 69 6E 56 65 72 73 69 6F 6E: containVersion, 域名字描述;

²  0x78: TC_ENDBLOCKDATA对象块结束的标志;

这时,序列化算法会检查contain是否有超类,如果有的话会接着输出;

²  0x70:TC_NULL,没有超类了;

最后,将contain类实际域值输出:

²  00 00 00 0B: 11, containVersion的值。

6、Java的序列化算法

序列化算法一般会按步骤做如下事情:

◆ 将对象实例相关的类元数据输出。

◆ 递归地输出类的超类描述直到不再有超类。

◆ 类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。

◆ 从上至下递归输出实例的数据

7、序列化实例介绍

7.1 定制数据格式的序列化

验证怎样用writeObjectreadObject方法编码一个定制数据格式。当有大量持久性数据时,数据应该以简洁、精简的格式存放。此例子用一个矩形对称阵列,只对其一半数据序列化,即只写/读一半数据再恢复成完整阵列。

package com.asc.alibaba.base;

import java.io.*;

public class CustomDataExample implements Serializable 

    transient int dimension;
    transient int thearray[][];

    CustomDataExample(int dim)
        dimension = dim;
        thearray = new int[dim][dim];
        arrayInit();
    

    public static void main(String args[]) 
        CustomDataExample corg = new CustomDataExample(4);
        CustomDataExample cnew = null;
        try 
            FileOutputStream fo = new FileOutputStream("cde.tmp");
            ObjectOutputStream so = new ObjectOutputStream(fo);
            so.writeObject(corg);
            so.flush();
            so.close();
         catch (Exception e) 
            e.printStackTrace();
            System.exit(1);
        
        try 
            FileInputStream fi = new FileInputStream("cde.tmp");
            ObjectInputStream si = new ObjectInputStream(fi);
            cnew = (CustomDataExample) si.readObject();
            si.close();
         catch (Exception e) 
            e.printStackTrace();
            System.exit(1);
        
        System.out.println();
        System.out.println("Printing the original array...");
        System.out.println(corg);
        System.out.println();
        System.out.println("Printing the new array...");
        System.out.println();
        System.out.println(cnew);
        System.out.println();
        System.out.println("The original and new arrays should be the same!");
        System.out.println();
    

    private void writeObject(ObjectOutputStream s) throws IOException 
        /* 先调用缺省的defaultWriteObject()方法保存非临时变元(若有) */
        s.defaultWriteObject();
        /* 对临时变元明确调用原型数据writeInt()方法以保存其 */
        s.writeInt(dimension);
        for (int i = 0; i < dimension; i++) 
            for (int j = 0; j <= i; j++) 
                /* 对临时变元明确调用原型数据writeInt()方法以保存其阵列的一半数据 */
                s.writeInt(thearray[i][j]);
            
        

    

    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException 
        /* 先调用缺省的defaultReadObject()方法恢复非临时数据,再显式调用原型数据readInt()方法恢复临时数据,写入和恢复数据的顺序要求一致 */
        s.defaultReadObject();
        dimension = s.readInt();
        thearray = new int[dimension][dimension];
        for (int i = 0; i < dimension; i++) 
            for (int j = 0; j <= i; j++) 
                thearray[i][j] = s.readInt();
            
        
        for (int i = 0; i < dimension; i++) 
            for (int j = dimension - 1; j > i; j--) 
                thearray[i][j] = thearray[j][i];
            
        
    

    /* 此方法的功能是将阵列作成对角阵 */
    void arrayInit() 
        int x = 0;
        for (int i = 0; i < dimension; i++) 
            for (int j = 0; j <= i; j++) 
                thearray[i][j] = x;
                thearray[j][i] = x;
                x++;
            
        
    

    public String toString() 

        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < dimension; i++) 
            for (int j = 0; j < dimension; j++) 

                sb.append(Integer.toString(thearray[i][j]) + " ");
            
            sb.append("\\n");
        
        return (sb.toString());
    

结果如下:
  Printing the original array...
0 1 3 6
1 2 4 7
3 4 5 8
6 7 8 9
Printing the new array...
0 1 3 6
1 2 4 7
3 4 5 8
6 7 8 9
The original and new arrays should be the same!

7.2 非序列化超类的序列化

当一个已序列化的子类的超类没有序列化时,子类必须显式存储超类的状态。

/* NonSerialSuperExample */
package com.asc.alibaba.base;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class NonSerialSuperExample 

    public static void main(String args[]) 
        Book bookorg = new Book(100, "How to Serialize", true, "R.R", "Serialization", 97);
        Book booknew = null;
        try 
            FileOutputStream fo = new FileOutputStream("tmp");
            ObjectOutputStream so = new ObjectOutputStream(fo);
            so.writeObject(bookorg);
            so.flush();
         catch (Exception e) 
            System.out.println(e);
            System.exit(1);
        
        try 
            FileInputStream fi = new FileInputStream("tmp");
            ObjectInputStream si = new ObjectInputStream(fi);
            booknew = (Book) si.readObject();
         catch (Exception e) 
            System.out.println(e);
            System.exit(1);
        
        System.out.println();
        System.out.println("Printing original book..");
        System.out.println(bookorg);
        System.out.println("Printing new book..");
        System.out.println(booknew);
        System.out.println("Both original and new should be the same");
        System.out.println();
    

/* Book.java */
package com.asc.alibaba.base;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

class Book extends ReadingMaterial implements Serializable 

    int     numpages;
    String  name;
    boolean ishardcover;

    public Book()
        super();
    

    public Book(int pages, String n, boolean hardcover, String author, String subject, int yearwritten)
        super(author, subject, yearwritten);
        numpages = pages;
        name = n;
        ishardcover = hardcover;
    

    private void writeObject(ObjectOutputStream out) throws IOException 
        out.defaultWriteObject();
        out.writeObject(author);
        out.writeObject(subject);
        out.writeInt(yearwritten);
    

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException 

        in.defaultReadObject();

        author = (String) in.readObject();
        subject = (String) in.readObject();
        yearwritten = in.readInt();
    

    public String toString() 
        return ("Name:" + name + "\\n" + "Author:" + author + "\\n" + "Pages:" + numpages + "\\n" + "Subject:" + subject
                + "\\n" + "Year:" + yearwritten + "\\n");
    
/* ReadingMaterial.java */
 package com.asc.alibaba.base;

class ReadingMaterial 

    protected String author;
    protected String subject;
    protected int    yearwritten;

    public ReadingMaterial()
    

    public ReadingMaterial(String auth, String sub, int year)
        author = auth;
        subject = sub;
        yearwritten = year;
    

结果如下: 
Printing original book..
Name:How to Serialize
Author:R.R
Pages:100
Subject:Serialization
Year:97
Printing new book..
Name:How to Serialize
Author:R.R
Pages:100
Subject:Serialization
Year:97
Both original and new should be the same

7.3 超类具体化的具体化

当用具体化接口时,一个具体化对象必须运行writeExternal()方法存储对象的状态,用readExternal方法读取对象的状态。此例子验证了一个怎样存储和恢复它可具体化超类对象的状态。当一个可具体化对象的超类也具体化,子类要在它自己的writeExternal()readExternal()方法中调用其超类的writeExternal()readExternal()方法。

/*SaveSuper*/
package com.asc.alibaba.base;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SaveSuper 

    public static void main(String args[]) 
        Book1 bookorg = new Book1(100, "How to Serialize", true, "R.R", "Serialization", 97);
        Book1 booknew = null;
        try 
            FileOutputStream fo = new FileOutputStream("tmp");
            ObjectOutputStream so = new ObjectOutputStream(fo);
            so.writeObject(bookorg);
            so.flush();
         catch (Exception e) 
            System.out.println(e);
            System.exit(1);
        
        try 
            FileInputStream fi = new FileInputStream("tmp");
            ObjectInputStream si = new ObjectInputStream(fi);
            booknew = (Book1) si.readObject();
         catch (Exception e) 
            System.out.println(e);
            System.exit(1);
        
        System.out.println();
        System.out.println("Printing original book..");
        System.out.println(bookorg);
        System.out.println("Printing new book..");
        System.out.println(booknew);
        System.out.println("Both original and new should be the same");
        System.out.println();
    

/*Book1*/
package com.asc.alibaba.base;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

class Book1 extends ReadingMaterial1 implements Externalizable 

    private int     numpages;
    private String  name;
    private boolean ishardcover;

    public Book1()
        super();
    

    public Book1(int pages, String n, boolean hardcover, String author, String subject, int yearwritten)
        super(author, subject, yearwritten);
        numpages = pages;
        name = n;
        ishardcover = hardcover;
    

    public void witeExternal(ObjectOutput out) throws IOException 
        super.writeExternal(out);
    

    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException 
        super.readExternal(in);
        numpages = in.readInt();
        name = (String) in.readObject();
        ishardcover = in.readBoolean();
    

    public String toString() 
        return ("Name:" + name + "\\n" + "Author:" + super.getAuthor() + "\\n" + "Pages:" + numpages + "\\n" + "Subject:"
                + super.getSubject() + "\\n" + "Year:" + super.getYearwritten() + "\\n");
    


/*ReadingMaterial1*/
package com.asc.alibaba.base;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

class ReadingMaterial1 implements Externalizable 

    private String author;
    private String subject;
    private int    yearwritten;

    public ReadingMaterial1()
    

    public ReadingMaterial1(String auth, String sub, int year)
        author = auth;
        subject = sub;
        yearwritten = year;
    

    public String getAuthor() 
        return author;
    

    public String getSubject() 
        return subject;
    

    public int getYearwritten() 
        return yearwritten;
    

    public void writeExternal(ObjectOutput out) throws IOException 
        out.writeObject(author);
        out.writeObject(subject);
        out.writeInt(yearwritten);
    

    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException 
        author = (String) in.readObject();
        subject = (String) in.readObject();
        yearwritten = in.readInt();
    

结果如下:
Printing original book..
Name:How to Serialize
Author:R.R
Pages:100
Subject:Serialization
Year:97

Printing new book..
Name:null
Author:R.R
Pages:0
Subject:Serialization
Year:97
Both original and new should be the same

7.4 超类非具体化的具体化

当用具体化接口时,一个具体化对象必须运行writeExternal()方法存储对象的状态,用readExternal()方法读取对象的状态。此例子验证了一个对象怎样存储和恢复它非具体化超类的状态。当一个可具体化对象的超类没有具体化,子类必须用它自己的writeExternal()readExternal()方法明确存储和恢复其超类的可具体化对象状态。

/*Nonexternsuper1*/
package com.asc.alibaba.base;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class Nonexternsuper1 

    public static void main(String args[]) 
        Book2 bookorg = new Book2(100, "How to Serialize", true, "R.R", "Serialization", 97);
        Book2 booknew = null;
        try 
            FileOutputStream fo = new FileOutputStream("tmp");
            ObjectOutputStream so = new ObjectOutputStream(fo);
            so.writeObject(bookorg);
            so.flush();
         catch (Exception e) 
            System.out.println(e);
            System.exit(1);
        
        try 
            FileInputStream fi = new FileInputStream("tmp");
            ObjectInputStream si = new ObjectInputStream(fi);
            booknew = (Book2) si.readObject();
         catch (Exception e) 
            System.out.println(e);
            System.exit(1);
        
        System.out.println();
        System.out.println("Printing original book..");
        System.out.println(bookorg);
        System.out.println("Printing new book..");
        System.out.println(booknew);
        System.out.println("Both original and new should be the same");
        System.out.println();
    

/*Book2*/
package com.asc.alibaba.base;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

class Book2 extends ReadingMaterial2 implements Externalizable 

    int     numpages;
    String  name;
    boolean ishardcover;

    public Book2()
        super();
    

    Book2(int pages, String n, boolean hardcover, String author, String subject, int yearwritten)
        super(author, subject, yearwritten);
        numpages = pages;
        name = n;
        ishardcover = hardcover;
    

    public void writeExternal(ObjectOutput out) throws IOException 
        out.writeObject(author);
        out.writeObject(subject);
        out.writeInt(yearwritten);
        out.writeInt(numpages);
        out.writeObject(name);
        out.writeBoolean(ishardcover);
    

    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException 
        author = (String) in.readObject();
        subject = (String) in.readObject();
        yearwritten = in.readInt();
        numpages = in.readInt();
        name = (String) in.readObject();
        ishardcover = in.readBoolean();
    

    public String toString() 
        return ("Name:" + name + "\\n" + "Author:" + author + "\\n" + "Pages:" + numpages + "\\n" + "Subject:" + subject
                + "\\n" + "Year:" + yearwritten + "\\n");
    

/*ReadingMaterial2*/
package com.asc.alibaba.base;

public class ReadingMaterial2 

    String author;
    String subject;
    int    yearwritten;

    public ReadingMaterial2()
    

    ReadingMaterial2(String auth, String sub, int year)
        author = auth;
        subject = sub;
        yearwritten = year;
    

结果:
Printing original book..
Name:How to Serialize
Author:R.R
Pages:100
Subject:Serialization
Year:97

Printing new book..
Name:How to Serialize
Author:R.R
Pages:100
Subject:Serialization
Year:97

Both original and new should be the same

7.5 演进类的序列化

/* EvolutionExampleOriginalClass3*/
package com.asc.alibaba.base;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class EvolutionExampleOriginalClass3 

    public static void main(String args[]) 
        boolean serialize = false;
        boolean deserialize = false;
        if (args.length == 1) 
            if (args[0].equals("-d")) 
                deserialize = true;
             else if (args[0].equals("-s")) 
                serialize = true;
             else 
                usage();
                System.exit(0);
            
         else 
            usage();
            System.exit(0);
        
        AClass serializeclass = new AClass(10, "serializedByOriginalClass");
        AClass deserializeclass = null;
        if (serialize) 
            try 
                FileOutputStream fo = new FileOutputStream("evolve.tmp");
                ObjectOutputStream so = new ObjectOutputStream(fo);
                so.writeObject(serializeclass);
                so.flush();
             catch (Exception e) 
                System.out.println(e);
                System.exit(1);
            
        
        if (deserialize) 
            try 
                FileInputStream fi = new FileInputStream("evolve.tmp");
                ObjectInputStream si = new ObjectInputStream(fi);
                deserializeclass = (AClass) si.readObject();
             catch (Exception e) 
                System.out.println(e);
                System.exit(1);
            
            System.out.println("Now printing deserialzed object's name:");
            System.out.println();
            System.out.println(deserializeclass);
            System.out.println();
        
    

    static void usage() 
        System.out.println("Usage:");
        System.out.println("    -s(in order to serialize)");
        System.out.println("    -d(in order to deserialize)");
    


class AClass implements Serializable 

    private int num;
    String      name;

    AClass(int n, String s)
        num = n;
        name = s;
    

    private void writeObject(ObjectOutputStream s) throws IOException 
        s.defaultWriteObject();
    

    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException 
        s.defaultReadObject();
    

    public String toString() 
        return ("Name:" + name + "\\n" + "Num:" + num + "\\n");
    

结果:
Now printing deserialzed object's name:

Name:serializedByOriginalClass
Num:10

8、Java序列化的高级认识

8.1 序列化 ID 问题

情境:两个客户端 AB试图通过网络传递对象数据,A端将对象C序列化为二进制数据再传给BB反序列化得到C

问题C对象的全类路径假设为 com.alibaba.Test,在AB端都有这么一个类文件,功能代码完全一致。也都实现了Serializable接口,但是反序列化时总是提示不成功。

解决虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化ID是否一致(就是 privatestatic final long serialVersionUID = 1L。清单 1中,虽然两个类的功能代码完全一致,但是序列化ID不同,他们无法相互序列化和反序列化。

package com.asc.alibaba.base;

import java.io.Serializable;

public class AA implements Serializable 

    private static final long serialVersionUID = 1L;
    private String            name;

    public String getName() 
        return name;
    

    public void setName(String name) 
        this.name = name;
    


package com.asc.alibaba.base;

import java.io.Serializable;

public class AA implements Serializable 

    private static final long serialVersionUID = 2L;
    private String            name;

    public String getName() 
        return name;
    

    public void setName(String name) 
        this.name = name;
    

序列化IDEclipse下提供了两种生成策略,一个是固定的1L,一个是随机生成一个不重复的long类型数据(实际上是使用JDK工具生成),在这里有一个建议,如果没有特殊需求,就是用默认的1L就可以,这样可以确保代码一致时反序列化成功。

8.2静态变量的序列化

情境:两个客户端A B试图通过网络传递对象数据,A端将对象C序列化为二进制数据再传给B,对C中的静态变量进行修改,B在反序列化出C对象后,重新访问该静态变量,将会发现该变量已经被修改。代码清单如下:

package com.asc.alibaba.base;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class StaticTest implements Serializable 

    private static final long serialVersionUID = 1L;
    public static int         staticVar        = 5;

    public static void main(String[] args) 
        try 
            // 初始时staticVar为5
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));
            out.writeObject(new StaticTest());
            out.close();
            // 序列化后修改为10
            StaticTest.staticVar = 10;
            ObjectInputStream oin = new ObjectInputStream(new FileInputStream("result.obj"));
            StaticTest t = (StaticTest) oin.readObject();
            oin.close();
            // 再读取,通过t.staticVar打印新的值
            System.out.println(t.staticVar);
         catch (FileNotFoundException e) 
            e.printStackTrace();
         catch (IOException e) 
            e.printStackTrace();
         catch (ClassNotFoundException e) 
            e.printStackTrace();
        
    

将对象序列化后,修改静态变量的数值,再将序列化对象读取出来,然后通过读取出来的对象获得静态变量的数值并打印出来。依照清单2,这个System.out.println(t.staticVar)语句输出的是 10 还是 5 呢?

最后的输出是 10,对于无法理解的读者认为,打印的 staticVar是从读取的对象里获得的,应该是保存时的状态才对。之所以打印10的原因在于序列化时,并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量

8.3 父类的序列化与 Transient 关键字

情境:一个子类实现了Serializable接口,它的父类都没有实现Serializable接口,序列化该子类对象,然后反序列化后输出父类定义的某变量的数值,该变量数值与序列化时的数值不同。

解决要想将父类对象也序列化,就需要让父类也实现Serializable接口。如果父类没有实现的话,就需要有默认的无参的构造函数。在父类没有实现Serializable接口时,虚拟机是不会序列化父对象的,而一个Java对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。如果你考虑到这种序列化的情况,在父类无参构造函数中对变量进行初始化,否则的话,父类变量值都是默认声明的值,如int型的默认是0string型的默认是 null

Transient关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient变量的值被设为初始值,如int型的是0,对象型的是 null

8.4 对敏感字段加密

情境:服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。

解决:在序列化过程中,虚拟机会试图调用对象类里的writeObjectreadObject方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是ObjectOutputStreamdefaultWriteObject方法以及ObjectInputStreamdefaultReadObject方法。用户自定义的writeObjectreadObject方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。基于这个原理,可以在实际应用中得到使用,用于敏感字段的加密工作。

package com.asc.alibaba.base;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.io.ObjectInputStream.GetField;
import java.io.ObjectOutputStream.PutField;

public class EncryptTest implements Serializable 

    private static final long serialVersionUID = 1L;
    private String            password         = "password1";

    public String getPassword() 
        return password;
    

    public void setPassword(String password) 
        this.password = password;
    

    private void writeObject(ObjectOutputStream out) 
        try 
            PutField putFields = out.putFields();
            System.out.println("原密码:" + password);
            password = "password2";// 模拟加密
            putFields.put("password", password);
            System.out.println("加密后的密码" + password);
            out.writeFields();
         catch (IOException e) 
            e.printStackTrace();
        
    

    private void readObject(ObjectInputStream in) 
        try 
            GetField readFields = in.readFields();
            Object object = readFields.get("password", "");
            System.out.println("解密的字符串:" + object.toString());
            password = "password1";// 模拟解密,需要获得本地的密钥
         catch (IOException e) 
            e.printStackTrace();
         catch (ClassNotFoundException e) 
            e.printStackTrace();
        
    

    public static void main(String[] args) 
        try 
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));
            out.writeObject(new EncryptTest());
            out.close();

            ObjectInputStream oin = new ObjectInputStream(new FileInputStream("result.obj"));
            EncryptTest t = (EncryptTest) oin.readObject();
            System.out.println("解密后的字符串:" + t.getPassword());
            oin.close();
         catch (FileNotFoundException e) 
            e.printStackTrace();
         catch (IOException e) 
            e.printStackTrace();
         catch (ClassNotFoundException e) 
            e.printStackTrace();
        
    

RMI 技术是完全基于 Java序列化技术的,服务器端接口调用所需要的参数对象来自于客户端,它们通过网络相互传输。这就涉及 RMI的安全传输的问题。一些敏感的字段,如用户名密码(用户登录时需要对密码进行传输),我们希望对其进行加密,这时,就可以采用本节介绍的方法在客户端对密码进行加密,服务器端进行解密,确保数据传输的安全性。

8.5 序列化的存储规则

情境:当将一个对象连续两次进行序列化时,它并不能够使文件翻倍,而且在反序列化出来的两个对象完全一致。

解答Java序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的 5字节的存储空间就是新增引用和一些控制信息的空间。反序列化时,恢复引用关系,使得清单中的 t1 t2 指向唯一的对象,二者相等,输出 true。该存储规则极大的节省了存储空间。

package com.asc.alibaba.base;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class StoreTest implements Serializable 

    private static final long serialVersionUID = 1L;

    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException 
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));
        StoreTest test = new StoreTest();
        // 试图将对象两次写入文件
        out.writeObject(test);
        out.flush();
        System.out.println(new File("result.obj").length());
        out.writeObject(test);
        out.close();
        System.out.println(new File("result.obj").length());
        ObjectInputStream oin = new ObjectInputStream(new FileInputStream("result.obj"));
        // 从文件依次读出两个文件
        StoreTest t1 = (StoreTest) oin.readObject();
        StoreTest t2 = (StoreTest) oin.readObject();
        oin.close();
        // 判断两个引用是否指向同一个对象
        System.out.println(t1 == t2);
    

以上是关于JAVA序列化机制的深入研究的主要内容,如果未能解决你的问题,请参考以下文章

Java 反射机制深入研究

深入对比Java与Hadoop大数据序列化机制Avro

Java深入研究3JVM内存管理机制

Java深入研究6fail-fast机制

Java异常的深入研究与分析

深入理解JAVA I/O系列五:对象序列化