常见ArrayLIst面试题

Posted 写Bug的渣渣高

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了常见ArrayLIst面试题相关的知识,希望对你有一定的参考价值。

文章目录

ArrayList

1,甚至可以存放null

2.基于数组是实现数组存储

3.无线程安全控制,即线程不安全,基本等同于Vector ,在多线程情况下不建议ArrayList

数组扩容效率问题

当需要加入的数据量特别多,如果是无参的话,数组的容量是逐渐增加的,那么就会触发很多次的扩容,因为扩容的时候会使用到数组拷贝,这个过程很耗费性能,会导致ArrayList效率下降

//这里是测试当添加很多数据时,ArrayList性能急剧下降的问题
public class AddManyElementProblem 
    public static void main(String[] args) 
        add1();
    
//  如果指定了容量
    public static void add1()
        //创建集合对象
        List<String> list = new ArrayList<String>();
//添加元素
        list.add("hello");
        list.add("php");
        list.add("Java");
        long startTime = System.currentTimeMillis();
//需求:还需要添加10W条数据
        for (int i = 0; i < 100000; i++) 
//            优化效果图
//            注意:这种优化方式只针对特定的场景,如果添加的元素是少量的、未知的,不推荐使用
//            4.3 ArrayList插入或删除元素一定比LinkedList慢么?
//                    根据索引删除
//            案例:ArrayList和LinkedList对比
            list.add(i+"");
        
        long endTime = System.currentTimeMillis();
        System.out.println("未指定容量: "+ (endTime - startTime));
//创建集合的时候指定足够大的容量
        List<String> list1 = new ArrayList<String>(100000);
        startTime = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) 
            list1.add(i+"");
        
        endTime = System.currentTimeMillis();
        System.out.println("指定容量: "+ (endTime - startTime));
    

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t1Kgj232-1658498591138)(images.images/image-20220722213420918.png)]

底层和源码分析

1.维护了一个Objec的数组,transient Object[] elementData

2 当创建对象时,若使用无参构造,则初始容量为0,此时第一次添加就需要扩容为10,如需再次扩容为1.5倍

3.如果使用指定大小构造器,扩容同样1.5

添加方法:

​ 首先确定索引是否范围合理,然后确定容量是否满足,这里的原理是:在极端情况下,此时容量=长度=size,那么当前所需要的最小长度是size+1,假如size+1大于当前长度,就应该扩容。如果不需要扩容,那么直接赋值即可

    public void add(int index, E element) 
        rangeCheckForAdd(index);//检测索引范围是否正确

        ensureCapacityInternal(size + 1);  //这里是判断是否需要扩容,原理是:在极端情况下,此时容量=长度=size,那么当前所需要的最小长度是size+1,假如size+1大于当前长度,就应该扩容
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    

modCount记录修改次数,minCapacity-elementData.length若大于零,代表当前长度不够存储

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BEIIKpXh-1658498591138)(images.images/image-20220601104236172.png)]

    private void grow(int minCapacity) 
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);//原先数组1.5倍,但是第一次是0
        if (newCapacity - minCapacity < 0)//如果,
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)//若大于最大数组限制
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        // 真正扩容的是这里,拷贝到新数组并且设置新的容量
        elementData = Arrays.copyOf(elementData, newCapacity);
    

扩容

无参构造创建时容量为0,第一次add时候会扩容,否则他内部数组就是一个空数组,然后每次数组满之后,再次添加,会触发扩容机制,扩容1.5倍
有参构造,创建时容量为传入的值,数组满之后,再次添加,会触发扩容机制,扩容1.5倍。

注意:扩容的时机都是内部数组满了之后,再次add才会扩容

添加流程

ensureCapacityInternal:判断是否扩容

public boolean add(E e) 
    ensureCapacityInternal(size + 1);  //这里判断是否要扩容,size+1代表着极端情况下最小的容量,因为极端情况下,当前数组满了,所有需要的最小容量是size+1
    elementData[size++] = e;
    return true;

    private void ensureCapacityInternal(int minCapacity) 
    	ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    

3.确定minCapacity,若使用的是无参构造,那么初始时容量为0,当第一次添加的时候,这里就会返回10。还有一种情况,就是有参构造,但是传值小于DEFAULT_CAPACITY,也会返回DEFAULT_CAPACITY,10。这样做的目的时防止当容量小时,添加元素会触发多次扩容。例如如果传入2,那么在添加了两个元素,就要扩容,然后此时2*1.5=3,添加第四个元素还需要扩容,会触发多次扩容,影响性能。

    private static int calculateCapacity(Object[] elementData, int minCapacity) 
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) 
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        
        return minCapacity;
    

4.确定修改次数,再查看当前所需要的minCapacity和数组长度的差,若需要扩容再扩容

    private void ensureExplicitCapacity(int minCapacity) 
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    
    private void grow(int minCapacity) 
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);//每次1.5倍
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;//第一次调用这个方法,因为oldCapacity为0
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:若超越了最大的容量进入这里
        //将前newCapacity个拷贝至elementData
        elementData = Arrays.copyOf(elementData, newCapacity);
    

