揭开Java序列化的神秘面纱(下)Serializable源码剖析


在上一篇文章中我们明白了 Serializable 的大致用法。感兴趣的朋友,请前往查阅。揭开Java序列化的神秘面纱(上)Serializable使用详解


本篇文章重点关注 Serializable 序列化的实现 ,一切从源头说起,Java序列化的设计和实现都源于Serializable这个看似简单的接口。作为Java序列化机制的基石,它的由来和精髓值得我们仔细探讨。


一、Serializable的起源和设计理念


`Serializable`接口是Java语言中用于支持对象序列化和反序列化的标准机制的一部分。序列化是将对象的状态信息转换为可以存储或传输的形式的过程,而反序列化则是这一过程的逆过程。

1、起源

序列化的概念并非Java所独有,它在计算机科学中有着悠久的历史,主要用于数据持久化和网络通信。Java在1995年由Sun Microsystems公司推出,而Serializable接口作为Java语言的一部分,也在这个时期被引入。

Serializable接口最初出现在JDK 1.1版本,其设计目的是为了支持对对象序列化的描述。

Java的序列化机制设计时考虑了以下几个方面:

  1. 跨平台性:Java语言的一个核心理念是“一次编写,到处运行”(Write Once, Run Anywhere,WORA)。序列化机制允许Java对象在不同的平台和Java虚拟机(JVM)之间进行传输,从而保持了跨平台性。

  2. 简单性:Java的序列化机制设计得非常简单直观。开发者只需通过实现Serializable接口,即可让一个类的对象支持序列化,无需编写额外的代码。

  3. 透明性:Java序列化是透明的,这意味着开发者不需要关心对象状态是如何被转换为字节序列的。Java运行时环境会自动处理这些细节。

  4. 兼容性:序列化机制需要能够处理类定义随时间变化的情况。Java通过serialVersionUID字段来确保序列化版本的兼容性。

  5. 安全性:Java序列化机制提供了一定的安全性,例如,通过transient关键字可以控制哪些字段被序列化,从而保护敏感信息。


2、设计理念


设计者们意识到在分布式环境和网络传输等场景下,将内存中的对象按照某种可复原的表示方式存储或传输是一种普遍的需求。于是Serializable接口应运而生。

作为标记接口,Serializable自身没有任何方法声明,所有的序列化细节都由JVM底层实现。

这一设计理念非常巧妙,它使得对象序列化的支持对于应用层来说是完全透明的,无需关心字节流的生成和解析细节。

对于对象的使用者来说,只需让目标类实现Serializable接口,JVM底层的序列化机制就会自动生效,从而达到对象层面的跨平台支持和数据迁移。

Serializable的设计理念考虑了以下方面:

  1. 对象状态的保存:序列化允许开发者保存对象的状态,这在需要持久化对象或在应用程序之间共享对象时非常有用。
  2. 网络传输:在分布式系统中,对象序列化可以用于通过网络传输对象,使得对象可以在不同的系统间移动。
  3. 简化开发:通过简化序列化和反序列化的过程,Java允许开发者更容易地构建需要这些功能的应用程序。
  4. 性能考虑:虽然Java序列化机制简单易用,但也考虑到了性能问题。例如,通过transient关键字可以排除不需要序列化的字段,减少序列化的数据量。
  5. 可扩展性:Java序列化机制允许开发者通过实现Externalizable接口或自定义writeObjectreadObject方法来扩展序列化过程,以满足特定的需求。
  6. 安全性:Java序列化在设计时也考虑了安全性,提供了机制来防止序列化过程中的某些攻击,如通过transient关键字忽略敏感字段。
  7. 版本控制:通过serialVersionUID,Java序列化机制支持类的版本控制,使得即使在类定义发生变化后,旧的序列化对象仍然可以被正确地反序列化。

总的来说,Java的Serializable接口和序列化机制的设计反映了Java语言的核心价值观:跨平台性、简单性、透明性和安全性。这些设计理念使得序列化成为了Java开发者工具箱中一个强大而灵活的工具。


