Java集合“坑王”:ArrayList为啥越界还能浪?

321 阅读2分钟

提起ArrayList,新手总觉得它就是个“自动扩容的数组”,随便add、get都不怕——直到遇到 IndexOutOfBoundsException ,才发现这货藏着不少小心机。今天咱扒一扒ArrayList的扩容黑科技,以及那些让你踩坑的细节!

一、先搞懂:ArrayList为啥能“自动变长”?

ArrayList底层是普通数组,默认初始容量是10。当添加元素导致数组满了,它会悄悄扩容:新容量=旧容量×1.5(源码里是 oldCapacity + (oldCapacity >> 1) ),然后把旧数组的数据复制到新数组。

看个简单例子,直观感受扩容:

import java.util.ArrayList;

public class ArrayListDemo {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>(); // 初始容量10
        
        // 加10个元素,刚好填满初始数组
        for (int i = 0; i < 10; i++) {
            list.add("元素" + i);
        }
        
        // 第11个元素:触发扩容!新容量变成15
        list.add("触发扩容的元素");
        
        System.out.println("当前元素数量:" + list.size()); // 输出11
        System.out.println("能存但未用的容量:" + (list.size() < 15 ? 15 - list.size() : "已再次扩容")); // 输出4
    }
}

 

二、踩坑重灾区:size和capacity别搞混!

很多人以为 list.size() 是数组容量,其实大错特错:

  •  size() :实际存储的元素数量(你add了多少个)
  •  capacity :底层数组的长度(ArrayList内部用 elementData 数组,容量不对外暴露)

这就是为啥 get(10) 会报错——哪怕数组容量是15,只要你只存了11个元素,索引10是有效的,但索引11就越界:

public static void main(String[] args) {
    ArrayList<String> list = new ArrayList<>();
    for (int i = 0; i < 11; i++) {
        list.add("元素" + i);
    }
    
    System.out.println(list.get(10)); // 正常输出:元素10
    list.get(11); // 直接抛IndexOutOfBoundsException!
}

 

三、实用技巧:提前指定容量,避免频繁扩容

如果知道要存多少元素,初始化时直接指定容量,能减少数组复制的性能损耗:

// 已知要存1000个元素,直接指定容量
ArrayList<String> list = new ArrayList<>(1000);
// 后续add1000个元素,一次扩容都不会触发

 

最后划重点

1. ArrayList扩容是“偷偷摸摸”的,扩容后旧数组会被GC回收;

2. 越界异常看的是 size() ,不是底层数组容量;

3. 大数据量场景一定要指定初始容量,优化性能。