🍧 Peach

蜜桃学代码

IO - 序列化/反序列化


序列化:将程序中的结构体、对象等结构化内存对象转换为扁平的二进制流;
反序列化:将二进制流还原为结构化内存对象。

%%{
  init: {
    'themeVariables': {
       'fontSize': '14px'
     }
  }
}%%
graph LR
subgraph "应用层1"
A("结构体、对象
(字节数组)"):::lb end subgraph "应用层2" B("结构体、对象
(字节数组)"):::lb end subgraph 网络协议 C("二进制流"):::p A -.-> |"[转换]"|C -.-> |"[还原]"|B end style 网络协议 fill:#f4e4e9, stroke-dasharray: 3 3, stroke-width: 2px classDef p fill:#ddaebd classDef b fill:#aab7d2 classDef g fill:#9ac5bb classDef lp fill:#f4e4e9 classDef lb fill:#d9dfeb classDef lg fill:#ddebe6 classDef info fill:#f6f6f7,color:#737379,stroke-dasharray: 3 3, stroke-width: 2px

一、实现方式

(1)Serializable + 默认

自定义类实现Serializable不包含 readObject()writeObject()。使用默认的序列化和反序列化的方式进行数据写入和读取操作。

* Demo:

public class Person implements Serializable {

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

public Person(String name, int age) {
this.name = name;
this.age = age;
}

}

(2)Serializable + 自定义

自定义类实现Serializable包含 readObject()writeObject(),使用自定义的序列化和反序列化的方式进行数据写入和读取操作。

* Demo

public class Person implements Serializable {

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

public Person(String name, int age) {
this.name = name;
this.age = age;
}

// 🔻 readObject() 和 writeObject() 的可见性没有限制:private、protected、default、public
private void writeObject(ObjectOutputStream outputStream) throws Exception {
// 省略实现
}

private void readObject(ObjectIntputStream inputStream) throws Exception {
// 省略实现
}

}

(3)Externalizable

自定义类实现 Externalizable重写 readExternal()writeExternal() 实现序列化/反序列化功能。

public class Person implements Externalizable {

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

// 🔻 无参构造函数:必不可少的,否则会抛异常
public Person() {}

// 🔻 可见性没有限制:private、protected、default、public
private void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// 省略实现
}

private void writeExternal(ObjectOutput out) throws IOException {
// 省略实现
}

}



二、核心类

graph LR
t([序列化核心类]):::g
t --> A("⑴ Serializable"):::lg
t --> B("⑵ Externalizable"):::lg
t --> |"[ 序列化
关键类 ]"|C("⑶ ObjectOutputStream
将对象转换为二进制流"):::p t --> |"[ 反序列化
关键类 ]"|D("⑷ ObjectInputStream
将二进制流还原为对象"):::p C -.- c([内部
关键类]):::info c -.-> c1(["① BolckDataOutputStream - 缓冲"]):::lp c -.-> c2(["② HandleTable - 防止重复"]):::lp c -.-> c3(["③ ReplaceTable - 自定义序列化"]):::lp c -.-> c4(["④ ObjecyStreamClass - 记录元数据、反射"]):::lp classDef p fill:#ddaebd classDef b fill:#aab7d2 classDef g fill:#9ac5bb classDef lp fill:#f4e4e9 classDef lb fill:#d9dfeb classDef lg fill:#ddebe6 classDef info fill:#f6f6f7,color:#56565b,stroke-dasharray: 3 3, stroke-width: 2px

(1)Serializable

Serializable 是一个空接口,表明了实现自该接口的子类具有序列化行为特征,所有要支持序列化的类都应该实现这个接口。

* 源码:

public interface Serializable {

}

(2)Externalizable

