为什么 ArrayList 底层数组要使用 transient
关键字?
在 Java 中,ArrayList
是一个常用的动态数组实现,它内部使用一个数组来存储元素。为了提高性能和节省空间,ArrayList
在序列化时并不会直接序列化整个底层数组,而是通过自定义的序列化方法来实现。这里的关键在于 transient
关键字的使用。本文将深入探讨为什么 ArrayList
的底层数组要使用 transient
关键字,并通过丰富的代码示例和详细的解释,帮助你全面理解其工作原理及实际应用。
前置知识
在深入探讨之前,我们需要了解一些基本概念:
- 序列化:将对象转换为字节流的过程,以便存储或传输。
- 反序列化:将字节流转换回对象的过程。
- Serializable 接口:Java 提供的一个标记接口,用于表示对象可以被序列化。
- transient 关键字:用于标记字段,使其不参与序列化过程。
- ArrayList:Java 集合框架中的一种动态数组实现,继承自
AbstractList
类并实现了List
接口。
transient 关键字的作用
transient
关键字用于标记字段,使其不参与默认的序列化过程。当一个对象被序列化时,默认情况下,所有非 transient
字段都会被序列化。而 transient
字段则会被忽略,不会被序列化。
ArrayList 的底层数组
ArrayList
内部使用一个数组来存储元素。为了提高性能,ArrayList
会预留一些额外的空间,因此数组的大小可能大于实际存储的元素数量。这个内部数组被标记为 transient
,表示该字段不参与默认的序列化过程。
transient Object[] elementData;
为什么底层数组要使用 transient?
使用 transient
关键字标记底层数组的主要原因有以下几点:
- 节省空间:
ArrayList
的内部数组可能会预留一些额外的空间,这些额外的空间在序列化时是不必要的。通过标记为transient
,可以避免序列化这些未使用的空间,从而节省空间。 - 性能优化:序列化整个数组可能会导致性能问题,特别是当数组很大时。通过自定义序列化方法,只序列化实际存储的元素,可以提高序列化和反序列化的性能。
- 灵活性:通过自定义序列化方法,
ArrayList
可以更灵活地控制序列化过程,例如只序列化非null
的元素,从而避免序列化空值。
自定义序列化方法
ArrayList
通过自定义的 writeObject
和 readObject
方法来实现序列化和反序列化。这些方法位于 ArrayList
的内部,用于处理内部数组的序列化。
writeObject 方法
writeObject
方法用于将 ArrayList
的元素写入到输出流中。该方法会遍历内部数组,将非 null
的元素写入输出流。
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
// 写入非静态字段和非 transient 字段
s.defaultWriteObject();
// 写入元素数量
s.writeInt(size);
// 写入所有元素
for (int i = 0; i < size; i++) {
s.writeObject(elementData[i]);
}
}
readObject 方法
readObject
方法用于从输入流中读取 ArrayList
的元素。该方法会读取元素数量,并根据元素数量创建一个新的数组,然后将元素读取到数组中。
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
// 读取非静态字段和非 transient 字段
s.defaultReadObject();
// 读取元素数量
int size = s.readInt();
// 创建新的数组
elementData = new Object[size];
// 读取所有元素
for (int i = 0; i <