序列化你只会Serializable?面试官想和谈谈Externalizable咋办呢。

149 阅读5分钟

从最简单开始:聊聊 Java 的序列化

在 Java 里,对象序列化是个挺常见的需求。啥叫序列化?简单说,就是把一个对象变成字节流,方便存到文件里或者通过网络传出去,然后还能再反过来还原回去。Java 提供了两种方式来干这事儿:实现 Serializable 接口,或者实现 Externalizable 接口。这俩听起来差不多,但用起来差别可不小。

先说最朴素的那个:Serializable。这个接口简单得不能再简单了,你啥都不用干,只要让类实现它,JVM 就自动帮你把对象序列化。字段、值,能存的它全给你存了,连个招呼都不打。这种“全自动”听起来很省心,对吧?比如我有个类:

class Person implements Serializable {
    String name = "小明";
    int age = 25;
}

我啥都不用管,扔到 ObjectOutputStream 里一写,再从 ObjectInputStream 读出来,一个完整的 Person 对象就回来了。简单粗暴,适合懒人。

但问题来了,这种“全自动”也有坑。你有没有想过,万一我有些字段不想存呢?比如 Person 里加个 password

String password = "123456";

这玩意儿要是也被序列化,传出去不就泄密了?JVM 可不管你这些,它默认全存。好在这时候可以用 transient 关键字,把不想序列化的字段标一下,比如:

transient String password = "123456";

这样 password 就不会被存下来,读回来时是 null。但这也有个问题:你得一个一个字段去标 transient,万一类里字段多了,或者以后加了新字段忘了标咋办?全自动的策略到这儿就露出马脚了——它不够灵活,控制力太弱。


再复杂一点:Externalizable 登场

这时候 Externalizable 就该出场了。这个接口比 Serializable 高级点,它不搞全自动,而是把主动权交给你。你得实现两个方法:writeExternalreadExternal,自己指定存啥、咋存。比如:

class Person implements Externalizable {
    String name = "小明";
    int age = 25;
    String password = "123456";

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeInt(age); // 只存 name 和 age,password 我不存
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.name = in.readUTF();
        this.age = in.readInt();
    }
}

看到没?这儿我明确说了只存 nameagepassword 直接被忽略了,哪怕它没标 transient。这就比 Serializable 牛多了——你能精确控制序列化的内容,不用依赖 JVM 的默认行为。

但这也有代价。你得自己写存和读的逻辑,字段多起来代码量就上去了。而且存的时候和读的时候顺序得一模一样,不然就乱套了。比如我 writeExternal 里先存 name 再存 age,那 readExternal 也得先读 name 再读 age,要是反过来,25 就跑到 name 里去了,妥妥的 bug。


朴素策略的问题暴露

从这儿就能看出,SerializableExternalizable 的朴素用法都有自己的麻烦。Serializable 太“傻白甜”,啥都存,控制不住,容易把敏感数据漏出去,或者存一堆没用的东西,浪费空间。举个例子,假设 Person 里有 10 个字段,我只想存 3 个,那得标 7 个 transient,多麻烦啊。而且它存的时候还会带上类的元信息(比如类名、字段描述),字节流体积一下就膨胀了。

Externalizable 呢,虽然控制力强,但太“手工”了。字段一多,写代码跟搬砖似的,稍不留神顺序错了就完蛋。更别提维护了——以后加个字段,还得改两边代码,累不累啊?

这些问题其实都指向一个核心:朴素的序列化策略不够聪明。它要么太懒(Serializable),要么太累(Externalizable),都没找到效率和灵活性的平衡点。


优化方向:向现代方案靠拢

那咋优化呢?咱们得想想,现代主流的序列化方案,比如 JSON(用 Jackson 库)或者 Protocol Buffers(Protobuf),为啥那么受欢迎?它们的核心思路是啥?其实无非是:灵活性、可维护性、性能 三者兼顾。基于这个,咱们可以从朴素的 demo 推导出几个方向:

  1. 元数据驱动,别全靠手写
    Externalizable 的问题在于全手写,太死板。能不能加个“描述层”呢?比如用注解标记哪些字段要存、哪些不要存,运行时自动解析。像 Jackson 的 @JsonProperty 那样,我加个注解:

    @SerializeField
    String name = "小明";
    

    然后有个工具自动根据注解生成序列化逻辑。这样既省了手写麻烦,又能灵活控制,比 transient 高级多了。

  2. 压缩元信息,瘦身字节流
    Serializable 的字节流为啥大?因为它存了一堆类描述信息。现代方案像 Protobuf,直接用预定义的 schema,字段用数字编号(比如 1 表示 name2 表示 age),存的时候只存编号和值,体积小得飞起。咱们可以借鉴这招,搞个轻量化的元数据格式,扔掉不必要的类描述。

  3. 版本兼容,别怕改字段
    加字段改顺序是 Externalizable 的噩梦,但现代方案都考虑了兼容性。比如 Protobuf,字段是可选的,读的时候没这字段就跳过去。咱们可以设计个机制,让序列化时带上版本号,读的时候根据版本动态调整字段映射,这样维护起来不怕翻车。

  4. 性能优先,减少反射
    Serializable 内部用反射实现,性能其实不咋地。Externalizable 手写逻辑倒是快,但太费劲。优化方向可以是生成代码——编译时根据类结构生成序列化方法,像 Kryo 库那样,既快又省心。


总结一下

从最朴素的 SerializableExternalizable 开始,咱们发现全自动太笨、手动太累的问题。顺着这些坑往前推,能看到现代序列化方案的影子umlah:注解驱动、轻量元数据、版本兼容、性能优化。这些方向不仅能解决朴素策略的短板,还跟 JSON、Protobuf 这些主流方案不谋而合。