二、深入剖析Serializable的源码


尽管Serializable只是一个空接口,但它所承载的序列化细节却相当丰富。我们从JDK的源码入手,探究这个简洁的接口背后的机制。

public interface Serializable {
}

在这里插入图片描述


看似简单,实则精深。实现Serializable接口的类,将自动获得对象序列化和反序列化的能力,底层序列化机制会对该类的实例进行编码,转换为一串字节序列。这一串字节序列可以持久化存储或通过网络传输。

  • Serializable 和 Externalizable 序列化接口。Serializable 接口没有方法或字段,仅用于标识可序列化的语义,实际上 ObjectOutputStream#writeObject 时通过反射调用 writeObject 方法,如果没有自定义则调用默认的序列化方法。Externalizable 接口该接口中定义了两个扩展的抽象方法:writeExternal 与 readExternal。
  • DataOutput 和 ObjectOutput DataOutput 提供了对 Java 基本类型 byte、short、int、long、float、double、char、boolean 八种基本类型,以及 String 的操作。ObjectOutput 则在 DataOutput 的基础上提供了对 Object 类型的操作,writeObject 最终还是调用 DataOutput 对基本类型的操作方法。
  • ObjectOutputStream 我们一般使用 ObjectOutputStream#writeObject 方法把一个对象进行持久化。ObjectInputStream#readObject 则从持久化存储中把对象读取出来。
  • ObjectStreamClass 和 ObjectStreamField ObjectStreamClass 是类的序列化描述符,包含类描述信息,字段的描述信息和 serialVersionUID。可以使用 lookup 方法找到/创建在此 Java VM 中加载的具体类的 ObjectStreamClass。而 ObjectStreamField 则保存字段的序列化描述符,包括字段名、字段值等。

ObjectOutputStream 源码分析

ObjectOutputStream是对象序列化的核心类,它的主要作用是将Java对象转换为字节序列并输出到底层的I/O流中。


1、主要功能
  • 对象写入ObjectOutputStream允许将对象写入输出流,包括对象的状态和对象之间的引用关系。
  • 引用处理:它能够处理对象之间的引用,确保每个对象只被写入一次,并且可以被正确地引用。
  • 序列化ObjectOutputStream负责调用对象的writeObject方法,或者如果对象实现了Externalizable接口,则调用writeExternal方法。
  • 异常处理:它能够处理序列化过程中可能发生的异常,如NotSerializableExceptionInvalidClassException
  • 协议版本控制ObjectOutputStream使用流协议版本号来确保序列化数据的兼容性。

2、数据结构
private final BlockDataOutputStream bout;   // io流
private final HandleTable handles;          // 序列化对象句柄(编号)映射关系
private final ReplaceTable subs;            // 替换对象的映射关系

private final boolean enableOverride;       // true 则调用writeObjectOverride()来替代writeObject()
private boolean enableReplace;              // true 则调用replaceObject()

  • bout 是下层输出流,两个表是用于记录已输出对象的缓存。

  • handles ,HandleTable 从名称就知道这是一个轻量的 HashMap,保存序列化对象句柄(编号)映射关系。handles 的作用:Java 序列化除了保存字段信息外,还保存有类信息,当同一个对象序列化两次时第二次只用保存第一次的编号,这样可以大大减少序列化的大小。

  • subs,ReplaceTable 保存的是替换对象的映射关系。


3、 构造函数
public ObjectOutputStream(OutputStream out) throws IOException {
    bout = new BlockDataOutputStream(out);
    handles = new HandleTable(10(float) 3.00);
    subs = new ReplaceTable(10(float) 3.00);
    enableOverride = false;
    writeStreamHeader();
    bout.setBlockDataMode(true);
}
  • ObjectOutputStream 构建时会创建 BlockDataOutputStream 序列化流 bout,handles 和 subs 。

  • writeStreamHeader 方法是输出序列化流的头信息,用于文件校验,和 .class 文件头的魔数及版本作用一样。如果不需要,可以覆盖这个方法,什么也不做,Hadoop 默认的 Java 序列化就是这样。