writeExternal的参数:ObjectOutput,表示输出对象的抽象(它继承自DataOutput,能支持基本类型、String、数组、对象的输出。实际应用会使用其实现类 ObjectOutputStream。

readExternal的参数:ObjectInput,表示输入对象的抽象(它继承自DataInput,能支持基本类型、String、数组、对象的输入。实际应用会使用其实现类 ObjectInputStream。

* 源码

public interface Externalizable extends Serializable {

// 🔻 自定义的类必须覆盖 writeExternal 和 readExternal 方法。
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

// 🔻 自定义的类必须包含无参构造函数。
}

(3)ObjectOutputStream

BlockDataOutputStream

ObjectOutputStream 内置的具有缓冲作用的输出功能类,包含阻塞和非阻塞两种工作模式。

%%{
  init: {
    'themeVariables': {
       'fontSize': '14px'
     }
  }
}%%
graph LR
T("BlockDataOutputStream - 缓冲"):::p
T --> A("阻塞模式"):::lp
T --> B("非阻塞模式"):::lp
A -.- t("每次将缓冲区数据写入前会写入一个
阻塞标记头部(Block Data Header)"):::info classDef p fill:#ddaebd classDef b fill:#aab7d2 classDef g fill:#9ac5bb classDef lp fill:#f4e4e9 classDef lb fill:#d9dfeb classDef lg fill:#ddebe6 classDef info fill:#f6f6f7,color:#56565b,stroke-dasharray: 3 3, stroke-width: 2px

* 两种模式的工作流程相同,都是先把待写的数据写到缓冲区,直到缓冲区满后再执行真正的写入操作。


HandleTable

当前待序列化的对象(A),可能包含多个其他对象类型的成员变量,而这些成员变量可能引用同一个对象T:

%%{
  init: {
    'themeVariables': {
       'fontSize': '14px'
     }
  }
}%%
classDiagram
    class A
    class B
    class C
    class T对象

    A: b B
    A: c C
    B: t T
    C: t T

    A *-- B
    A *-- C
    C *-- T对象
    B *-- T对象

为了使得在序列化A对象时,不重复序列化T对象,引入了对象和句柄的映射关系。

句柄从0开始按递增顺序自增。每当要写入一个Object类型对象时,去 HandleTable 查询其是否已经存在:

❶ 如果存在,则说明已经写入过,本次只写入句柄即可;
❷ 如果不存在,则说明没有被写入过,需要写入该对象。


ReplaceTable

为了支持用户自定义的序列化行为(Java序列化支持对序列化和反序列化进行拦截并执行自定义的序列化行为。)

ReplaceTable 用来管理源对象和目标替换对象,在序列化/反序列化过程中,通过源对象查找目标替换对象

❶ 如果存在目标替换对象,则使用目标替换对象;
❷ 否则使用源对象。


ObjectStreamClass

记录了和序列化对象相关的元数据信息,并负责相关方法的反射初始化和执行

字段 字段意义
name 序列化类的完整类名
cl 序列化类的Class类型
suid 序列化类的serialVersionUID
isProxy 是否为代理类
isEnum 是否为枚举类
serializable 是否实现了Serializable
externalizable 是否实现了Externalizable
primDataSize 基本类型的成员字段个数,不包含static和transient类型字段
numObjFileds 非基本类型的成员字段个数
dataLayout 类层次结构:当前类、父类及所有继承类的类描述
writeObjectMethod writeObject方法,通过反射获取
readObjectMethod readObject方法,通过反射获取
writeReplaceMethod writeReplace方法,通过反射获取
readReplaceMethod readReplace方法,通过反射获取

(4)ObjectInputStream

java.io.ObjectInputStream 是实现Java反序列化的关键类,和 ObjectOutputStream 是对应的,内部包含了 BlockDataInputStreamHandleTableReplaceTableObjectStreamClass



三、内部原理

Java序列化的流程、原理,以及各种类型数据进行Java序列化后的格式和占用空间大小等细节。

Java序列化对整型数据采用固定4字节存储,而Protocol Buffer对整型数据采用变长存储,使得存储空间进一步减小。

(1)基本类型的序列化流程

Java序列化对基本类型数据的处理,严格按照其内存占用大小来进行。比如int类型占用4字节,Java序列化按照高位到低位依次放到字节数组,再写入到序列化输出对象:

写入int类型数据:

graph TD

subgraph "ObjectOutputStream.java"
A(["writeInt()"]):::p
end

subgraph "BlockDataOutputStream(内部类)"
A ==> |"调用"|B(["writeInt()"]):::p
B --- b["1024字节大小
的缓冲区"]:::lb b -.-> |"① 缓冲区还可容纳
待写入的int类型数据
"|b1("把当前数据放入缓冲区"):::lg B2(["write()"]):::p B2 --- 2b["缓冲区"]:::lb 2b -.-> |"① 缓冲区能容纳
当前待写入字节
"|2b1("把当前字节写入缓冲区"):::lg 2b -.-> |"② 缓冲区已满
"|2b2("把缓冲区的数据输出,
再把当前待写入字节放到缓冲区"):::lg end b -.-> |"② 缓冲区不能容纳
待写入的int类型数据
"|C subgraph "DataOutputStream.java" C(["writeInt()"]):::p C --> d("按照从高到低的字节顺序
依次写入4个字节"):::lg d ==> |"调用"|B2 end classDef p fill:#ddaebd classDef b fill:#aab7d2 classDef g fill:#9ac5bb classDef lp fill:#f4e4e9 classDef lb fill:#d9dfeb classDef lg fill:#ddebe6 classDef info fill:#f6f6f7,color:#737379,stroke-dasharray: 3 3, stroke-width: 2px
  • ObjectOutputStream 的 writeInt():

    public void writeInt(int val) throws IOException {
    // 🔻 真正的写入是通过调用 BlockDataOutputStream(bout)
    // 的 writeInt 方法实现的
    bout.writeInt(val);
    }
  • BlockDataOutputStream 的 writeInt():

    public void writeInt(int v) throws IOException {

    // 🔻 BlockDataOutputStream内部维护了一个1024字节大小的缓冲区
    if (pos + 4 <= MAX_BLOCK_SIZE) { // MAX_BLOCK_SIZE = 1024
    // 🔹 若缓冲区还可以容纳写入的int类型数据:将数据放入缓冲区
    Bits.putInt(buf, pos, v);
    pos += 4;

    } else {
    // 🔹 若缓冲区无法容纳:调用 DataOutputStream 的 writeInt()
    dout.writeInt(v)
    }

    }
  • DataOutputStream 的 writeInt():

    // 🔻 int类型是按照从高到低的字节顺序依次写入4个字节的
    public final void writeInt(int v) throws IOException {
    // out:BlockDataOutputStream
    out.write((v >>> 24) & 0xFF);
    out.write((v >>> 16) & 0xFF);
    out.write((v >>> 8) & 0xFF);
    out.write((v >>> 0) & oxFF);
    incCount(4);
    }
  • BlockDataOutputStream 的 write() :

    public void write(int b) throws IOEception {
    // 🔹 若缓冲区已满:将缓冲区数据推出
    if(pos >= MAX_BLOCK_SIZE) {
    drain();
    }
    // 🔹 将待写入字节写入缓冲区
    buf[pos++] = (byte) b;
    }

    void drain() throws IOException {
    // ..
    // 🔸 将buf缓冲区已有数据输出
    out.write(buf, 0, pos);
    pos = 0;
    }

    (其他基本类型数据的写入流程与此类似)

(2)基本类型的序列化大小

数据类型 Java内存中的大小 序列化后的大小 序列化操作
(假设变量为V)
boolean 1 整数1或0的
最低一个字节
write(v ? 1 : 0)
byte 1 1 write(v)
char 2 2 高位字节先写入
write((v>>>8) & 0xFF)
write((v>>>0) & 0xFF)
short 2 2 高位字节先写入
write((v>>>8) & 0xFF)
write((v>>>0) & 0xFF)
int 4 4 高位字节先写入
write((v>>>24) & 0xFF)
write((v>>>16) & 0xFF)
write((v>>>8) & 0xFF)
write((v>>>0) & 0xFF)
long 8 8 高位字节先写入
write((v>>>56) & 0xFF)
write((v>>>48) & 0xFF)
write((v>>>40) & 0xFF)
write((v>>>32) & 0xFF)
write((v>>>24) & 0xFF)
write((v>>>16) & 0xFF)
write((v>>>8) & 0xFF)
write((v>>>0) & 0xFF)
float 4 4 符合IEEE754标准的4字节
double 8 8 符合IEEE754标准的8字节

(3)对象类型的序列化流程

Java序列化对非基本类型的数据处理比基本类型的数据处理更复杂,这里说的非基本类型包括Object、Enum、Array等 —— 通过 ObjectOutputStreamwriteObject 方法实现的:

writeObject()

graph TD
%%subgraph "ObjectOutputStream"
A(["writeObject()"]):::p
subgraph "ObjectOutputStream.java"
A --> |"[ 调用 ]"|B(["writeObject0()"]):::p
B --> |"核心实现"|b("if分支:判断obj类型"):::lb
end

subgraph "String / Enum"
b -...- 1t(["writeString()"]):::lp
b -...- 3t(["writeEnum()"]):::lp
1t -.-> 1a("实现自Serializable,
具备支持序列化/反序列化的能力"):::info 3t -.-> 1a end subgraph "Array" b -.- 2t(["writeArray()"]):::lp 2t -.-> |"基本类型"|2a("调用基本类型
的序列化方式"):::lg 2t -.-> |"Object类型"|2b("递归调用
执行if分支"):::lb %% 2a -.-> b end subgraph "自定义的序列化类" b -.....- 4a(["writeOrdinaryObject()"]):::lp 4a -.-> |"Externalizable类型
且不是动态代理类
"|4a1(["writeExternalData()"]):::p 4a -.-> |"Serializable类型"|4a2(["writeSerialData()"]):::p end %%end classDef p fill:#ddaebd classDef b fill:#aab7d2 classDef g fill:#9ac5bb classDef lp fill:#f4e4e9 classDef lb fill:#d9dfeb classDef lg fill:#ddebe6 classDef info fill:#f6f6f7,color:#737379,stroke-dasharray: 3 3, stroke-width: 2px
  • ObjectOutputStreamwriteObject() 内部实现核心部分:

    public final void writeObject(Object obj) throws IOEXception {
    // 🔹 当前类是否是 ObjectOutputStream 的子类
    if(enableOverride) {
    writeObjectOverride(obj);
    return;
    }

    // 🔹 执行对象写入,false表示共享写入
    try {
    writeObject0(obj, false); // 🔻
    } catch (IOEception ex) {}

    }
  • writeObject0() 的参数:

    1. obj:表示自定义的序列化对象或者Array、Enum类型对象。
    2. 第2个参数:表示一个对象被多个对象共同引用时,在序列化的时候是否要共享写入
      %%{
      init: {
        'themeVariables': {
           'fontSize': '13px'
         }
      }
    }%%
    graph LR
    B(["writeObject0() 参数:"]):::p
    B -.- b("① 被引用的对象实例obj, 
    ② 是否共享写入"):::lp subgraph "[ false ]: 共享写入" b1 end subgraph "[ true ]: 不共享写入" b2 end b --- |"默认"|b1("obj只会被序列化一次,
    其他引用只会写入引用对象句柄"):::lg b -.- b2("obj会被序列化多次,序列
    化后的数据大小会增加"):::lg classDef p fill:#ddaebd classDef b fill:#aab7d2 classDef g fill:#9ac5bb classDef lp fill:#f4e4e9 classDef lb fill:#d9dfeb classDef lg fill:#ddebe6 classDef info fill:#f6f6f7,color:#737379,stroke-dasharray: 3 3, stroke-width: 2px
  • writeObject0() 核心实现:

    if (obj instanceof String) {
    // 🔹 String类:实现自Serializable,具备支持序列化/反序列化能力
    writeString((String) obj, unshared);

    } else if (cl.isArray()) {
    // 🔹 Array类:①基本类型:调用基本类型序列化方式 ②Object类型:递归if
    writeArray(obj, desc, unshared);

    } else if (obj instanceof Enum) {
    // 🔹 Enum类:实现自Serializable,具备支持序列化/反序列化能力
    writeEnum((Exum<?>) obj, desc, unshared);

    } else if (obj instanceof Serializable) {
    // 🔻 自定义的类实现了 Serializable:执行writeOrdinaryObject方法
    writeOrdinaryObject(obj, desc, unshared);

    } else {
    if(extendedDebugInfo) {
    throw new NotSerializableException(cl.getName() + "\n" +
    debugInfoStack.toString());
    } else {
    // ⭕️ 要能够被ObjectOutputStream的writeObject方法序列化,对象必须
    // 实现自Serializable,否则会抛NotSerializableException异常
    throw new NotSerializableException(cl.getName());
    }
    }
  • writeOrdinaryObject() 核心实现:

    // 自定义的序列化类:
    writeClassDesc(desc, false); // 写入类描述信息
    handles.assign(unshared ? null : obj);

    // 🔻 若自定义的类是Externalizable类型:调用writeExternalData方法
    if (desc.isExternalizable() && !desc.isProxy()) {
    writeExternalData((Externalizable) obj);

    // 🔻 若自定义的类是Serializable类型:调用writeSerialData方法
    } else {
    writeSerialData(obj, desc);
    }

➋ writeSerialData()

writeOrdinaryObject方法实现中,如果自定义类实现Serializable,则会调用 writSerialData() 实现序列化:

%%{
  init: {
    'themeVariables': {
       'fontSize': '13px'
     }
  }
}%%
graph
subgraph "writeSerialData()"
A(["desc.getClassDataLayout()"]):::lp
A --> |"返回值"|b(["ClassDataSlot类型数组"]):::b
%% 数组元素顺序:从最根部的祖先到当前类。
b -.- 1("(ObjectStreamClass)desc"):::lb
b -.- 2("(boolean)hasData
"):::lb b -.- 3("... ..."):::lb 1 --> |"判断数组元素对应的类
是否包含 writeObject 方法"|t(["desc.hasWriteObjectMethod()"]):::lp end subgraph "ObjectStreamClass.java" t -.-> |"[ 包含 ]
反射调用"|1a(["invokeWriteObject()"]):::p 1a --> 2a(["writeObjectMethod.invoke() ✔︎"]):::lp end t -.-> |"[ 未包含 ]"|1b(["defaultWriteFields()
"]):::p subgraph "defaultWriteFiled()" 1b --> |"① 先写入基本类型的字段"|2b(["bout.write()"]):::lp 2b --> |"② 写入对象类型的字段"|3b(["writeObject0() ✔︎"]):::lp end classDef p fill:#ddaebd classDef b fill:#aab7d2 classDef g fill:#9ac5bb classDef lp fill:#f4e4e9 classDef lb fill:#d9dfeb classDef lg fill:#ddebe6 classDef info fill:#f6f6f7,color:#737379,stroke-dasharray: 3 3, stroke-width: 2px
⑴ 字段
  • 字段说明:

    字段 类型 说明
    getClassDataLayout() ClassDataSlot[] 获取当前类及继承链路上所有直接或间接实现了 Serializable 的祖先类对应的序列化元数据信息

    * 数组元素顺序:从最根部的祖先到当前类。
    desc ObjectStreamClass 记录了序列化和反序列化对象相关的元数据信息,并负责相关方法的反射初始化和执行。
    hasData boolean 表示desc对应的Java对象是否有数据被序列化
  • Demo:getClassDataLayout()

    1. 当序列化 MyPeople 对象时,getClassDataLayout() 返回的数组大小:1

      • ClassDataSlot[0] = MyPeople 类对应的 ObjecStreamClass
      public class People { // ... }
      public class MyPeople extends People implements Serializable { // ... }

    1. 当序列化 MyPeople 对象时,getClassDataLayout() 返回的数组大小:2

      • ClassDataSlot[0] = People 类对应的 ObjectStreamClas
      • ClassDataSlot[1] = MyPeople 类对应的 ObjecStreamClass
      public class People implements Serializable { // ... }
      public class Mypeople extends Prople { // ... }

⑵ 源码
  • writeSerialData()

    private void writeSerialData(Object obj, ObjectStreamCLass desc) throws IOException {

    // 🔴 对于ClassDataSlot数组的每一个元素,
    // 如果该元素对应的类包含writeObject方法,则调用writeObjet方法。
    ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
    // 获取当前待写入Object类描述信息
    for (int i = 0; i < slots.length; i++) {
    ObjectStreamClass slotDesc = slots[i].desc;
    // 🔹 ① 若有 writeObject 方法:反射调用
    if (slotDesc.hasWriteObjectMethod()) {
    // ...
    try {
    // ...
    // 🔻 反射调用obj里的 writeObject 方法
    slotDesc.invokeWriteObject(obj, this);
    } finally {
    // ...
    }
    // 🔹 ② 若无 writeObject 方法:🔻 调用默认方法
    } else {
    defaultWriteFileds(obj, slotDesc);
    }
    }
    }
  • ObjectStreamClass 的 invokeWriteObject() 内部实现

    // 🔴 wirteObject 方法以反射方式被调用:
    writeObjectMethod.invoke(obj, new Object[]{ out });
    // writeObjectMethod 的初始化通过反射方式实现:(ObjectStreamClass的构造方法)
    writeObjectMethod = getPrivateMethod(cl, "writeObject",
    new Class<?>[] { ObjectOutputStream.class },
    Void.TYPE);
  • defaultWriteFileds()

    // 🔴 若当前待序列化的类没有writeObject方法,则调用defaultWriteFields方法实现序列化:

    // 🔸 先写入基本类型字段
    int primDataSize = desc.getPrimDataSize();
    if (primVals == null || primVals.lengnth < primDataSize) {
    primVals = new byte[primDataSize];
    }
    desc.getPrimFiledValues(obj, primVals);
    bout.write(primVals, 0, primDataSize, false); // BlockDataOutputStream

    // 🔸 写入对象类型的字段
    ObjectStreamFiled[] fileds = desc.getFileds(false);
    Object[] objVals = new Object[desc.getNumObjFields()];
    int numPrimFileds = fields.length - objVals.length;
    desc.getObjectFiledValues(obj, objVals);
    for (intn i = 0; i < objVals.length; i++) {
    try {
    // ⭕️ 对象类型字段继续调用writeObject0方法
    writeObject0(objVals[i], fileds[numPrimFileds + i].isUnshared());
    } finally { }
    }

    🦋 从ObjectOutputStream 的 writeObject 执行流程来看,Java序列化以深度优先遍历方式使整个对象实现序列化。)

➌ writeExternal()

回到 writeOrdinaryObject 方法实现,如果自定义类实现了 Externalizable 且不是动态代理类,则会调用 writeExternalData 方法实现序列化:

  • writeExternalData

    // 🔻 最终调用自定义类对象的 writeExternal 方法实现写入
    if (protocol == PROTOCOL_VERSION_1) {
    obj.writeExternal(this);

    } else {
    bout.setBlockDataMode(true);
    obj.writeExternal(this);
    bout.setBlockDataMode(false);
    bout.writeByte(TC_ENDBLOCKDATA);
    }
  • writeExternalData 方法的实现看起来比 writeSerializable 类更简洁,原因:

      %%{
      init: {
        'themeVariables': {
           'fontSize': '13px'
         }
      }
    }%%
    graph 
    T(["Externalizable 接口"]):::p
    T -.- A("接口包含:readExternal() 和 writeExternal()"):::info
    A --> |"(有规范)"|A2("Externalizable 的子类必须覆盖 
    readExternal() 和 writeExternal()"):::lp T2(["Serializable 接口"]):::g T2 -.- B("空接口"):::info B --> |"(无规范)"|B2("本身是空接口,在具体行为上没有做
    强制性的约束和规范。(仅是一种特性)"):::lg classDef p fill:#ddaebd classDef b fill:#aab7d2 classDef g fill:#9ac5bb classDef lp fill:#f4e4e9 classDef lb fill:#d9dfeb classDef lg fill:#ddebe6 classDef info fill:#f6f6f7,color:#737379,stroke-dasharray: 3 3, stroke-width: 2px

(4)对象类型的序列化数据成分

序列化过程会写入的数据成分:(ObjectOutputStream 的 writeObject 方法执行流程)
 1. 写入 TC_OBJECT
 2. 写入类描述(writeClassDesc)
 3. 写入基本类型字段
 4. 获取、遍历所有对象类型的字段递归调用 writeObject0() 实现写入

(1)写入 TC_OBJECT,值为0x73,byte类型。


(2)写入类描述(writeClassDesc),类描述依赖待序列化的对象,占用字节:

  • 类描述数据成分:

    类描述取值 Tag Tag Value(byte) Tag之后的数据
    Null TC_NULL 0x70
    Handle
    (已经写入过的Object类型对象)
    TC_REFERENCE 0X71 4字节的对象
    动态代理类 TC_
    PROXYCLASSDESC
    0x7D 1)4字节的所有父接口类个数
    2)循环写入父接口的名字(UTF-8格式)、父接口的类描述
    自定义的 Serializable 或 Externalizable
    实现类
    TC_CLASSDESC 0x72 1)类名字
    2)serialVersionUID
    3)类标签
    4)字段个数
    5)一次循环写入每个字段的类型码、字段名、字段签名(非基本类型)
  • 类标签及意义

    枚举值 整型值 意义
    SC_WRITE_METHOD 0x01 Serializable 实现类包含 writeObject 方法
    SC_SERIALIZABLE 0x02 Serializable 实现类
    SC_EXTERNALIZABLE 0x04 Externalizable 实现类
    SC_BLOCK_DATA 0x08 Externalizable 实现类写入数据使用 Block 模式
    SC_ENUM 0x10 枚举类
    类标签可以是上述取值的组合。例如,自定义类实现自 Serializable 且包含了 writeObject 方法,则类标签的取值为:SC_WRITE_METHOD | SC_SERIALIZABLE。


(3)写入基本类型字段

  • 将基本类型字段按字段名的字典顺序写入一个字节数组,再把字节数组最终写入到输出流。通过 ObjectStreamClass 的构造函数初始化调用了 getSerialFields 方法得到验证:

    private static ObjectStreamField[] getSerialFields(Class<?> cl)
    throws InvalidClassException
    {
    ObjectStreamField[] fields;
    if (Serializable.class.isAssignableFrom(cl) &&
    !Externalizable.class.isAssignableFrom(cl) &&
    !Proxy.isProxyClass(cl) &&
    !cl.isInterface())
    {
    if ((fields = getDeclaredSerialFields(cl)) == null) {
    fields = getDefaultSerialFields(cl);
    }
    // 🔻 ObjectStreamField 排序
    Arrays.sort(fields);
    } else {
    fields = NO_FIELDS;
    }
    return fields;
    }
  • ObjectStreamFiled 的 comparTo() 核心实现:

    // 🔸 获取字段后会对字段排序,排序逻辑:
    public int compareTo(Object obj){
    ObjectStreamField other = (ObjectStreamField) obj;
    boolean isPrim = isPrimitive();
    if(isPrim != other.isPrimitive()) {
    return isPrim ? -1 : 1;
    }
    return name.compareTo(other.name);
    }

    📌 排序规则总结:

    • 两个字段类型相同,按照字段名的字典顺序进行排序;
    • 两个字段类型不同,基本类型排在非基本类型之前。
  • 回到 ObjectOutputStream 的 defaultWriteFields(),对基本类型字段的处理过程为 —— 先获取所有基本类型字段占用的字节数,再将基本类型字段的值依次放入一个字节数组

    // 🔹 获取所有基本类型字段的字节数
    int primDataSize = desc.getPrimDataSize();
    if (primVals == null || primVals.length < primDataSize) {
    primVals = new byte[primDataSize];
    }
    // 🔹 将基本类型字段依次写入字节数组
    desc.getPrimFieldValues(obj, primVals);
    // 🔸 将字节数组输出
    bout.write(primVals, 0, primDataSize, false);

(4)获取所有对象类型的字段,循环遍历每一个字段,递归调用 writeObject0() 实现写入。

(略,上述 writeObject 流程里已涉及)



四、高级特性

(1)transient 关键字

transient 关键字在Java序列化中用来控制字段不被序列化。

ObjectStreamClassgetDefaultFields() 内部实现:

private static ObjectStreamField[] getDefaultFields(Class<?> cl) {
Field[] clFields = cl.getDeclaredFields();
ArrayList<ObjectStreamField> list = new ArrayList<>();
// 🔻 序列化字段不能为 static 且不能为 transient
int mask = Modifier.STATIC | Modifier.TRANSIENT;
for (int i = 0; i < clFields.length; i++) {
if ((clFields[i].getModifiers() & mask) == 0) {
list.add(new ObjectStreamField(clFields[i], false, true));
}
}
int size = list.size();
return (size == 0) ? NO_FIELDS : list.toArray(new ObjectStreamField[size]);
}

(2)static 关键字

static 字段属于类全局共有,不会被序列化。

在反序列化得到的结果里,静态变量的值依赖类对该静态字段的 ①初始化操作以及 ②是否在同一个JVM进程内

%%{
  init: {
    'themeVariables': {
       'fontSize': '13px'
     }
  }
}%%
graph LR
subgraph 静态变量
t("a = 0"):::p
end
t --> |"赋值"|1("a = 10"):::lp
1 -.-> |"反序列化
操作 1
"|A("a = 10"):::lp 1 -.-> |"反序列化
操作 2
"|B("a = 0"):::g subgraph "JVM进程 2" B end subgraph "JVM进程 1(同个)" A end B -.-> info("因为类在JVM进程内
只会被加载一次
………………………
相同的类在不同JVM
都会被初始化一遍。"):::lg classDef p fill:#ddaebd classDef b fill:#aab7d2 classDef g fill:#9ac5bb classDef lp fill:#f4e4e9 classDef lb fill:#d9dfeb classDef lg fill:#ddebe6 classDef info fill:#f6f6f7,color:#737379,stroke-dasharray: 3 3, stroke-width: 2px

(3)serialVersionUID

serialVersionUID 用来实现类版本兼容,在实际开发中能满足类字段变化的需求
  • 必须定义 serialVersionUID 字段:

    • 如果不定义serialVersionUID字段,Java序列化会根据类字段和其他上下文计算一个默认值
    • 当类字段发生变化时,serialVersionUID 值也会跟着变化,就会出现因类字段变化导致反序列化失败的问题。比如增加新字段后,无法再读取增加字段之前的序列化数据。反序列化会报InvalidCastException 异常。
  • serialVersionUID 字段必须是 static + final 类型,才能被序列化。

    ObjectStreamClass 的 getDeclaredSUID():

    private static Long getDeclaredSUID(Class<?> cl) {
    try {
    Field f = cl.getDeclaredField("serialVersionUID");
    // 🔻 serialVersionUID 字段必须是 static + final 类型
    int mask = Modifier.STATIC | Modifier.FINAL;
    if ((f.getModifiers() & mask) == mask) {
    f.setAccessible(true);
    return Long.valueOf(f.getLong(null));
    }
    } catch (Exception ex) { }
    return null;
    }

(4)序列化/反序列化hook

writeReplace() 和 readResolve() 用于序列化写入 / 反序列化时拦截并替换成一个自定义的对象

➊ writeReplace方法

  • 方法的完整定义:

    private/default/protected/public Object writeReplace() throws ObjectStreamException;
    // 由于基于反射执行,因此作用域限定符不受限制。

    若定义了 wirteReplace(),就没必要再定义 ❌ writeObject() 了。内部会先调用writeReplace方法将当前序列化对象替换成自定义目标对象。(readObject 同理)

  • 工作原理:

    ObjectOutputStream 的 writeObject0():

    // check for replacement object
    Object orig = obj;
    Class<?> cl = obj.getClass();
    ObjectStreamClass desc;
    for(;;) {
    // REMIND: skip this check for strings/arrays?
    Class<?> repCL;
    // 🔻 调用 obj 序列化写入
    if (!desc.hasWriteReplaceMethod() ||
    (obj = desc.invokeWriteReplace(obj)) == null ||
    (repCl = obj.getCLass()) == cl) {
    break;
    }
    cl = repCl;
    }

➋ readResolve方法

  • 方法的完整定义:

    private/default/protected/public Object readResolve() throws ObjectStreamException;
    // 由于基于反射执行,因此作用域限定符不受限制。

    若定义了 readResolve 方法,✅ readObject 方法是允许出现的。

  • 工作原理:

      %%{
      init: {
        'themeVariables': {
           'fontSize': '13px'
         }
      }
    }%%
    graph LR
    subgraph "ObjectInputStream.java"
    A(["readObject0()"]):::p
    A --> |"🔸 返回反序列化结果"|x("判断是否存在 readResolve()"):::lg
    x -.-> |"[ 存在 ]"|B
    B(["调用 readResolve()"]):::g
    B -.-> |"🔹 返回自定义的对象"|C(["readObject"]):::p
    
    C
    end
    x --> |"[ 不存在 ]

    🔸 直接返回对象结果"|C classDef p fill:#ddaebd classDef b fill:#aab7d2 classDef g fill:#9ac5bb classDef lp fill:#f4e4e9 classDef lb fill:#d9dfeb classDef lg fill:#ddebe6 classDef info fill:#f6f6f7,color:#737379,stroke-dasharray: 3 3, stroke-width: 2px
    1. 首先调用 readObject0 方法得到反序列化结果
    2. 若 readResolve 方法存在,则会调用该方法返回自定义的对象。
    3. 将自定义的对象作为 ObjectInputStream 的 readObject 的返回值。
  • 使用场景:

    使用 readResolve() 在反序列化的过程中对枚举对象进行恢复性保护

    /**
    * 对于类实现的枚举类型,反序列化出来的枚举对象期望是定义的枚举对象,
    * 这也体现了枚举的意义。
    *
    * 但从代码执行情况看,反序列化出来的的枚举对象是一个新建出来的枚举对象,
    * 虽然值和枚举值定义的一样,但不是同一个对象。
    */
    public class Brand implements Serializable {
    private int val;

    private Brand(int val) {
    this.val = val;
    }

    public static final Brand NIKE = new Brand(0);
    public static final Brand ADDIDAS = new Brand(1);

    public static void main(String[] args) throws Exception {
    ObjectOutputStream outputStream = new ObjectOutputStream( new
    FileOutputStream(new File("out1")));
    outputStream.writeObject(Brand.ADDIDAS);
    outputStream.close();

    ObjectInputStream inputStream = new ObjectInputStream( new
    FileInputSrteam(new File("out1")));
    Brand brand = (Brand) inputStream.readObject();
    System.out.println(brand == Brand.ADDIDAS); // ❌ false
    }
    }
  • 加上 readResolve() 后:

    /** 
    * 使用 readResolve() 在反序列化的过程中对枚举对象进行恢复性保护。
    */
    public class Brand implements Serializable {
    private int val;

    private Brand(int val) {
    this.val = val;
    }

    public static final Brand NIKE = new Brand(0);
    public static final Brand ADDIDAS = new Brand(1);

    // 🔻
    public Object readResolve() throws ObjectStreamException {
    if (val == 0) {
    return NIKE;
    }
    if (val == 1) {
    return ADDIDAS;
    }
    return null
    }

    public static void main(String[] args) throws Exception {

    ObjectOutputStream outputStream = new ObjectOutputStream( new
    FileOutputStream(new File("out1")));
    outputStream.writeObject(Brand.ADDIDAS);
    outputStream.close();

    ObjectInputStream inputStream = new ObjectInputStream( new
    FileInputSrteam(new File("out1")));
    Brand brand = (Brand) inputStream.readObject();
    System.out.println(brand == Brand.ADDIDAS); // ⭕️ true

    }
    }

(5)数据校验

Java序列化机制在反序列化时支持对读取到的数据进行校验(默认不校验)。这是因为Java序列化后的数据是明文形式,有可能被修改。

使用数据校验特性需要让自定义的序列化类实现java.io.ObjectInputValidation接口

public class Person implements Serializable, ObjectInputValidation

通过调用回调函数 validateObject 来实现数据验证。

@Override
public void validateObject() throws InvalidObjectException {
if (age <= 0) {
throw new InvalidObjectException("age invalid")
}
}


五、序列化方式对比

选择 Serializable 还是 Externalizable ?

* 对比:

Serializable (🟢优先选择) Externalizable
功能 java序列化已支持 java序列化已支持
易用性 易用性更好:Serializable 提供了默认的序列化与反序列化行为,用户无需关注序列化的实现细节即可拿来使用。 Externalizable 必须实现实现 readExternalwriteExternal 接口,且要提供默认构造函数。
自定义序列化 也可通过 readObject 和 writeObject 来支持

* 终止序列化特性:

在继承链路上,如果要终止一个子类的 Serializable 或者 Externaizable 特性:

readObject / writeObject 方法或 readExternal / writeExternal 方法接口里抛出 UnsupportedOperation-Exception 异常,表示不支持序列化和反序列化功能。



六、小结

Java序列化方案成熟度高,但性能和压缩效果均一般,只适合JVM系列语言共享数据,不具备完全的跨语言能力。另外,它会带来一些数据安全性和完整性问题。

在实际项目中,往往会选择具有跨语言能力、性能高效、压缩效果显著的方案,例如 Thrift、Protocol Buffer等。




- end -


🔖 笔记来自:《深入理解序列化与反序列化》

📚 扩展阅读:《FastSerializer序列化特性介绍 - 反序列化原理》