List 源码会问哪些面试题
说说你自己对 ArrayList 的理解
很多面试官喜欢这样子开头,考察面试者对 ArrayList 有没有总结经验,介于 ArrayList 内容很多,建议先回答总体架构,再从某个细节出发作为突破口,比如这样:
ArrayList 底层数据结构是个数组,是有序的集合并且可以重复,支持null值,支持随机读写能力,是非线程安全的。其 API 都做了一层对数组底层访问的封装,ArrayList 集合在调用无参构造方法实例化的时候,并没有创建数组,而是在用户第一次执行添加的时候进行创建数组,ArrayList 的 add(),先进行判断数组是否已创建,如果没创建先创建数组(默认大小为10),如果数组容量不够了,add的时候会调用扩容方法grow,每次扩容的大小为 原大小+(原大小>>1) …
扩容类问题
使用 ArrayList 无参构造器,add 一个值进去,此时数组的大小是多少,下一次扩容前最大可用大小是多少?
使用无参构造器,此时数组为空,add 一个值,数组大小为 1 ,下次扩容前最大可用大小为 10
如果我连续往 list 里面新增值,增加到第 11 个的时候,数组的大小是多少?
增加到 11 的时候,此时希望数组的最小容量(minCapacity)为 11,需要扩容,扩容的公式为
newCapacity = oldCapacity + (oldCapacity >> 1)
即 10 + 10/2 = 15,数组会扩容为15。
数组初始化,加入一个值后,如果我使用 addAll 方法,一下子加入 15 个值,那么最终数组的大小是多少
加入第一个值后,数组最大容量为 10 ,addAll 加入 15个,期望数组容量为 16,需要扩容,扩容公式 10 + 10/2 = 15 ,仍达不到我们期望的大小,此时扩容 grow() 方法里有个判断,``
if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
最终扩容后值为 16。
现在有一个很大的数组需要拷贝,原数组大小是 5k,请问如何快速拷贝?
因为原数组比较大,如果新建新数组的时候,不指定数组大小的话,就会频繁扩容,频繁扩容就会有大量拷贝的工作,造成拷贝的性能低下,所以回答说新建数组时,指定新数组的大小为 5k 即可。
为什么说扩容会消耗性能?
扩容底层使用的是
System.arraycopy
方法,会把原数组的数据全部拷贝到新数组上,所以性能消耗比较严重。
源码扩容过程有什么值得借鉴的地方?
- 扩容的思想值得学习,通过自动扩容的方式,让使用者不用关心底层数据结构的变化,封装得很好,1.5 倍的扩容速度,可以让扩容速度在前期缓慢上升,在后期增速较快,大部分工作中要求数组的值并不是很大,所以前期增长缓慢有利于节省资源,在后期增速较快时,也可快速扩容。
- 扩容过程中,有数组大小溢出的意识,比如要求扩容后的数组大小,不能小于 0,不能大于 Integer 的最大值。
删除类问题
有一个 ArrayList,数据是 2、3、3、3、4,中间有三个 3,现在我通过 for (int i=0;i<list.size ();i++) 的方式,想把值是 3 的元素删除,请问可以删除干净么?最终删除的结果是什么,为什么?删除代码如下:
List<String> list = new ArrayList<String>() {{
add("2");
add("3");
add("3");
add("3");
add("4");
}};
for (int i = 0; i < list.size(); i++) {
if (list.get(i).equals("3")) {
list.remove(i);
}
}
不能删除干净,最终删除的结果是 2、3、4,有一个 3 删除不掉,因为每次删除元素之后,数组会往前挪动,而此时循环的 i 在不断地增长
还是上面的 ArrayList 数组,通过增强 for 循环进行删除,可以么?
不可以,会报错。因为增强 for 循环过程其实调用的就是迭代器的 next () 方法,当你调用 list.remove () 方法进行删除时,modCount 的值会 +1,而这时候迭代器中的 expectedModCount 的值却没有变,导致在迭代器下次执行 next () 方法时,expectedModCount != modCount 就会报 ConcurrentModificationException 的错误。
还是上面的数组,如果删除时使用 Iterator.remove () 方法可以删除么,为什么?
可以的,因为 Iterator.remove () 方法在执行的过程中,会把最新的 modCount 赋值给 expectedModCount,这样在下次循环过程中,modCount 和 expectedModCount 两者就会相等。
以上三个问题对于 LinkedList 也是同样的结果么?
是的,虽然 LinkedList 底层结构是双向链表,但对于上述三个问题,结果和 ArrayList 是一致的。
ArrayList 和 LinkedList 有何不同?
-
底层数据结构:
- ArrayList 底层是数组
- LinkedList 底层是双向链表
-
新增的实现:
-
ArrayList 会先计算并决定是否扩容,然后把新增的数据直接赋值到数组上
-
LinkedList 仅仅只需要改变插入节点和其前后节点的指向位置关系即可
-
-
删除的实现:
- ArrayList 将制定节点删除后,指定节点之后的节点需要向前移动
- LinkedList 仅仅只是改变删除节点及其前后节点指向关系即可
-
查询的实现:
- ArrayList 只需要通过瞎编就能找到对应节点,支持快速随机访问
- LinkedList 需要遍历查找(使用二分折半查找)
-
容量:
- ArrayList 有最大容量,为 Integer 的最大值,大于这个值 JVM 不会为数组分配内存空间
- LinkedList 底层是双向链表理论上可以无限大,但在源码中,LinkedList 实际大小用的是 int 类型,所以 LinkedList 也不能超过 Integer 的最大值,不然会溢出
ArrayList 和 LinkedList 应用场景
- ArrayList 更适合快速查找匹配,不适合频繁新增删除
- LinkedList 更适合经常新增和删除,查询少的场景
ArrayList 和 LinedList 是线程安全的么,为什么?
当两者作为非共享变量时,比如说仅仅是在方法里面的局部变量时,是没有线程安全问题的,只有当两者是共享变量时,才会有线程安全问题。主要的问题点在于多线程环境下,所有线程任何时刻都可对数组和链表进行操作,这会导致值被覆盖,甚至混乱的情况。
如果有线程安全问题,在迭代的过程中,会频繁报 ConcurrentModificationException 的错误,意思是在当前循环的过程中,数组或链表的结构被其它线程修改了。
如何解决线程安全问题?
Java 源码中推荐使用 Collections#synchronizedList
进行解决,Collections.synchronizedList
的返回值是 List 的每个方法都加了 synchronized 锁,保证了在同一时刻,数组和链表只会被一个线程所修改,或者采用 CopyOnWriteArrayList
并发 List 来解决