Externalizable接口必须显式实现public的writeExternal和readExternal方法,并提供public无参构造器;字段读写顺序、类型、数量必须严格一致,否则运行时崩溃;transient关键字无效,敏感字段需手动跳过;不具Serializable的兼容性容错机制,维护成本高。

Externalizable 接口必须重写 writeExternal 和 readExternal
不实现这两个方法,反序列化时会直接抛 java.io.InvalidClassException: no valid constructor —— 即使你写了无参构造器也没用。Externalizable 要求类自己完全接管序列化逻辑,JVM 不再自动处理字段,连默认构造器都必须显式提供(且必须是 public)。
常见错误是只写了 writeExternal,忘了 readExternal;或者把读写顺序写反了,导致类型错位、数据截断甚至 StreamCorruptedException。
- 必须声明 public 无参构造器,哪怕内容为空
-
writeExternal和readExternal都必须是 public,且参数类型严格为ObjectOutput/ObjectInput - 读写字段的顺序、类型、数量必须完全一致,否则运行时崩溃
字段顺序错乱比字段遗漏更危险
Serializable 是按字段名+类型反射写入,而 Externalizable 是纯顺序流协议:先写什么,就必须先读什么。比如你写入 out.writeInt(age) 后跟 out.writeUTF(name),读取时如果调换顺序,in.readUTF() 就会试图从 int 的二进制里解析 UTF 字符串,大概率触发 UTFDataFormatException 或静默错乱。
这问题在本地测试常被掩盖,因为 JVM 版本、平台字节序一致;但一旦跨服务、跨语言(比如对接 Go 的 binary decoder),立刻暴露。
立即学习“Java免费学习笔记(深入)”;
- 建议在
writeExternal开头写一个版本号(如out.writeByte(1)),并在readExternal中校验 - 避免动态跳过字段:不要用 if 判断是否写某个字段,除非读端有完全对称的 if
- 集合类别直接调
writeObject,别手动遍历写 size + 元素——除非你确定对方也这么读
transient 字段在 Externalizable 下完全失效
加了 transient 关键字的字段,在 Serializable 中会被跳过;但在 Externalizable 中它毫无意义——因为你本来就要手动控制所有读写。如果你没在 writeExternal 里写它,它就不会被序列化;写了,它就一定会被序列化,和 transient 无关。
很多人误以为加了 transient 就能“安全地忽略敏感字段”,结果在 Externalizable 里反而漏掉校验逻辑,把密码或 token 写进流里。
- 敏感字段必须显式跳过(即不在 writeExternal 中调用 out.xxx)
- 但注意:如果父类也实现了 Externalizable,子类的 readExternal 必须主动调用
super.readExternal(in),否则父类字段不会恢复 - static 字段永远不参与序列化,无论接口类型
兼容性比性能更值得警惕
Externalizable 常被当成“比 Serializable 更快”的方案,但实际中,除非你做极致压缩(比如把 float[] 改成 byte[] 手动编码),否则性能差异微乎其微。真正代价是维护成本:每次加字段、改类型、删字段,都得同步更新两个方法,还不能出错。
更麻烦的是,一旦序列化格式固化(比如存进数据库或发给下游系统),后续任何改动都会破坏兼容性——没有 serialVersionUID 那种容错机制,也没有字段默认值回退。
- 上线前务必用旧版本序列化数据,用新版本反序列化跑一次回归
- 不要在 writeExternal 里调用可能抛异常的业务方法(如远程调用、IO),否则序列化过程会中断且不可恢复
- 如果只是想跳过几个字段或调整顺序,优先考虑 Serializable +
serialPersistentFields,而不是切 Externalizable
真正需要 Externalizable 的场景其实很窄:比如要加密部分字段、适配遗留二进制协议、或字段结构随版本剧烈变化且无法靠 serialVersionUID 挽救。多数时候,它不是优化,是给自己埋了一个必须亲手擦的火药桶。








