# 一篇文章搞懂 ArrayList 核心底层 + 使用场景

一篇文章搞懂 ArrayList 核心底层 + 使用场景

ArrayList 的使用场景

  • 概念:

    1. ArrayList 的底层基于动态数组实现。

    2. 特点:

      • 查询操作快(基于索引定位)。

      • 增删操作较慢(涉及数据移动或扩容)。

        补充:
        对于插入或删除操作频繁的场景(特别是在中间位置插入或删除元素),推荐使用 LinkedList,因为它基于链表实现,插入和删除操作的时间复杂度为 O(1)(前提是已找到位置)。

    3. 适用场景:

      • 在业务中,增删操作较少而查询操作较多的情况下,推荐使用 ArrayList。

ArrayList 的查询本质

前置知识 : 时间复杂度

  • O(1)(常数时间复杂度)
    O(1) 表示操作所需的时间固定,无关数据规模。直接通过索引定位特定元素,无需遍历整个数据结构。

    例子

    arr = [1, 2, 3, 4]
    element = arr[2]  // O(1)
    
  • O(N)(线性时间复杂度)

    ​ O(N) 表示操作所需的时间与数据规模 N 成正比,通常指需要遍历整个数据结构。

    ​ 遍历一个列表、查找一个元素(未排序情况下)等操作都需要逐个访问所有元素。

    arr = [1, 2, 3, 4]
    target = 3
         for (x in arr) {  // O(N)
              if( x == target ) {
                   sout("Find!")
                        break
              }
         }
    
    • 最坏情况:目标元素不存在或在最后一个位置,需要访问整个列表,复杂度为 O(N)。

    • 最佳情况:目标元素是第一个,复杂度是 O(1),但我们通常讨论的是 最坏复杂度

  1. 数组的查询特点:

    • 底层是连续内存存储,通过索引可以直接定位到目标元素的位置(时间复杂度为 O(1))。

      连续内存存储 ?
      人话 : 数据在内存中按顺序分布

    • 这也是数组查询快的原因之一。

  2. ArrayList 的查询特点:

    • ArrayList 是基于数组实现的,因此它也具备通过索引访问的特性,查询某个索引上的元素是常数时间复杂度 O(1)
    • 但是,ArrayList 本身并不支持通过来直接定位元素,也就是说,如果你要查找某个值的位置,需要进行线性搜索,这时候时间复杂度是 O(n)

区分两种查询:

  • 通过索引访问(随机访问/定位访问):

    • arrayList.get(index) 这种操作是 O(1),因为它底层是数组,可以直接通过索引快速访问。

    • 例如:

      ArrayList<Integer> list = new ArrayList<>();
      list.add(10);
      list.add(20);
      System.out.println(list.get(1)); // 直接访问索引1,返回20
      
  • 通过值搜索(线性搜索):

    • arrayList.contains(value)arrayList.indexOf(value) 等操作需要遍历整个数组,查找对应值的位置,时间复杂度是 O(n)

    • 例如:

      ArrayList<Integer> list = new ArrayList<>();
      list.add(10);
      list.add(20);
      System.out.println(list.indexOf(20)); // 需要遍历 ArrayList 才能找到20的位置
      

总结:

  • ArrayList 的查询效率取决于查询方式:
    1. 通过索引访问O(1),性能与数组相同,直接定位
    2. 通过值查找O(n),需要线性搜索

ArrayList 的底层逻辑

数组的默认长度

  • 虽然默认初始容量为 10,但底层数组只有在第一次添加元素时才会被初始化为长度为 10
  • 扩容机制:
    • 每次数组容量不足时,会扩容到原本长度的 1.5 倍

数组的动态初始化与扩容原理

  1. 添加元素的核心逻辑:
    • 入口方法:add(E e)

      public boolean add(E e) {
          modCount++; // 标记数组变动的次数,便于并发操作时的检测
          add(e, elementData, size); // 实际执行添加操作
          return true;
      }
      
      • modCount:记录数组的修改次数,用于并发安全检测。
      • 实际添加逻辑由内部方法 add(e, elementData, size) 实现。
    • 内部方法:add(E e, Object[] elementData, int s)

      private void add(E e, Object[] elementData, int s) {
           if (s == elementData.length) { // 判断是否需要扩容
                elementData = grow();
                elementData[s] = e;          // 添加元素
                size = s + 1;                // 更新实际长度
           }
      }
      
      • 两种情况:
        1. 数组未初始化(elementData 长度为 0):
          • 调用扩容方法 grow() 初始化数组。
        2. 数组已初始化但即将溢出(s == elementData.length):
          • 调用扩容方法 grow() 增加容量。
        3. 如果当前容量足够,则直接添加元素并更新长度。

  1. 扩容的核心逻辑:
    • 扩容触发:grow()

      private Object[] grow() {
          return grow(size + 1); // 模拟现有容量的扩展
      }
      
    • 实际扩容实现:grow(int minCapacity)

      private Object[] grow(int minCapacity) {
          int oldCapacity = elementData.length; // 当前数组容量
          if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
              // 计算新容量
              int newCapacity = ArraysSupport.newLength(oldCapacity, 
                              minCapacity - oldCapacity, 
                              oldCapacity >> 1); // 扩容为原容量的 1.5 倍
              return elementData = Arrays.copyOf(elementData, newCapacity);
          } else {
              // 初始化容量,默认 10
              return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
          }
      }
      
      • 逻辑解析:
        • 已初始化(oldCapacity > 0):
          • 通过 ArraysSupport.newLength() 方法计算扩容后的长度。
        • 未初始化:
          • 初始化数组,默认长度为 10 或等于 minCapacity(更大者)。
      • 核心扩容公式:
        • 新容量 = 旧容量 + Math.max(新增容量, 旧容量的一半)

  1. 扩容长度计算:
    • ArraysSupport.newLength() 方法:
      public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
           int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // 优化容量
           if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
                return prefLength;
           } else {
                return hugeLength(oldLength, minGrowth); // 极限情况下的处理
           }
      }
      
      • 逻辑解析:
        • prefLength 为推荐的新容量,计算方式为旧容量加上较大的增长幅度。
        • 如果推荐的新容量在合理范围内(0 < prefLength <= SOFT_MAX_ARRAY_LENGTH),则直接返回。
        • 超出范围时,调用 hugeLength() 方法确保安全扩容。

ArrayList 总结

  1. ArrayList 的底层通过动态数组实现,初始容量为 10,并根据需要动态扩容。
  2. 扩容策略为 1.5 倍增长,同时确保扩容后的容量在合理范围内。
  3. 优点: 查询速度快,适合读多写少的场景。
  4. 缺点: 增删操作需要移动数据或扩容,性能相对较慢。

如果这篇文章帮到你, 帮忙点个关注呗, 点赞或收藏也行鸭 ~ (。•ᴗ-)✧

在这里插入图片描述
^ '(இ﹏இ`。)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值