面试&笔试|ArrayList,征服你(附常见面试总结)

面试&笔试|ArrayList,征服你(常见面试总结)

ArrayList简介(以下没有特别说明,分析都是基于jdk1.8)

        ArrayList是java集合(容器)中的一员,其本身的数据结构是数组,其基于数组则最具特色的就是寻址快,支持随机访问,删改操作较链表结构的容器慢(一般情况下)。ArrayList 的初始容量是 10;加载因子(元素个数达到本身容量的一半就扩容)为 0.5; 扩容增量:0.5倍;第一次扩容后长度为 15。(后续的扩充会有.5的情况,直接舍去取整,jdk1.7以上的版本直接原容量*1.5,jdk1.6版本是1.5倍+1)

ArrayList继承与实现 (若使用idea开发工具,可以双击shift键搜索ArrayList查看源码)

        从ArrayList源码中可以看到:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

        ArrayList首先声明了E类型的泛型,其继承了AbstractList抽象类,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。

        ArrayList 实现了RandomAccess接口, RandomAccess 是一个标志接口,表明支持快速随机访问的。

        ArrayList 实现了Cloneable 接口,即覆盖了函数 clone(),能被克隆。

        ArrayList 实现Serializable 接口,这意味着ArrayList支持序列化,可以远程传输

        源码中定义的成员变量:

   /**
     * 默认容量大小
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 空数组(用于空实例)。
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

     //用于默认大小空实例的共享空数组实例,把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 保存数据的数组
     */
    transient Object[] elementData; 

    /**
     * ArrayList的元素大小
     */
    private int size;

ArrayList扩容

    /**
     * 数组的添加元素方法
     * @param e 元素
     * @return 返回添加成功的标志
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);
        elementData[size++] = e;
        return true;
    }

    /**
     * 确认数组的最大扩容量
     * @param minCapacity 扩容大小
     */
    private void ensureCapacityInternal(int minCapacity) {
        //若相等则证明是第一次添加
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //与默认值10容量大小相比,取最大值,确保扩容后的容量能装下新增的数据
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }
   /**
     * 确定数组的容量大小之后,需要判断当前元素个数和扩容容量的大小
     * @param minCapacity
     */
    private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 若大于,则要扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
    }
   /**
     * 扩容的核心方法,扩容大小为1.5
     * @param minCapacity
     */
    private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    //使用位运算计算容量,较oldCapacity/2更高效
    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);
    }

        ArrayList核心扩容可以看出最终是Arrays.copyOf()方法,而Arrays.copyOf()方法最终是调用 System.arraycopy()方法,而 System.arraycopy()是由native(被该关键字修饰,表示调用的是C语言编写的方法,此方法可以理解成为java对C的api)修饰,System.arraycopy()和Arrays.copyOf()方法的区别:

(1)copyOf()在内部新建一个数组,并返回该数组

(2)arraycopy()需要目标数组,将原数组拷贝到自定义的数组里,需要选择拷贝的起点和长度

ArrayList添加、删除、修改元素(以删除源码分析为例)

        ArrayList的容量大小每发生一次变动(添删改)都需要重新复制新的数组,重新调整容量大小(关于容量大小,在创建ArrayList之初若未指定大小,会先赋值为空数组,只有在添加第一个元素的时候会扩容为默认容量大小10),从这点上,也是本文开头说的较链表慢的原因,链表只需要更改引用即可,而数组结构的容器需要重新调整容量大小,以下ArrayList删除的源码就是验证了这一点

   /**
     * 删除元素
     * @param index 被删除元素的下标
     * @return 返回被删除元素的值
     */
    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);
        // 置为null让GC容易回收该引用
        elementData[--size] = null; 
        return oldValue;
    }

ArrayList序列化(以writeObject() 和 readObject()为例)

        ArrayList 基于数组实现,通过其加载因子0.5可以看出,他总会预留一半的空间出来,以备扩容,那么在序列化的过程中,这一半的空间我们或许根本不需要去序列化他。

保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化

transient Object[] elementData; 

        ArrayList 在序列化的时候会使用writeObject()方法,将size和element长度大小的元素写入ObjectOutputStream;反序列化时调用readObject(),从ObjectInputStream获取size和element长度大小元素,再恢复到elementData,从源码可知,序列化的长度是size而不是element.length

   /**
     * 序列化方法
     */
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;

        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in capacity
        s.readInt(); // ignored
		//此处的size为实际元素大小
        if (size > 0) {
            // be like clone(), allocate array based upon size not capacity
            int capacity = calculateCapacity(elementData, size);
            SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
            ensureCapacityInternal(size);

            Object[] a = elementData;
            //读取实际元素.
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }

ArrayList的fail-fast机制

        ArrayList在每次的删除,修改,添加过程中都会有一个全局的变量来自增,这个变量就是modCount,代表元素的修改次数,这个全局变量定义在AbstractList类里面,在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。

