ArrayList的elementData被声明为transient,是因为其因扩容常存在大量未使用空间(如size=3但length=16),若默认序列化会写入冗余null元素,浪费空间和性能;因此ArrayList重写writeObject和readObject,仅序列化size个有效元素并重建最小必要数组。

ArrayList 的扩容机制本身不直接参与序列化与反序列化过程,但它的设计与自定义序列化逻辑紧密配合——关键在于:扩容产生的“冗余容量”不能被序列化,否则浪费空间;而实际存储的元素必须完整、准确地保存和还原。
为什么 elementData 被声明为 transient
ArrayList 底层用 transient Object[] elementData 存储数据,是因为数组常因扩容而远大于实际元素个数(比如 size=3,elementData.length=16)。若按默认序列化规则,整个数组(含大量 null)都会被写入字节流,既低效又冗余。
因此 ArrayList 显式实现了:
- private void writeObject(ObjectOutputStream s):只序列化有效元素(size 个),跳过空位
- private void readObject(ObjectInputStream s):先读出 size,再创建刚好容纳 size 元素的数组,逐个还原
扩容机制如何影响序列化行为
扩容本身发生在 add、addAll 等运行时操作中,不影响序列化逻辑;但它的存在是自定义序列化的根本动因:
立即学习“Java免费学习笔记(深入)”;
- 无参构造的 ArrayList 首次 add 会扩容到 DEFAULT_CAPACITY=10 → 此时 elementData.length=10,但 size=1 → 序列化仅写 1 个元素
- 多次扩容后 elementData 可能达 100,而 size 仍为 5 → 序列化输出体积几乎不变
- 反序列化时完全忽略原数组长度,按读出的 size 重建最小必要数组,不继承扩容历史
自定义序列化方法的关键步骤
以 JDK 源码逻辑为准,核心流程如下:
- writeObject 中:先调用
s.defaultWriteObject()序列化非 transient 字段(如 size) - 再循环遍历
elementData[0] 到 elementData[size-1],逐个调用s.writeObject() - readObject 中:先
s.defaultReadObject()恢复 size - 然后 new 一个 Object[size] 数组,再循环
s.readObject()填充前 size 个位置 - 最后赋值给 elementData,并确保数组类型为 Object[](兼容旧版 toArray 返回非 Object[] 的情况)
对开发者的影响与注意事项
你无需重写 writeObject/readObject,除非继承 ArrayList 并新增需序列化的字段。但需注意:
- 自定义类放入 ArrayList 时,该类自身必须实现 Serializable,否则反序列化会抛 NotSerializableException
- 序列化后的字节流不包含任何扩容信息,反序列化得到的是“紧凑数组”,其 length == size
- 若依赖 elementData.length 做性能判断(如预估容量),反序列化后该值会变小,可能触发新扩容
- serialVersionUID 必须保持一致,否则跨版本反序列化会失败(ArrayList 的 serialVersionUID 是固定常量)


















