ArrayList的add操作
下面是ArrayList的扩容机制,当ArrayList对象调用add(E e)方法时,会首先调用calculateCapacity方法计算需要的最小容量,然后调用ensureExplicitCapacity方法来确保容量足够,可能会触发数组的扩容。
/**
* elementData[] 为当前集合存储的数据
* minCapacity 为当前数组的元素个数再加上当前add的数据,也就是加上1
*/
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 判断当前的数组地址是否与默认的数组地址一样,如一样,代表没扩容过,Math.max返回最大的数,
// DEFAULT_CAPACITY默认为10,minCapacity(数组存储最小容量)可能为10以内,可能为11
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
// minCapacity 数组存储最小容量
private void ensureExplicitCapacity(int minCapacity) {
// 记录集合被修改的次数
modCount++;
// 下面该判断是是为了减少无必要的扩容,当需要的最小容量小于当前数组的长度,则不需要扩容
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
在ensureExplicitCapacity方法中,会判断需要的最小容量是否大于当前数组的长度,如果大于,则会触发数组的扩容。而在calculateCapacity方法中,为了减少无必要的扩容,会判断当前的数组地址是否与默认的数组地址一样,如果是第一次添加元素,则有可能minCapacity小于数组的长度,这样在ensureExplicitCapacity方法中就不会触发数组的扩容。
因此,为了更好地理解代码,可以在注释中加入以上说明。
private void grow(int minCapacity) {
// 获取当前数组长度
int oldCapacity = elementData.length;
// 计算新的数组长度,扩容一般为原数组的1.5倍,即oldCapacity的1.5倍,>>这个是位运算的意思,具体可以自行了解
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果计算得到的新长度小于需要的最小容量,则新长度设置为需要的最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新长度超过了最大数组长度限制,则调用 hugeCapacity 方法进行处理
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 将旧数组拷贝到新数组中,返回新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
// 常量 MAX_ARRAY_SIZE 表示数组的最大长度,这个值是 JVM 可以支持的最大数组长度减去一些安全边界值
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 当需要的数组长度超过最大数组长度时,会调用这个方法进行处理
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
上面就是ArrayList扩容的机制,关注点就是grow(int minCapacity)方法中的int newCapacity = oldCapacity + (oldCapacity >> 1);这行代码,扩容为原来的1.5倍。
ArrayList之所以被认为查询快,增删慢,是因为它底层是基于数组实现的,具有索引,可以直接根据索引查询指定位置的元素。而每次新增操作都要考虑数组是否需要扩容,如果需要扩容,就需要新建一个更大的数组,并把原来的数据拷贝过去,增加了一定的时间和空间成本。删除元素时,需要判断被删除的元素是否处于中间位置,如果是,则需要将右侧的元素移动到左侧,导致增删操作比较慢。因此,在编写代码时,合理选择不同类型的集合,可以提高代码的性能。
LinkedList的add操作
/**
* 将元素添加到链表的末尾
*
* @param e 要添加的元素
*/
void linkLast(E e) {
// 获取当前链表的最后一个节点
final Node<E> l = last;
// 创建一个新的节点,记录当前元素值,上一个节点为 l,下一个节点为空
final Node<E> newNode = new Node<>(l, e, null);
// 将新的节点设置为链表的最后一个节点
last = newNode;
// 如果链表为空,则将新的节点设置为第一个节点
if (l == null) {
first = newNode;
} else {
// 如果链表不为空,则将新的节点设置为上一个节点的下一个节点
l.next = newNode;
}
// 增加链表大小,记录修改次数
size++;
modCount++;
}
LinkedList 中的 remove 方法和 get 方法类似,都是根据索引或元素值查找要删除或获取的节点,并对其进行相应的操作。由于 LinkedList 中的节点保存了上一个节点和下一个节点的引用,所以对于添加和删除操作来说,效率较高,但对于查找操作,需要从最后一个节点开始遍历整个链表,故效率较低,不适合用于大规模的查找操作。 因此ArrayList和LinkedList都是按照顺序插入的,所以存取元素顺序一致的。