有参构造

public ArrayList(int initialCapacity) 
    if (initialCapacity > 0) 
        this.elementData = new Object[initialCapacity];
     else if (initialCapacity == 0) 
        this.elementData = EMPTY_ELEMENTDATA;
     else 
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    

如何复制某个ArrayList到另一个ArrayList中去?

使用clone()方法

使用ArrayList构造方法

使用addAll方法

线程安全问题

当然不是线程安全的,线程安全版本的数组容器是Vector。 Vector的实现很简单,就是把所有的⽅法统统加上synchronized就完事了。 你也可以不使⽤Vector,⽤Collections.synchronizedList把⼀个普通ArrayList包装成⼀个线程安全版本 的数组容器也可以,原理同Vector是⼀样的,就是给所有的⽅法套上⼀层synchronized。

ArrayList插入或删除元素一定比LinkedList慢么

结果:

​ 在数据量大的情况下,因为ArrayList底层数组, LinkedList底层双向链表。ArrayList增删时,越靠前头部,增删效率越低,因为ArrayList增删的时候是需要拷贝数组的。而LinkedList当增删、查找效率都不是很高,特别是对象处于链表中部位置

​ 所以当插入删除元素在中间/或者随机查找的时候,数据量大的情况下,ArrayList可能会比LinkedList快。

public void add(int index, E element) 
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index;//把索引位置的元素后移一格
    elementData[index] = element;
    size++;

public E remove(int index) 
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);// 这里拷贝数据,即把待删除元素后面的元素覆盖到删除元素的位置
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;

ArrayList适合做队列吗

队列一般都是FIFO的,先进先出,如果用ArrayList,那么就需要在数组头部删除,尾部添加,反过来也行,但是都会有一个操作涉及数组拷贝,比较耗费性能

ArrayList的遍历和LinkedList遍历性能⽐较如何

论遍历ArrayList要⽐LinkedList快得多,ArrayList遍历最⼤的优势在于内存的连续性,CPU的内部缓存 结构会缓存连续的内存⽚段,可以⼤幅降低读取内存的性能开销

ArrayList常⽤的⽅法总结

  • boolean add(E e)

将指定的元素添加到此列表的尾部。

  • void add(int index, E element)

将指定的元素插⼊此列表中的指定位置

  • boolean addAll(Collection c)

按照指定 collection 的迭代器所返回的元素顺序,将该 collection 中的所有元素添加到此列表的尾部。

  • boolean addAll(int index, Collection c)

从指定的位置开始,将指定 collection 中的所有元素插⼊到此列表中。

  • void clear()

移除此列表中的所有元素。

  • Object clone()

返回此 ArrayList 实例的浅表副本。

  • boolean contains(Object o)

如果此列表中包含指定的元素,则返回 true。

  • void ensureCapacity(int minCapacity)

如有必要,增加此 ArrayList 实例的容量,以确保它⾄少能够容纳最⼩容量参数所指定的元素数。

  • E get(int index)

返回此列表中指定位置上的元素。

  • int indexOf(Object o)

返回此列表中⾸次出现的指定元素的索引,或如果此列表不包含元素,则返回 -1。

  • boolean isEmpty()

如果此列表中没有元素,则返回 true int lastIndexOf(Object o) 返回此列表中最后⼀次出现的指定元素的索引,或如果此列表不包含索引,则返回 -1。

  • E remove(int index)

移除此列表中指定位置上的元素。

  • boolean remove(Object o)

移除此列表中⾸次出现的指定元素(如果存在)。

  • protected void removeRange(int fromIndex, int toIndex)

移除列表中索引在 fromIndex(包括)和 toIndex(不包括)之间的所有元素。

  • E set(int index, E element)

⽤指定的元素替代此列表中指定位置上的元素。 int size() 返回此列表中的元素数。

  • Object[] toArray()

按适当顺序(从第⼀个到最后⼀个元素)返回包含此列表中所有元素的数组。

  • T[] toArray(T[] a)

按适当顺序(从第⼀个到最后⼀个元素)返回包含此列表中所有元素的数组;返回数组的运⾏时类型是 指定数组的运⾏时类型。

  • void trimToSize()

将此 ArrayList 实例的容量调整为列表的当前⼤⼩。

以上是关于常见ArrayLIst面试题的主要内容,如果未能解决你的问题,请参考以下文章

常见ArrayLIst面试题

Java面试题:ArrayList和LinkedList的区别

阿里面试常见题:ArrayListLinkedList和CopyOnWriteArrayList

阿里面试常见题:ArrayListLinkedList和CopyOnWriteArrayList

直通BATBAT后端开发36题:RedisNosqlMysql秒杀题目!含面试经验总结!

Java基础面试题-第二集