ArrayList的核心字段
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;
通过上方可以看出默认的容量是10,还存在两个空数组,以及底层的真正的数据数组与实际的数据大小。
-
底层是数组:所有元素连续存储在内存中
-
size ≠ capacity:size是元素数量,capacity是数组长度
-
自动扩容:当数组满时自动创建更大的新数组
-
EMPTY_ELEMENTDATA:表示一个真正的空数组实例,当ArrayList通过指定初始容量为0的构造函数创建时使用。在添加第一个元素时,会直接分配一个所需大小的新数组(最小为1),不涉及默认容量逻辑。
-
DEFAULTCAPACITY_EMPTY_ELEMENTDATA:作为一个特殊标记,表示使用默认初始容量(通常为10)。当ArrayList通过默认构造函数(无参数)创建时使用。在添加第一个元素时,会触发延迟内存分配,直接分配一个大小为默认容量(10)的新数组。
底层数组的序列化
我们发现真实的底层数组使用了transient进行修饰,表明这个字段不会被Java默认的序列化机制处理,因为 elementData数组的容量(length)通常大于实际存储的元素个数(size)。如果直接序列化整个数组,会序列化大量无用的 null值,浪费空间和时间。
ArrayList自己实现了writeObjec和readObject方法。在这两个方法中,它手动地、只序列化和反序列化实际包含的元素(从 0到 size-1),而不是整个elementData数组。
源码如下:
@java.io.Serial
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioral compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
@java.io.Serial
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// like clone(), allocate array based upon size not capacity
SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Object[].class, size);
Object[] elements = new Object[size];
// Read in all elements in the proper order.
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的构造方法
ArrayList有三种构造方法:
- 无参构造
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
无参构造是直接将DEFAULTCAPACITY_EMPTY_ELEMENTDATA这个空数组赋给底层数组。
- 有参构造
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
有参构造中传入一个初始的大小,如果这个值大于0,则创建一个大小为这个值的数组,如果等于0,则将一个空数组赋给底层数组。
- 集合参数构造
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
先将原本的集合转为数组,然后再拷贝一份后赋给底层数组。
这里有一个问题:拷贝的目的是为了防止影响原本集合的数据,但是为什么该集合是ArrayList就不需要拷贝了呢?
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
原来ArrayList中的toArray方法已经是拷贝底层数组了。
ArrayList的增加元素
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
它实际上会调用一个内部方法add
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
如果当前的底层数组的长度与当前ArrayList的大小相等,意味着需要扩容,之后再加入数据。
扩容机制1
private Object[] grow() {
return grow(size + 1);
}
grow方法会调用另一个grow方法,并传入一个minCapacity,这代表了至少需要的容量
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow
if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
return prefLength;
} else {
// put code cold in a separate method
return hugeLength(oldLength, minGrowth);
}
}
当底层数组有数据时,会调用ArraysSupport.newLength,其逻辑是旧数组的长度+ minCapacity - oldCapacity和 oldCapacity >> 1的最大值。
- 当要求新增的容量<1.5倍原容量时,数组扩容为原先的1.5倍
- 当要求新增的容量>1.5倍原容量时,数组扩容为旧容量+要求新增的容量
当底层数组是EMPTY_ELEMENTDATA时,依旧调用ArraysSupport.newLength,此时oldCapacity>>1=0,故此时数组扩容为要求新增的容量。
当底层数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA时,数组扩容为DEFAULT_CAPACITY和要求新增容量的最大值。
为什么总是需要和要求新增的容量对比呢?原因很简单,因为扩容之后起码要容纳的下所有的数据。
扩容机制2
在newLength方法中如果即将返回的数组大小超过了SOFT_MAX_ARRAY_LENGTH或者小于0,将使用hugeLength,传入oldGrowth和minGrowth。
private static int hugeLength(int oldLength, int minGrowth) {
int minLength = oldLength + minGrowth;
if (minLength < 0) { // overflow
throw new OutOfMemoryError(
"Required array length " + oldLength + " + " + minGrowth + " is too large");
} else if (minLength <= SOFT_MAX_ARRAY_LENGTH) {
return SOFT_MAX_ARRAY_LENGTH;
} else {
return minLength;
}
}
当小于0时,说明两者相加溢出了,会报错,当minLength <= SOFT_MAX_ARRAY_LENGTH时,说明在newLength是oldLength+prefGrowth超过了这个界限,因此尽可能分配更大的空间。
如果minLength> SOFT_MAX_ARRAY_LENGTH,则直接返回minLength。
SOFT_MAX_ARRAY_LENGTH的作用是什么?
为底层数组的扩容提供一个比理论最大值(Integer.MAX_VALUE)更小、更安全的容量上限。其主要目的是确保在极端扩容场景下,为 JVM 的对象头预留空间,提高内存分配的可靠性和跨 JVM 实现的兼容性,防止因数组过大而直接引发底层内存错误。
添加大容量数据的场景
因为ArrayList是动态扩容的,如果需要增加很多数据,不应该一个个加入,这样可能会触发多次扩容,导致效率极低。
应该在加入元素前,使用ensureCapacity来判断是否需要扩容,并直接扩容到对应的容量。
public void ensureCapacity(int minCapacity) {
if (minCapacity > elementData.length
&& !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
&& minCapacity <= DEFAULT_CAPACITY)) {
modCount++;
grow(minCapacity);
}
}
RandomAccess
ArrayList实现了RandomAccess接口,这个接口的存在主要是为了给泛型算法提供性能优化的提示。
例如,Collections工具类中的一些方法(如 Collections.binarySearch, Collections.shuffle, Collections.reverse等)会根据传入的 List是否实现了 RandomAccess来选择不同的底层算法,以达到最佳性能。
- 对于实现了
RandomAccess的List(如ArrayList) :可能会使用基于索引的循环遍历,效率更高。 - 对于未实现
RandomAccess的List(如LinkedList) :可能会使用迭代器进行顺序访问,避免因频繁的get(index)调用导致的性能灾难。
Vector
ArrayList是List的主要实现类,底层使用Object[]存储,适用于频繁的查找工作,线程不安全 。Vector是List的古老实现类,底层使用Object[]存储,线程安全。
Vector之所以线程安全,是因为其方法上使用了synchronized修饰