ArrayList底层是Object[]数组,非链表或哈希结构;默认容量10,不自动压缩空间,扩容为1.5倍,支持null和重复元素,随机访问O(1),增删平均O(n)。

ArrayList底层用Object[]数组存储元素
它不是链表,也不是哈希结构,就是一个普通的Object[]数组,初始化时默认长度是10(JDK 8),所有元素都存在这个数组里,按插入顺序连续存放。
常见错误是以为ArrayList能自动“压缩”空间——其实不会。即使调用remove()删掉中间元素,数组空位依然存在,只是后续元素前移覆盖;只有显式调用trimToSize()才会真正缩小内部数组容量。
-
add(E e)时若数组已满,会触发扩容:新容量 = 旧容量 × 1.5(即oldCapacity + (oldCapacity >> 1)) - 扩容是通过
Arrays.copyOf()创建新数组并复制,有性能开销,频繁添加建议预设初始容量 - 允许存
null,也支持重复元素,这点和HashSet完全不同
get(int index)和set(int index, E element)是O(1)随机访问
因为底层是数组,所以根据下标直接计算内存偏移量,不遍历、不查找,只要index在合法范围内(0 ≤ index ),就能瞬间拿到或替换元素。
但要注意:如果传入越界下标,会抛IndexOutOfBoundsException,不是NullPointerException,也不是静默失败。
立即学习“Java免费学习笔记(深入)”;
-
get(5)不会检查对应位置是否为null,只检查索引是否越界 -
set(3, "new")要求索引必须已存在(即size > 3),不能用于“扩展”数组 - 想在末尾外插入?得用
add(index, element),但那是O(n)操作,会移动后续所有元素
add()和remove()涉及数组复制,时间复杂度不是常数
在末尾添加(add(E))平均是O(1),但触发扩容时是O(n);在中间或开头增删(add(int, E)或remove(int))一定是O(n),因为要调用System.arraycopy()移动数据。
典型误用场景:用for (int i = 0; i 配合remove(i)遍历删除——会导致漏删,因为每次删除后后续元素前移,下标对不上。
- 安全删除全部匹配项,用
Iterator.remove()或removeIf() - 批量插入多个元素,比反复调用
add()更高效的方式是先收集到另一个集合,再用addAll() -
remove(Object)是按值查找删除第一个匹配项,需遍历,最坏O(n);而remove(int)是按索引,不查值
线程不安全,多线程写入必须额外同步
它的所有方法都没有内置锁,add()、remove()、甚至size()都不是原子操作。两个线程同时add()可能造成数组越界、元素丢失或size错乱。
别用Collections.synchronizedList(new ArrayList())就以为万事大吉——它只保证单个方法原子,复合操作(如“检查是否存在再添加”)仍需手动加锁。
- 高并发读多写少?考虑
CopyOnWriteArrayList,但注意写操作代价极高 - 纯并发计数/累加?优先用
ConcurrentHashMap或LongAdder,别硬套ArrayList - 迭代过程中修改会触发
ConcurrentModificationException,这是fail-fast机制,不是线程安全的保障
实际写代码时,最容易被忽略的是扩容边界和并发组合操作——前者影响性能抖动,后者导致偶发Bug,很难复现。










