arraylist扩容是新建1.5倍容量数组并拷贝元素,非简单加格子;默认构造初始容量为0,首次add即扩至10,预知元素量应指定初始容量;add末尾为o(1)均摊,中间插入或删除为o(n)移动。

ArrayList扩容时到底发生了什么
扩容不是“加几个格子”,而是整个数组内存重分配。每次add触发容量不足时,ArrayList会新建一个长度为oldCapacity + (oldCapacity >> 1)(即1.5倍)的Object[],再把老数组内容System.arraycopy拷过去。
这意味着:频繁在小容量下反复add,会产生大量临时数组和拷贝开销;而一次性预估不准,又浪费内存。常见错误是默认构造(new ArrayList()),初始容量为0,第一次add就扩容到10——但如果你明确知道要存1000个元素,直接new ArrayList(1000)更稳。
- 扩容阈值由
size >= elementData.length触发,不是“满了才扩”,而是“要放不下了才扩” -
ensureCapacity可以手动预热容量,但一般没必要;trimToSize()能缩容,但极少用(GC压力小,且后续add又得扩) - 并发场景下扩容+写入可能引发
ConcurrentModificationException,这不是线程安全问题,而是fail-fast机制在报错
add/remove操作的真实成本差异
add(E e)在末尾追加是O(1)均摊,但add(int index, E element)在中间插入是O(n),因为要移动后续所有元素;同理,remove(int index)也是O(n)移动,而remove(Object o)先遍历再移,最坏O(n)查找+O(n)移动。
容易踩的坑是循环中用remove删元素还用普通for索引递增:for (int i = 0; i ——这会导致漏删(删掉i后原i+1变成新i,但i已自增)。正确做法是倒序遍历,或用<code>Iterator.remove()。
立即学习“Java免费学习笔记(深入)”;
- 批量删除建议用
removeIf(Predicate),它内部用位图标记+单次复制,比循环调用remove快 - 如果只关心存在性、不在乎顺序,
LinkedList对中间增删更友好,但随机访问退化成O(n) -
add(null)完全合法,ArrayList允许null元素;但remove(null)只会删第一个null,不是全部
get/set为什么快,以及什么时候会变慢
get(int index)和set(int index, E element)都是O(1),靠数组下标直取。但前提是index合法——越界会立刻抛IndexOutOfBoundsException,不是静默失败。
性能陷阱藏在泛型擦除和自动装箱里:比如ArrayList<integer></integer>,get(0)返回Integer,若赋给int变量会触发自动拆箱;若此时值为null,运行时抛NullPointerException。这个异常不在编译期报,容易漏测。
- 不要用
list.get(i)反复查同一位置,缓存引用更安全(尤其在循环内) -
set不改变size,只替换;若index等于size,会抛异常,不能“扩展式set” - 多线程读写同一个
ArrayList,即使只用get,也可能看到部分初始化的数组(因缺乏volatile语义),必须同步或换Collections.synchronizedList
toArray()的两种用法与空指针风险
toArray()(无参)返回Object[],不能强转成具体类型数组;toArray(T[] a)才是类型安全的用法,但很多人写成list.toArray(new String[0])或list.toArray(new String[list.size()]),前者创建了多余数组,后者在JDK 11+虽可,但仍有兼容顾虑。
最稳妥写法是list.toArray(new String[0]):传入长度为0的数组,让ArrayList内部用反射新建正确类型的数组。传null会直接NPE;传长度不够的数组,ArrayList会新建;传过长的,多出部分填null——但你得手动截断,否则可能引发后续NPE。
-
toArray(new T[0])是JDK推荐写法,避免类型检查失败和冗余对象 - 如果确定只读且不需要修改,用
list.toArray()拿到Object[]也行,但别试图强转 - 流式转换如
list.stream().toArray(String[]::new)本质也是调toArray,性能略低(额外对象开销),但语义清晰
扩容策略、索引边界、泛型擦除带来的隐式拆箱、数组类型转换——这些点单独看都简单,但组合起来就是线上NullPointerException和偶发ConcurrentModificationException的温床。写的时候多想半秒,比上线后翻日志快得多。









