文章目录
  1. 1. 概念
  2. 2. 如何序列化一个对象
    1. 2.1. 序列化头信息
    2. 2.2. 类的描述部分
    3. 2.3. 属性域的值部分
  3. 3. 更多
    1. 3.1. 序列化ID的问题
    2. 3.2. 静态变量序列化
    3. 3.3. 父类的序列化与 Transient 关键字
    4. 3.4. 序列化存储规则
  4. 4. 其他序列化协议

Java序列化是经常使用到的技术,大部分情况下,我只是简单的去实现Serializable接口就算了,今天就简单的了解一下序列化的一些原理。

概念

序列化: 把Java对象转换为字节序列的过程;

反序列化: 把字节序列恢复为对象的过程。

使用场景:

  1. 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
  2. 在网络上传送对象的字节序列(远程通信)。

如何序列化一个对象

一个对象如果想实现序列化只需实现java.io.Serializable接口,该接口没有方法,只是一种标记。java.io.ObjectOutputStream类中的writeObject0方法如下:

private void writeObject0(Object obj, boolean unshared)
    throws IOException
{
    // remaining cases
    if (obj instanceof String) {
        writeString((String) obj, unshared);
    } else if (cl.isArray()) {
        writeArray(obj, desc, unshared);
    } else if (obj instanceof Enum) {
        writeEnum((Enum) obj, desc, unshared);
    } else if (obj instanceof Serializable) {
        writeOrdinaryObject(obj, desc, unshared);
    } else {
        if (extendedDebugInfo) {
            throw new NotSerializableException(
                cl.getName() + "\n" + debugInfoStack.toString());
        } else {
            throw new NotSerializableException(cl.getName());
        }
    }
} 

由源码可知,如果没有实现java.io.Serializable接口,就会抛出NotSerializableException异常(String、Enum等类也是实现了该接口)。

下面我们举个具体的小例子。我们定义两个类:Animal和Cat类,代码如下:

public class Animal implements Serializable {
    private int type;

    public Animal() {
        this.type = 1;
    }
}

Cat类:

public class Cat extends Animal {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("out.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        Cat cat = new Cat();
        cat.setName("Test");
        oos.writeObject(cat);
        oos.flush();
        oos.close();

    }
}

运行之后,打开out.txt,内容如下:

aced 0005 7372 0012 636f 6d2e 7474 706f
642e 736f 6e67 2e43 6174 4d6e 97a6 ab6e
c12e 0200 014c 0004 6e61 6d65 7400 124c
6a61 7661 2f6c 616e 672f 5374 7269 6e67
3b78 7200 1563 6f6d 2e74 7470 6f64 2e73
6f6e 672e 416e 696d 616c a991 5474 2068
e443 0200 0149 0004 7479 7065 7870 0000
0001 70

Java序列化序列化对象的信息包括:类元数据描述、类的属性、父类信息以及属性域的值。Java将这些信息分成3部分:序列化头信息、类的描述部分以及属性域的值部分。现在我们对out.txt加以分析。

序列化头信息

  1. 0xAC 0xED :STREAM_MAGIC 流的幻数,用于标记序列化协议
  2. 0x00 0x05 :STREAM_VERSION 标记序列化协议的版本

