hello啊,各位观众姥爷们!!!本baby今天来报道了!哈哈哈哈哈嗝🐶
面试官:ArrayList 怎么序列化的? 为什么用 transient 修饰数组?
ArrayList 的序列化机制与 transient
修饰数组的深层原因
一、ArrayList 的序列化实现
Java 的序列化机制默认会序列化对象的 所有非 transient
字段。但 ArrayList
的内部实现中,存储元素的数组 elementData
被 transient
修饰,这意味着它不会被默认序列化。
为了解决这个问题,ArrayList
自定义了序列化逻辑,仅序列化实际存储的元素,而不是整个数组。以下是关键步骤:
1. 自定义 writeObject
方法
ArrayList
通过重写 writeObject
方法,手动控制序列化过程:
private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException {// 1. 写入默认的非transient字段(如size、modCount)s.defaultWriteObject(); // 2. 写入数组的实际容量(可能大于size)s.writeInt(size); // 3. 仅序列化有效元素(从0到size-1),跳过未使用的数组槽位for (int i=0; i<size; i++) {s.writeObject(elementData[i]);}
}
- 优化点:避免序列化整个
elementData
数组(可能包含未使用的null
值),减少序列化后的数据大小。
2. 自定义 readObject
方法
反序列化时,ArrayList
通过 readObject
方法重建数组:
private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException {// 1. 读取默认的非transient字段elementData = EMPTY_ELEMENTDATA;s.defaultReadObject();// 2. 读取实际元素数量(size)s.readInt();if (size > 0) {// 3. 根据size创建新数组(避免预分配多余空间)elementData = new Object[size];// 4. 逐个读取元素,填充数组for (int i=0; i<size; i++) {elementData[i] = s.readObject();}}
}
- 优化点:反序列化后,
elementData
的容量严格等于size
,避免空间浪费。
二、为什么用 transient
修饰 elementData
数组?
transient
关键字的作用是 阻止字段被默认序列化。ArrayList
使用 transient
修饰 elementData
的原因主要有以下几点:
1. 避免序列化冗余数据
- 问题背景:
ArrayList
的elementData
数组长度通常大于当前元素数量(size
)。例如,初始容量为 10,但仅存储了 5 个元素时,剩余的 5 个槽位为null
。 - 直接序列化的后果:
如果直接序列化整个数组,会写入大量无效的null
值,显著增加序列化后的数据大小(尤其在大容量集合中)。
2. 减少网络传输与存储开销
- 自定义序列化的优势:
通过仅序列化有效元素(0
到size-1
),数据量减少到最小。例如:ArrayList<Integer> list = new ArrayList<>(100); list.add(1); list.add(2);
- 默认序列化:写入长度为 100 的数组(含 98 个
null
)。 - 自定义序列化:仅写入 2 个有效元素。
- 默认序列化:写入长度为 100 的数组(含 98 个
3. 避免反序列化后的空间浪费
- 默认反序列化的问题:
如果直接反序列化整个数组,重建后的elementData
可能包含大量未使用的空间(例如容量 100,但实际仅需 2 个元素)。 - 自定义反序列化的优化:
反序列化时,根据size
创建容量精确等于元素数量的数组,确保内存高效利用。
三、对比默认序列化与自定义序列化
场景 | 默认序列化(无 transient ) | 自定义序列化(transient + writeObject ) |
---|---|---|
序列化数据大小 | 包含未使用槽位的 null ,数据量大 | 仅包含有效元素,数据量小 |
反序列化后数组容量 | 与序列化前一致(可能远大于实际元素数量) | 等于元素数量(size ) |
网络传输效率 | 低(传输冗余数据) | 高(仅传输有效数据) |
内存占用 | 可能浪费内存(未使用的数组槽位) | 严格按需分配内存 |
四、源码示例解析
以 ArrayList
的 writeObject
和 readObject
为例,进一步理解设计思想:
// ArrayList.java
public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable {// 存储元素的数组,被transient修饰transient Object[] elementData;// 自定义序列化逻辑private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException {s.defaultWriteObject(); // 写入非transient字段(如size)s.writeInt(size); // 写入元素数量for (int i=0; i<size; i++) {s.writeObject(elementData[i]); // 仅写入有效元素}}// 自定义反序列化逻辑private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException {elementData = EMPTY_ELEMENTDATA;s.defaultReadObject(); // 读取非transient字段(如size)int capacity = s.readInt(); // 读取元素数量if (capacity > 0) {elementData = new Object[capacity]; // 创建精确容量的数组for (int i=0; i<capacity; i++) {elementData[i] = s.readObject(); // 填充元素}}}
}
五、🌶️
- 为什么用
transient
:
避免序列化elementData
中的冗余数据(未使用的null
槽位),显著优化序列化后的数据大小和传输效率。 - 如何实现序列化:
通过重写writeObject
和readObject
,仅序列化有效元素,并在反序列化时按需重建数组。 - 设计思想:
在保持集合功能的前提下,通过精细控制序列化过程,实现空间与性能的最优平衡。