面试官:小厂大厂10道ArrayList高频面试题

上一篇ArrayList基础知识必知必会  本篇文章的很多内容可以参数上篇文章

 

微信扫码关注

 

1、如何复制某个ArrayList到另一个ArrayList中去?写出你的代码?

  • 使用clone()方法,比如ArrayList newArray = oldArray.clone();

  • 使用ArrayList构造方法,比如:ArrayList myObject = new ArrayList(myTempObject);

  • 使用Collection的copy方法。

    注意前两种是浅拷贝(shallow copy)

2、ArrayList的默认容量大小?

    在Java1.8中,如果new ArrayList(),则默认的容量是0,在第一次添加数据时会设置默认的容量为10。后续扩容是会增长到原来的1.5倍,如果增长后长度大于MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8),则最大长度为Integer.MAX_VALUE。

 

3、ArrayList是如何扩容的?必会

    ArrayList的扩容主要发生在向ArrayList集合中添加元素的时候,通过add()方法添加单个元素时,会先检查容量,看是否需要扩容。如果容量不足需要扩容则调用grow()扩容方法,扩容后的大小等于扩容前大小的1.5倍,也就是10+10/2。比如说超过10个元素时,会重新定义一个长度为15的数组。然后把原数组的数据,原封不动的复制到新数组中,这个时候再把指向原数的地址换到新数组

// grow扩容方法
private void grow(int minCapacity) {
    // 记录扩容前的数组长度
    int oldCapacity = elementData.length;
    // 位运算,右移动一位。整体相当于newCapacity =oldCapacity + 0.5 * oldCapacity
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果扩容后的长度小于当前的数据量,那么就将当前的数据量的长度作为本次扩容的长度
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 判断新数组的长度是否大于可分配数组的最大值
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        // 将扩容长度设置为最大可用长度
        newCapacity = hugeCapacity(minCapacity);
    // 拷贝,扩容,构建一个新的数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

 

4、ArrayList频繁扩容导致添加性能急剧下降,如何处理?

    使用ArrayList时,可以 new ArrayList(大小)构造方法来指定集合的大小,以减少扩容的次数,提高写入效率。那么这个大小指定多少合适呢?需要根据你的业务实际分析得出答案,eq:每次查询20条,那就设置20即可。

 

5、ArrayList用来做队列合适么?

    队列一般是FIFO(先入先出)的,如果用ArrayList做队列,需要在数组尾部追加数据,数组头部删除数组,反过来也可以。但是无论如何总会有一个操作会涉及到数组的数据搬迁,这个是比较耗费性能的。

虽然ArrayList不适合做队列,但是数组是非常合适的。比如ArrayBlockingQueue内部实现就是一个环形队列,它是一个定长队列,内部是用一个定长数组来实现的。另外著名的Disruptor开源Library也是用环形数组来实现的超高性能队列,具体原理不做解释,比较复杂。简单点说就是使用两个偏移量来标记数组的读位置和写位置,如果超过长度就折回到数组开头,前提是它们是定长数组

 

6、ArrayList线程安全吗?

    ArrayList是线程不安全的。

当开启多个线程操作List集合,向ArrayList中增加元素,同时去除元素。最后输出list中的所有数据,会出现几种情况:

①有些元素输出为Null;②数组下标越界异常。

有两种解决方案:

第一种是选用线程安全的数组容器是Vector,它将所有的方法都加上了synchronized。

public static Vector<Object> vector= new Vector<Object>();

二种是用Collections.synchronizedList将ArrayList包装成线程安全的数组容器。

List<String> list = Collections.synchronizedList(new ArrayList<>());

为什么ArrayList线程不安全,我们还使用它?

因为大多数的场景中,查询操作使用频率高,增删操作的使用频率低。如果涉及频繁的增删,可以使用LinkedList,实际开发过程中还是ArrayList使用最多的。不存在一个集合既查询效率高,又增删效率高,还线程安全的,因为数据结构的特性就是优劣共存的,想找个平衡点很难,牺牲了性能,那就安全,牺牲了安全那就快速。

7、Java集合的快速失败机制 “fail-fast”?

    是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。

例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

解决办法:

  1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。

  2. 使用CopyOnWriteArrayList来替换ArrayList

8、怎么确保一个集合不能被修改?

可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 

Java. lang. UnsupportedOperationException 异常。

 

9、遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么?

遍历方式有以下几种:

  • for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。

  • 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。

  • foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。

最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。

  • 如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。

  • 如果没有实现该接口,表示不支持 Random Access,如LinkedList。

推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历


10、Arraylist最多可以存储多少条数据?Arraylist存满之后会发生什么?如何解决呢?

1)最多存储数据量

    Integer.MAX_VALUE 2147483647

2)满了之后add

  • 内存溢出

    首先list在被创建出来时会存放在jvm堆内存中,假设一个对象为1kb的大小,存放10个对象的数组大小就是10kb。而数组最大限制是2147483647,这就要求硬件内存超过2147483647kb(实际使用中可能需要2147483647kb*2 因为list在add的时候是需要扩容的,除非你在创建的时候指定大小为最大值,否则你在add的时候超过内存阀值会直接抛出内存溢出)

  • 数组越界

    当size达到int最大值以后,size++ 这个运算并不会报错(具体可以看java中的int类型计算),那存满之后再存值为什么会数组越界呢

3)实际开发中遇到解决方案

  • 链表形式存储(LinkedList)

为避免大量的扩容导致内存溢出,可以选择使用LinkedList,它里面是链表的结构,并不会有大小限制,不过使用它的话取值只能通过循环比较去取值,在保证内存够用的情况下,理论上可以想存多少存多少。但是取值效率会因为值太多效率极低。可以简单看下(LinkedList)里面的实现

  • map分级存储

如果即想存多一些的数据又想查询快一些可以通过所存的对象中的业务逻辑划分为map对象存储,比如学生表中有type,优/良/差 ,type为key,list为value,但是key分级少了,数据还是会很多,可以考虑多层分级,比如type基础上按年纪,班级再分层,到最后不就越来越少,插值或者查询都可以提高效率。

  • 数据库(pg,mysql,oracle等)

    与你同在架构之路一个专注于java软件架构师成长专栏1篇原创内容公众号

微信扫码关注

    • 点击关注一波,让我们一去学习、分享,共同进步,不进则退。

    •  

posted @ 2021-03-13 15:51  宇你同在  阅读(310)  评论(0)    收藏  举报