ArrayList和LinkedList源码解析

159 阅读4分钟

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都是按照顺序插入的,所以存取元素顺序一致的。