protected void writeStreamHeader() throws IOException {
    bout.writeShort(STREAM_MAGIC);
    bout.writeShort(STREAM_VERSION);
}

4、核心方法
  • writeObject(Object obj)
public final void writeObject(Object obj) throws IOException{
    if (enableOverride) {
        writeObjectOverride(obj);
        return;
    }
    try {
        writeObject0(obj, false);
    } catch (IOException ex) {
        if (depths != null) {
            writeFatalException(ex);
        }
        throw ex;
    }
}

writeObject方法是序列化对象的主入口。它首先会检查是否需要通过writeObjectOverride方法进行自定义的序列化处理。如果没有自定义,则调用底层的writeObject0方法进行实际的序列化操作。


  • writeObject0(Object obj, boolean unshared)
private void writeObject0(Object obj, boolean unshared) throws IOException {
    ...
    
    // 检查obj是否实现了Serializable接口
    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 {
        throw new NotSerializableException(cl.getName());
    }
}

writeObject0方法是真正执行序列化操作的地方。它首先会检查要序列化的对象的类型,如果是String、数组或Enum,会调用相应的序列化处理方法;如果是其他实现了Serializable接口的普通对象,则会调用writeOrdinaryObject方法进行处理。如果对象根本没有实现Serializable,则会抛出NotSerializableException异常。


  • writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared)
private void writeOrdinaryObject(Object obj,
                                 ObjectStreamClass desc,
                                 boolean unshared)
    throws IOException
{
    // 检查是否需要调用对象自身的writeReplace方法进行替换
    if (obj instanceof ObjectStreamClass.ClassDataSlime) {
        throw new IOException("Object graph corrupted");
    }
    
    if (desc.isProxy()) {
        // 处理代理类
    } else {
        // 调用底层的writeSerialData方法进行数据写入
        writeSerialData(obj, desc, unshared);
    }
}

对于普通的实现了Serializable接口的对象,writeOrdinaryObject方法会先检查对象是否需要通过writeReplace方法进行替换,如果需要则调用相应的替换逻辑。如果不需要替换,则调用writeSerialData方法来写入对象的实际序列化数据。


  • writeSerialData(Object obj,ObjectStreamClass desc, boolean unshared)
private void writeSerialData(Object obj,
                             ObjectStreamClass desc,
                             boolean unshared)
    throws IOException
{
    // 检查是否需要调用writeObject方法进行自定义序列化
    if (desc.isExternalizable() && !desc.isProxy()) {
        writeExternal(obj);
    } else {
        // 进行普通对象的序列化处理
        writeRegularObject(obj, desc, unshared);
    }
}

writeSerialData方法会检查要序列化的对象是否实现了Externalizable接口。如果实现了,就会调用对应的writeExternal方法进行自定义序列化。否则,会调用writeRegularObject方法进行普通对象的序列化处理。

通过上面几个核心方法的分析,我们可以看出ObjectOutputStream在序列化Java对象时,主要做了以下几件事:

  • 检查要序列化的对象是否实现了Serializable或Externalizable接口,如果没有实现任何一个,则抛出NotSerializableException异常。

  • 检查对象是否实现了writeReplace等钩子方法,如果实现了则优先进行替换逻辑。

  • 检查对象是否实现了Externalizable接口,如果实现了则调用自定义的writeExternal方法进行序列化。

  • 对于普通的实现了Serializable接口的对象,会调用底层的writeRegularObject等方法进行真实的序列化操作,将对象的字段数据转换为字节流并写入底层的输出流中。

总的来说,ObjectOutputStream的实现充分体现了Java对象序列化机制的基本流程和原理。它会根据对象的具体类型和实现的序列化接口,采取不同的序列化策略,并最终将对象转换为能够跨平台和网络传输的字节序列流。这种动态分发的实现使得Java序列化机制的功能相对完整,并且还预留了自定义序列化的扩展点。


三、Serializable接口的局限性


