文章目录
-
- 第一步:核心流程概述(高浓度总结)
- 第二步:细节拆解(体现源码功底)
- 第三步:性能与变种延伸(拉开差距的加分项)
-
- [💡 答题锦囊(面试官可能顺藤摸瓜的追问):](#💡 答题锦囊(面试官可能顺藤摸瓜的追问):)
面试官问到 ArrayList的 add 机制,千万不要一上来就背源码,而是要像讲故事一样,把"正常放入 -> 空间不足 -> 扩容 -> 搬家"的整个流程逻辑清晰地连贯起来。
建议采用以下"标准三步走"的逻辑来回答,既能精准命中考点,又显得条理清晰。
第一步:核心流程概述(高浓度总结)
话术 :
"
ArrayList的add()机制核心可以分为四个步骤:检查容量、触发扩容(可选)、尾部插入、返回成功。简单来说,每当我们调用
add(E e)时,它会先判断当前内部数组的剩余空间是否还装得下一个元素。如果够,就直接挂在尾部;如果不够,就会先触发动态扩容,然后再把元素放进去。"
第二步:细节拆解(体现源码功底)
这里你需要把首次添加 和后续扩容的细节抛出来,这是面试官最想听到的:
- 懒加载(延迟初始化) :如果我们用无参构造函数
new ArrayList(),为了节省内存,此时底层其实是一个长度为 0 的空数组 。只有在第一次 调用add()时,它才会真正分配内存,直接初始化一个容量为 10 的数组。 - 计算最小所需容量 :每次
add时,它的内部计数器size会加 1。JVM 会拿size + 1作为"最小所需容量"去和当前数组的长度做对比。 - 1.5 倍扩容 :如果
size + 1大于了数组长度,就会调用grow()方法进行扩容。新数组的容量会变成原数组的 1.5 倍。 - 内存拷贝 :扩容时会通过
Arrays.copyOf()(底层是System.arraycopy)在内存中开辟一块新的连续空间,把老数据原封不动地"搬家"过去。
第三步:性能与变种延伸(拉开差距的加分项)
(主动聊一聊性能和 add(index, element),能让面试官觉得你对数据结构有深度思考)
话术 :
"除了最常用的尾部插入,
ArrayList还有一个重载的 指定位置插入 方法add(int index, E element)。它们的性能损耗是有很大区别的:
- 尾部插入 :在不触发扩容的情况下,时间复杂度是 O ( 1 ) O(1) O(1),效率极高。
- 指定位置插入 :由于数组内存是连续的,为了在中间腾出位置,需要把目标位置后面的所有元素都往后挪动一位( O ( n ) O(n) O(n) 复杂度),因此越往前面插入,性能代价就越大。
所以在实际开发中,如果需要频繁在头部或中间插入元素,我会优先考虑使用
LinkedList。"
💡 答题锦囊(面试官可能顺藤摸瓜的追问):
- ArrayList 是线程安全的吗?
- 不是。它的
add()方法里包含多步操作(如elementData[size++] = e实际上是先赋值再自增,且没有加锁),多线程并发add会导致数据覆盖或ArrayIndexOutOfBoundsException(数组越界异常)。
- 怎么让它线程安全?
- 可以使用
Collections.synchronizedList()包装,或者在读多写少的高并发场景下使用CopyOnWriteArrayList。