类的描述部分

  1. 0x73 : TC_OBJECT. 声明这是一个新的对象
  2. 0x72 : TC_CLASSDESC 声明这是一个新的类描述
  3. 0x00 0x12 :类名字的长度,换算成十进制就是18
  4. 0x63 0x6f 0x6d 0x2e 0x74 0x74 0x70 0x6f 0x64 0x2e 0x73 0x6f 0x6e 0x67 0x2e 0x43 0x61 0x74 : 代表类的名称com.ttpod.song.Cat
  5. 0x4d 0x6e 0x97 0xa6 0xab 0x6e 0xc1 0x2e: 序列化ID的类型为long型因此占用8个字节
  6. 0x02 :标记号,该字节的8位分表代表不同的含义,

    SC_EXTERNALIZABLE 0x04 : 该类实现了java.io.Externalizable接口 
    SC_BLOCK_DATA 0x08 : Externalizable接口的writeExternal方法写入的数据
    SC_SERIALIZABLE 0x02 : 该类实现了java.io.Serializable接口
    SC_WRITE_METHOD 0x01 : 该序列化类实现了writeObject方法 
    SC_ENUM 0x10 : 该类是枚举(enum)类型 
    

    该标记号通过上述信息进行或运算(|)而获得。

  7. 0x00 0x01 : 代表类属性域的个数
  8. 0x4C : 域类型,0x4C代表L即该域类型为java对象类型
  9. 0x00 0x04 : 域名称长度
  10. 0x6E 0x61 0x6D 0x65 :域名称name
  11. 0x74 : TC_STRING 一个新字符串
  12. 0x00 0x12 :域类型的长度
  13. 0x4C 0x6A 0x61 0x76 0x61 0x2F 0x6C 0x61 0x6E 0x67 0x2F 0x53 0x74 0x72 0x69 0x6E 0x67 0x3B :对象类型签名Ljava.lang.String
  14. 0x78 : TC_ENDBLOCKDATA 对象数据块结束标志
    下面是父类描述:
  15. 0x72 : TC_CLASSDESC 声明这是一个新的类描述
  16. 0x00 0x15 : 类名长度,换算成十进制就是21
  17. 0x63 0x6f 0x6d 0x2e 0x74 0x74 0x70 0x6f 0x64 0x2e 0x73 0x6f 0x6e 0x67 0x2e 0x41 0x6e 0x69 0x6d 0x61 0x6c : 代表类的名称com.ttpod.song.Aniaml
  18. 0xa9 0x91 0x54 0x74 0x20 0x68 0xe4 0x43 : 序列化ID
  19. 0x02 : 标记号
  20. 0x00 0x01 : 域个数
  21. 0x49 : 域类型,0x49代表I即int类型
  22. 0x00 0x04 : 域名称长度
  23. 0x74 0x79 0x70 0x65 : 域名称type
  24. 0x78 : TC_ENDBLOCKDATA 对象数据块结束标志
  25. 0x70 : TC_NULL 再没有父类的标志

属性域的值部分

  1. 0x00 0x00 0x00 0x01 : 父类域type的值为1
  2. 0x74 :TC_STRING 一个新字符串
  3. 0x00 0x04 :域值的长度,十进制为4
  4. 0x54 0x65 0x73 0x74 : name值为Test

由上可知,Java序列化的算法的基本步骤如下:

  1. 输出序列化的头部信息,包括标识序列化协议的幻数以及协议的版本;
  2. 按照由子类到父类的顺序,递归的输出类的描述信息,直到不再有父类为止;类描述信息按照类元数据,类属性信息的顺序写入序列化流中;
  3. 按照由父类到子类的顺序,递归的输出对象域的实际数据值;而对象的属性信息是按照基本数据类型到java对象类型的顺序写入序列化流中;其中java对象类型的属性会从步骤a)重新开始递归的输出,直到不再存在java对象类型的属性。

更多

序列化ID的问题

序列化ID提供了两种生成策略,一个是固定的 1L,一个是随机生成一个不重复的 long 类型数据(实际上是使用JDK工具生成)。虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致。所以这里强烈建议在一个可序列化类中显示的定义serialVersionUID,为它赋予明确的值。

静态变量序列化

序列化并不保存静态变量。序列化保存的是对象的状态,静态变量属于类的状态。

父类的序列化与 Transient 关键字

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

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

序列化存储规则

Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用。

其他序列化协议

由上面分析我们发现Java序列化储存了很多我们并不关系的内容,所以占用空间比较大。下面我们再介绍几种其他协议,不过目前暂时还没有实践过,等实践之后再详细介绍,目前只贴几个链接。

  1. protocol buffers
  2. hessian
文章目录
  1. 1. 概念
  2. 2. 如何序列化一个对象
    1. 2.1. 序列化头信息
    2. 2.2. 类的描述部分
    3. 2.3. 属性域的值部分
  3. 3. 更多
    1. 3.1. 序列化ID的问题
    2. 3.2. 静态变量序列化
    3. 3.3. 父类的序列化与 Transient 关键字
    4. 3.4. 序列化存储规则
  4. 4. 其他序列化协议