尽管Serializable接口的设计理念优雅,为序列化编程提供了便利,但它也暴露出了一些局限性:

  1. 全盘序列化
    实现Serializable后,对象中的所有非transient字段都会被序列化,无法进行按需序列化。这对于包含大量不需序列化数据的对象来说是一种浪费。
  2. 版本控制问题
    当类定义发生变化时,JVM只能基于serialVersionUID进行过时的版本控制。面对类定义的较大变动,仍有可能导致反序列化异常。
  3. 扩展性差
    Serializable接口的功能是编码在底层JVM中,缺乏扩展能力。对于一些更加复杂的序列化需求(例如跨语言),就显得力有未逮。
  4. 安全性隐患
    由于序列化机制的透明性,一些重要的敏感字段也有可能被序列化,存在潜在的安全隐患。

四、Externalizable的更好方案


面对Serializable接口的局限性,Java还提供了另一个更加强大的序列化接口Externalizable。通过显式地实现writeExternal和readExternal方法,开发者可以自主决定序列化的字节表示形式,甚至将自定义序列化操作独立为第三方库。

public class Person implements Externalizable {
    
    public void writeExternal(ObjectOutput out) throws IOException {
        // 自定义序列化逻辑
    }

    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        // 自定义反序列化逻辑 
    }

    ...
}

Externalizable提供了更大的灵活性和扩展性,是实现对象序列化和网络传输的更佳选择。当然,它也需要开发者编写更多的代码。在一定程度上,它平衡了简单性和可控性之间的取舍。


五、Externalizable 接口 与 Serializable接口对比


Externalizable接口相比Serializable接口有以下一些主要优势和不足:

优势:

  1. 可控性强
    Externalizable接口需要显式实现writeExternal和readExternal方法,开发者可以完全自主控制序列化和反序列化的过程和字节表示形式。这使得开发者能够根据实际需求对序列化行为进行定制,比如只序列化某些字段、加密敏感数据等。
  2. 更小的序列化体积
    由于可以自主选择序列化哪些字段,使用Externalizable通常可以获得比Serializable更小的序列化后字节流大小,这在对序列化体积要求较高的场景(如网络传输)中很有优势。
  3. 版本控制更优雅
    Externalizable机制本身并不直接提供版本控制,但开发者可以在自定义的序列化/反序列化逻辑中实现自己的版本控制策略,相比Serializable版本控制的局限性更加灵活。
  4. 支持跨语言
    理论上,Externalizable的序列化数据可以使用任意字节编码格式,因此也就支持了跨语言的对象序列化,而Serializable是java语言内部使用的编码格式。

不足:

  1. 编码工作量大
    使用Externalizable需要为每个类手动实现writeExternal和readExternal方法,编码工作量较大。而Serializable是自动完成序列化的,只需标记接口即可。
  2. 无法自动绑定引用
    对于对象图的序列化,Externalizable无法像Serializable那样自动绑定对象间的引用关系,需要自行维护这些关系。
  3. 无法利用默认序列化
    Externalizable完全由开发者自己实现序列化逻辑,无法调用JVM提供的默认序列化行为,对一些简单场景来说有些复杂了。
  4. IDE支持较差
    主流IDE对Externalizable的支持较差,没有为其提供代码生成等便利性,而对于Serializable则通常都有良好的支持。

总的来说,Externalizable提供了比Serializable更大的灵活性和可控性,但代价是牺牲了一定的易用性和自动化程度。在对象序列化需求复杂或有特殊要求的场合,使用Externalizable会是更好的选择;而如果只是简单的序列化需求,使用Serializable就已经足够了。两者有自己适用的领域,需要根据实际情况进行选型。


总之,Serializable虽然简单,却蕴含了Java序列化机制的精华。透过这个曾经平凡的接口,我们可以更好地理解序列化在Java编程中的重要地位,同时也体会到它作为底层机制的不足之处。或许在未来,Java序列化机制会有进一步的革新,但Serializable这一点点设计独特之处,将持续影响着Java大家庭。


  • 26
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

w风雨无阻w

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值