关注

【java基础内容】ArrayList与LinkedList的区别及ArrayList源码解析

前言:

ArrayList 与 LinkedList 的核心区别

ArrayList 源码深度解析

构造方法:初始化时到底有没有创建10个大小的数组?

情况一:无参构造

情况二:指定容量构造

核心方法:add(E e) 与扩容机制

第一步:计算容量

第二步:判断是否需要扩容

第三步:核心扩容逻辑 grow()

如何选择

总结


前言:

在Java开发中,List接口是最常用的集合接口之一,而ArrayListLinkedList则是它的两个核心实现类。很多初学者甚至工作多年的开发者,往往只知道“ArrayList查询快,增删慢LinkedList增删快,查询慢”这句口诀。

本文将摒弃空谈,从底层数据结构源码实现以及性能分析三个维度,彻底搞清楚这两者的区别,并重点深入剖析ArrayList的源码,看看它究竟是如何工作的。

ArrayList 与 LinkedList 的核心区别

在开始看源码之前,我们先通过一张表快速建立直观的认知:

维度ArrayListLinkedList
底层数据结构动态数组 双向链表
内存占用连续内存空间,可能浪费尾部空间非连续内存,每个节点需存储前后指针,内存占用更大
随机访问O(1),效率极高O(n),需遍历链表,效率低
插入/删除 O(n),涉及数组元素的复制移动O(n),虽然找到位置需要遍历,但操作本身只需修改指针指向;若在头尾操作,效率极高为O(1)
尾部插入 O(1) (均摊),需考虑扩容O(1)
迭代效率较高,对CPU缓存友好

较低,节点在内存中分散,缓存命中率低

比较他们不同的时候从,底层结构,性能,内存,线程安全来说。他们线程都不安全。

一句话总结:
绝大多数业务场景(以遍历和随机访问为主),ArrayList 完胜 LinkedList。只有在频繁头部插入/删除且数据量极大时,LinkedList 才有理论上的优势。

ArrayList 源码深度解析

环境基于 JDK 1.8

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // 默认初始容量
    private static final int DEFAULT_CAPACITY = 10;
    
    // 空数组实例,用于构造方法指定初始容量为0时
    private static final Object[] EMPTY_ELEMENTDATA = {};
    
    // 默认容量的空数组实例,用于无参构造,用于区分第一次添加元素时的扩容逻辑
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    // 真正存储元素的数组,使用 transient 修饰,不参与默认序列化
    transient Object[] elementData;
    
    // 集合中实际元素个数
    private int size;
}

构造方法:初始化时到底有没有创建10个大小的数组?

这是一个经典的面试题。

情况一:无参构造
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

此时,elementData 指向的是一个空数组,并没有初始化容量为10。延迟加载(懒加载)策略,等到第一次 add 时才真正分配空间。

情况二:指定容量构造
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的数,那么就会直接创造指定大小的数组,如果指定容量为 10,则会立即创建一个长度为 10 的数组。

核心方法:add(E e) 与扩容机制

这是 ArrayList 最精髓的部分。

public boolean add(E e) {
    // 1. 确保内部容量足够(核心扩容逻辑)
    ensureCapacityInternal(size + 1);  // 传入最小需要容量 minCapacity = size + 1
    // 2. 赋值
    elementData[size++] = e;
    return true;
}
第一步:计算容量
private void ensureCapacityInternal(int minCapacity) {
    // 如果当前数组是默认的空数组(无参构造创建的)
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 取 DEFAULT_CAPACITY (10) 和 minCapacity 的最大值
        // 第一次 add 时,minCapacity = 1,所以这里会返回 10
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // 进入真正的扩容判断
    ensureExplicitCapacity(minCapacity);
}
第二步:判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
    modCount++; // 记录结构修改次数,用于快速失败机制(fail-fast)

    // 如果需要的容量 大于 当前数组的长度,则必须扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
第三步:核心扩容逻辑 grow()
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // 关键点:新容量 = 旧容量 + 旧容量 >> 1 (即原来的 1.5 倍)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    // 如果扩容 1.5 倍后还是小于最小需求容量(这种情况极少,发生在初始容量为0或1时)
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    
    // 如果新容量超过了规定的最大数组容量 (Integer.MAX_VALUE - 8)
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    
    // 核心:数组复制,产生一个新的数组,将原数据拷贝过去
    elementData = Arrays.copyOf(elementData, newCapacity);
}

扩容机制总结:

  1. 触发时机:当 size + 1 大于当前数组长度时。

  2. 扩容大小:变为原来的 1.5 倍oldCapacity + (oldCapacity >> 1))。

  3. 性能影响:扩容涉及 Arrays.copyOf,这是一个耗时的 O(n) 操作。如果能预估数据量,使用 new ArrayList<>(int capacity) 指定大小,可以避免频繁扩容,显著提升性能。

如何选择

优先使用 ArrayList:在绝大多数业务开发中(展示列表、查询数据、批量插入后仅遍历),ArrayList 是唯一正确的选择。

  1. 使用 LinkedList 的场景

    • 实现 队列(Queue) 或 双端队列(Deque) 结构,利用 offerpollpeek 等 API。

    • 需要频繁在列表头部进行插入或删除,且数据量极大(百万级以上)。

  2. 避免的错误

    • 不要使用 LinkedList 存储大量数据仅仅因为你需要偶尔的中间删除。

    • 使用 ArrayList 时,如果能预估数据量,务必指定初始容量,避免频繁扩容带来的性能损耗。

总结

底层结构ArrayList 是数组,LinkedList 是双向链表。

性能ArrayList 查询快,中间增删慢;LinkedList 查询慢,头尾增删快。

源码重点ArrayList 的扩容机制是 1.5 倍,无参构造延迟初始化。

场景:开发中 90% 以上的场景用 ArrayList,只有在需要实现队列或者极端高频的头尾操作时,才考虑 LinkedList

转载自CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/uybji/article/details/159734756

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--