引子
我们都知道 ArrayList
是通过 数组 来存储元素的。阅读 ArrayList
的源码,会发现:
css
transient Object[] elementData;
也就是说,用来存放元素的数组 elementData
被 transient
修饰了。按道理,transient
的意思是"这个字段不要被序列化",那么问题就来了------ArrayList
里的元素难道不需要被序列化吗?那它是怎么把数据存下来的?
带着这个疑问,我们继续往下看。
源码揭秘
首先确认一下,elementData
确实是 transient
的:
css
transient Object[] elementData;
那它的数据是怎么保存和恢复的呢?答案就是: ArrayList
自己实现了一套序列化和反序列化方法 ,也就是 writeObject
和 readObject
。
自定义序列化
arduino
@java.io.Serial
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
int expectedModCount = modCount;
// 先写入非 transient 字段,比如 size
s.defaultWriteObject();
// 写 size(元素数量),注意不是数组的容量
s.writeInt(size);
// 写入真正存储的元素
for (int i = 0; i < size; i++) {
s.writeObject(elementData[i]);
}
// 并发修改检测
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
自定义反序列化
arduino
@java.io.Serial
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// 先恢复非 transient 字段
s.defaultReadObject();
// capacity,这里直接忽略掉
s.readInt();
if (size > 0) {
// 按 size 来分配数组,而不是之前的容量
Object[] elements = new Object[size];
// 把序列化时保存的元素一个个读回来
for (int i = 0; i < size; i++) {
elements[i] = s.readObject();
}
elementData = elements;
} else if (size == 0) {
elementData = EMPTY_ELEMENTDATA;
} else {
throw new java.io.InvalidObjectException("Invalid size: " + size);
}
}
实现解析
从代码可以看出,ArrayList
在序列化时只关心 真实存放的元素,而不是整个数组容量。
为什么这么做?举个例子:
ini
ArrayList<String> list = new ArrayList<>(1000);
list.add("A");
这个时候,elementData
的数组长度可能是 1000,但实际上只存了一个元素 "A"
。 如果不加 transient
,直接把整个数组序列化,那 999 个 null
也会被写到文件里,结果就是 空间浪费 + 性能下降。
所以,JDK 作者就用了一个取巧的办法:
elementData
标记为transient
,让它不参与默认序列化。- 在
writeObject
里只把size
个真实元素写出去。 - 在
readObject
时再用size
来重建数组,把元素填回去。
这样既节省空间,也让 ArrayList
的序列化行为更合理。
总结
-
ArrayList
的底层存储数组elementData
被transient
修饰,不会直接参与序列化。 -
ArrayList
自己实现了writeObject
/readObject
,只序列化实际存储的元素,而不是整个数组容量。 -
这样做的好处是:
- 避免序列化过程中保存大量无效的
null
。 - 提升序列化和反序列化效率,节省存储空间。
- 让
ArrayList
在不同 JDK 版本的扩容策略下,依然保持序列化兼容性。
- 避免序列化过程中保存大量无效的
一句话总结:ArrayList 的序列化是"只保存有用的元素,不浪费空间" 。