protected transient int modCount = 0;

面试常问:

1、List、Vector 、Set 和 Map 的初始容量和加载因子?

答:
        1. List
        ArrayList 的初始容量是 10;加载因子为 0.5; 扩容增量:0.5倍;一次扩容后长度为 15。(后续的扩充会有.5的情况,直接舍去取整jdk1.7以上的版本直接原容量*1.5,jdk1.6版本是0.5倍+1)
        Vector 初始容量为 10,加载因子是 1。扩容增量:原容量的 1 倍,如 Vector 的容量为 10,一次扩容后是容量为 20。

        2. Set

        HashSet,初始容量为 16,加载因子为 0.75; 扩容增量:原容量的 1 倍; 如 HashSet 的容量为 16,一次扩容后容量为 32

        3. Map
         HashMap,初始容量 16,加载因子为 0.75; 扩容增量:原容量的 1 倍; 如 HashMap 的容量为 16,一次扩容后容量为 32

2、ArrayList list = new ArrayList(20); 中的list扩充几次?
答:0次,初始值指定,则默认使用初始值作为扩容的容量,与初始容量10没有关系

3、ArrayList list和LinkedList相比,区别是?
答:一个数组,一个链表,数组寻址快,链表添删改快(一般情况下,若添删改在数组的末尾,正常来说数组耗时与链表没多大区别,在数组的头部和中部添加需要移动数组)

4、ArrayList是否线程安全?
答:非线程安全

5、System.arraycopy()和Arrays.copyOf()方法的区别?
答:(1)copyOf()在内部新建一个数组,并返回该数组
(2)arraycopy()需要目标数组,将原数组拷贝到自定义的数组里,需要选择拷贝的起点和长度

6、ArrayList是否支持序列化?序列化过程?
答:支持,只序列化实际元素,使用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。而 writeObject() 方法在传入的对象存在 writeObject() 的时候会去反射调用该对象的 writeObject() 来实现序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法

7、ArrayList继承那些类,实现什么接口?
答:AbstractList、RandomAccess、Cloneable 、Serializable

8、ArrayList遍历有哪些?
答:基本的for循环遍历,Iterator迭代器遍历

9、ArrayList创建的时候,若知道容器大小,为什么推荐给定初始化容量?(阿里巴巴规范)
:减少扩容的次数,提高性能

10、说说List,Set,Map三者的区别?
答: (1)List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象
(2)Set不允许重复的集合。无序
(3)Map键值对,不允许key重复,value可以存储任何引用类型

11、Arraylist 与 LinkedList 区别?
答:(1)底层数据结构不同,ArrayList使用的是 Object 数组;LinkedList 底层使用的是 双向链表 数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别)
(2)Arraylist支持高效的随机元素访问,而LinkedList 不 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)
(3) ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作,LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)

12、关于ArrayList实现的RandomAccess接口,是否说明实现了RandomAccess接口,就是有了随机访问的能力?为什么?
答:通过阅读ArrayList源码可以知道,RandomAccess接口是空的,什么都么有定义,说明RandomAccess接口只是简单的标识作用,并不是标识了RandomAccess接口就有随机访问的能力,而是数组本身就支持,在Collections.binarySearch方法中传入的集合就会判断是否是实现RandomAccess接口的,以此调用不同的方法:

public static <T>
    int binarySearch(List<? extends Comparable<? super T>> list, T key) {
        //判断是否实现了RandomAccess接口
        if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
            return Collections.indexedBinarySearch(list, key);
        else
            return Collections.iteratorBinarySearch(list, key);
    }

13、ArrayList 与 Vector 区别呢?
答:ArrayList 线程不安全,Vector线程安全,ArrayList 的初始容量是 10;加载因子为 0.5; 扩容增量:0.5倍;一次扩容后长度为 15。 Vector 初始容量为 10,加载因子是 1。扩容增量:原容量的 1 倍,如 Vector 的容量为 10,一次扩容后是容量为 20。

14、ArrayList中有Fail-Fast嘛?说说Fail-Fast的原理?
:我们通常说的Java中的fail-fast机制,默认指的是Java集合的一种错误检测机制。当多个线程或者直接在原容器中修改时,就会触发这一保护机制,该机制的原理是有两个变量触发,modCount和expectedModCount变量,初始值为0,当做修改(添删)操作,modCount会自增,表明已经在修改,此时若再有线程对其修改,则会判断是否和expectedModCount是否相等,不相等则抛出ConcurrentModificationException异常,可以使用CopyOnWriteArrayList避免,具体请看:

一不小心就让Java开发者踩坑的fail-fast是个什么鬼?

15、手写个ArrayList呗?
:呵呵

ps:关于集合的面试资料,我整理成立脑图,有兴趣可以见评论区(密码)下载观看,有问题欢迎指正:超全集合复习脑图链接

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值