ArrayList 默认容量为0(JDK12+)或10(早期),size()始终为0直到add();add()末尾追加高效,set()需索引<size(),remove()移除元素导致O(n)移动;遍历时修改会抛ConcurrentModificationException。

Java 中的 ArrayList 本身就是可变长度的动态数组,不需要“实现”它——你需要的是正确使用它,避开扩容机制带来的隐性开销和并发陷阱。
为什么 new ArrayList() 后容量不等于 size
刚创建的 ArrayList(如 new ArrayList())内部数组默认是空的(JDK 12+ 用空数组,早期版本用长度为 10 的数组),但 size() 返回 0。只有调用 add() 才真正写入元素并更新 size;而底层数组扩容发生在 add() 触发容量不足时。
- 首次
add()可能触发扩容:JDK 17 中,空ArrayList第一次add()会分配长度为 10 的数组 - 扩容不是每次 +1,而是按 1.5 倍增长(如从 10 → 15 → 22 → 33…),避免频繁复制
- 如果已知大概元素数量,用
new ArrayList(initialCapacity)预设容量,减少扩容次数
add()、set() 和 remove() 的行为差异
add(E) 在末尾追加,安全且高效;set(int, E) 是替换指定索引处已有元素,**要求索引必须 size()**;remove(int) 删除指定索引元素,之后所有后续元素前移——这导致 O(n) 时间复杂度。
- 对中间位置高频修改?考虑
LinkedList或改用索引无关结构(如Map) -
set(0, x)合法,但set(5, x)在size()为 3 时抛IndexOutOfBoundsException -
remove()重载了对象值删除(remove(Object)),注意它调用equals(),可能误删
遍历时修改列表的典型崩溃场景
直接在 for-each 或普通 for (int i = 0; i 循环中调用 <code>remove() 或 add(),几乎必然触发 ConcurrentModificationException(即使单线程)。
你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在
立即学习“Java免费学习笔记(深入)”;
- 安全做法:用
Iterator.remove()——Iterator允许在遍历中删除当前元素 - 需要边遍历边添加?先收集待添加元素,循环结束后统一
addAll() - 多线程读写?
ArrayList不是线程安全的,别加synchronized了事,改用Collections.synchronizedList()或CopyOnWriteArrayList(注意后者适用于读多写少)
toArray() 的坑:为什么 new String[0] 比 new String[size()] 更好
list.toArray(new String[0]) 是推荐写法;传入一个长度为 0 的数组,让 ArrayList 内部新建合适大小的数组返回。而 list.toArray(new String[list.size()]) 看似省事,实则可能因类型擦除导致运行时返回 Object[](尤其泛型类型非 String 时)。
- JDK 11+ 中,
toArray(IntFunction)更直观:list.toArray(String[]::new) - 不要用
list.toArray()(无参)直接强转——它永远返回Object[],强转会报ClassCastException - 数组是协变的,但泛型不是;这个细节在跨模块传递数据时容易暴露
真正难的不是怎么“用”,而是判断什么时候不该用 ArrayList:插入删除频繁、需排序保证、元素量级达百万且内存敏感、或明确要求不可变——这些时候,ArrayList 的默认行为就成了瓶颈本身。

















