1.新建一个ArrayList,现在add一个值,此时数组的大小是多少?下一次扩容前最大可用大小是多少?
答:此处数组的大小是1,下一次扩容前最大可用大小是10。因为ArrayList无参构造器初始化时,默认大小是空数组,第一次添加元素的时候进行扩容,大小是默认的10。
2.如果连续往ArrayList里面新增值,增加到第11个的时候,数组的大小是多少?
答:此时的数组大小是15。这里考查的是扩容公式,当增加到第11个元素的时候,此时希望数组的大小为11,但实际上数组的最大容量只有10,此时便需要扩容,扩容的公式是oldCapacity + (oldCapacity>> 1),oldCapacity表示数组现有大小,目前场景计算公式是10 + 10 / 2 = 15,然后发现11满足需求,因此数组的大小会被扩容到15。
3.数组初始化,被加入一个值后,如果使用addAll方法,一下加入15个值,那么最终数组的大小是多少?
答:最终数组的大小是16。第一题中已经计算出来数组在加入一个值后,实际大小是1,最大可用大小是10 ,现在需要一下加入15个值,那期望数组的大小值就是16,此时数组最大可用大小只有10,明显不够,需要扩容,扩容后的大小是10 + 10 /2 = 15,这时候发现扩容后的大小仍然不到期望的值16,这时,会采用源码中的如下策略:
//newCapacity本次扩容的大小,minCapacity期望的数组最小大小
//如果扩容后的值 < 期望值,期望值就等于本次扩容的大小
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
所以最终数组扩容后的大小为16。
4.现在有一个很大的数组需要拷贝,原数组大小是5k,请问如何快速拷贝?
答:新建数组时指定新数组的大小为5k。因为原数组比较大,如果新建新数组的时候,不指定数组大小的话,就会频繁扩容,频繁扩容就会有大量的拷贝工作,造成额外的性能开销。
5.为什么说扩容会消耗性能?
答:扩容时,底层使用的是System.arraycopy方法,会把原数组的数据全部拷贝到新数组上,所以性能消耗比较严重。
6.有一个ArrayList,数据是 2、3、3、3、4,现通过for (int i = 0; i < list.size(); i++)的方式,把值为3的元素删除,请问可以删除干净么?如果删除不干净,那么最终删除的结果是什么,以及为什么会是这样的结果?
public static void main(String[] args) throws Exception {
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在不断增长。所以,删除第1个3后,第2个3会被遗漏,导致删不掉。
7.对于6中的问题,可以通过增强for进行删除吗?
答:不可以,会报ConcurrentModificationException异常。因为增强for在循环时,调用的是迭代器的next ()方法,当调用remove()方法进行删除时,modCount的值会+1,而这时迭代器中的expectedModCount值却没有变,导致在迭代器下次执行next()方法时,expectedModCount != modCount就会报ConcurrentModificationException。
8.对于6中的问题,可以使用Iterator.remove()方法删除吗?
答:可以。因为Iterator.remove()方法在执行过程中,会把最新的modCount赋值给 expectedModCount,这样在下次循环时,modCount和expectedModCount就会相等。
9.ArrayList和LinkedList有何不同?
答:最大的不同是两者底层数据结构不同,ArrayList底层是数组,LinkedList底层是双向链表,两者的数据结构不同也导致了操作API的实现有所差异。拿新增实现来说,ArrayList会先计算并决定是否扩容,然后把新增的数据直接赋值到数组上,而LinkedList仅仅是改变插入节点和其前后节点的指向关系。其次是应用场景不同,ArrayList更适合快速查找匹配的场景,而LinkedList更适合频繁新增或删除的场景。
10.ArrayList和LinkedList两者有没有最大容量?
答:ArrayList有最大容量,最大容量为Integer的最大值,大于这个值JVM是不会为数组分配内存空间的,而LinkedList底层是双向链表,理论上可以无限大。但源码中,LinkedList实际大小用的是int类型,这也说明了LinkedList不能超过Integer的最大值,不然也会溢出。
11.ArrayList和LinkedList是如何对null值进行处理的?
答:ArrayList允许新增null值,也允许删除null值。删除null值时,是从头开始,找到第一值是null的元素删除。LinkedList新增删除时对null值没有特殊校验,也是允许新增和删除的。
12.ArrayList和LinedList是线程安全的吗,为什么?
答:当两者作为非共享变量时,比如说仅仅是作为方法里面的局部变量时,是没有线程安全问题的,只有当两者是共享变量时,才会有线程安全问题。主要的问题点在于多线程环境下,所有线程在任何时刻都可对数组和链表进行操作,这会导致值被覆盖,甚至混乱的情况。如果有线程安全问题,在迭代的过程中,会频繁报ConcurrentModificationException的异常,意思是在当前循环过程中,数组或链表的结构被其它线程修改了。
13.如何解决ArrayList和LinedList的线程安全问题?
答:Java源码中推荐使用Collections的synchronizedList方法进行解决,synchronizedList的返回值是所有方法都加了synchronized锁的list,保证了在同一时刻,数组和链表只会被一个线程所修改,或者采用CopyOnWriteArrayList来解决,这个类后面会详细说明。
14.你能描述下双向链表吗?
答:双向链表中双向的意思是说前后节点之间互相有引用,链表的节点称为Node。Node有三个属性组,前一个节点,本身节点和下一个节点,假设A、B节点相邻,A节点的下一个节点就是B,B节点的上一个节点就是A,两者互相引用。在链表的头部节点,称为头节点,头节点的前一个节点是null,尾部称为尾节点,尾节点的后一个节点是null,如果链表数据为空,头尾节点是同一个节点,本身值是null,指向前后节点的值也是null。
15.描述下双向链表的新增和删除?
答:新增时可以选择从链表头部新增,也可以选择从链表尾部新增,如果是从链表尾部新增的话,直接把当前节点追加到尾节点之后,本身节点自动变为尾节点。删除时把删除节点的后一个节点的prev指向其前一个节点,把删除节点的前一个节点的next指向其后一个节点,最后把删除的节点置为null即可。