ArrayList(源码分析)—面试经典问题

目录

前言

ArrayList底层的数据结构是什么?

ArrayList扩容机制是什么?

1、长度变化

2、复制CopyOf()

ArrayList删除机制

ArrayList新增元素

ArrayList查询逻辑

ArrayList增删效率和查询效率相较于LinkedList比怎么样?

ArrayList线程安全吗?


前言

        在学习初期和刷面试题的时候,都是死记硬背,没有看过源码,,这些问题基本上都是从源码角度出发的,这次从源码角度去分析一下这些问题的答案。

ArrayList底层的数据结构是什么?

        是一个Object[] elementData 数组,看下源码

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

transient Object[] elementData;

//传入初始化长度
    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);
        }
    }

//无参构造,直接使用默认值,空
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

//传入一个集合,这里就直接调用copyof方法进行拷贝,这个方法底层是System.arraycopy
//在下面的add、扩容时候讲
    public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();
        if ((size = a.length) != 0) {
            if (c.getClass() == ArrayList.class) {
                elementData = a;
            } else {
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            // replace with empty array.
            elementData = EMPTY_ELEMENTDATA;
        }
    }

        这里要注意一个点,初始化的时候,使用无参构造,创建的数组是空数组,没有长度,是在第一次放入元素,才会进行第一次扩容,到底扩多少,继续看源码。

ArrayList扩容机制是什么?

1、长度变化

        在数组为空时,第一次放入元素,会先扩容10个长度,后面加入元素如果超过10个,则进行1.5倍扩容,此时的1.5倍不是直接乘1.5倍,而是(原长度+原长度>>1),像这样做了一个位运算,这是jdk1.8后做的一个运算优化,看下源码:

    public boolean add(E e) {
        modCount++;
//无参构造后,elementData和size都是为空的
        add(e, elementData, size);
        return true;
    }

    private void add(E e, Object[] elementData, int s) {
//无参构造后,这个if校验是相等的,都是0,随后调用了grow()方法
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }

    private Object[] grow() {
        return grow(size + 1);
    }

    private Object[] grow(int minCapacity) {
//调用了扩容的主要方法newCapacity(),然后配合copyof完成elementData的扩容
        return elementData = Arrays.copyOf(elementData,
                             newCapacity(minCapacity));
    }

        这里可以看到主要的扩容方法就是newCapacity(),进去看源码:


    private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
//这里就是上面所说的(原长度+原长度>>1),相当于(原长度+原长度/2)
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity <= 0) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
//这种情况的出现就表明了,创建list对象时是使用的无参构造或者,有参构造但是传入初始长度为0
//这里的DEFAULT_CAPACITY就是10
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
//这里表示新的长度小于MAX_ARRAY_SIZE,则使用新长度,否则,使用Integer.MAX_VALUE
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
    }

//这里MAX_ARRAY_SIZE比Integer.MAX_VALUE小8,为int的的取值范围留出了空余,防止越界
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE)
            ? Integer.MAX_VALUE
            : MAX_ARRAY_SIZE;
    }

2、复制CopyOf()

        这里ArrayList.copyof()方法中调用了System.arraycopy()方法,此方法上标注的@HotSpotIntrinsicCandidate注解,表示对此方法进行手写,可以跳过JNI,直接在JVM中运行,所以这个深拷贝是比较快的。

    public static <T> T[] copyOf(T[] original, int newLength) {
        return (T[]) copyOf(original, newLength, original.getClass());
    }

    @HotSpotIntrinsicCandidate
    public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

    @HotSpotIntrinsicCandidate
    public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

         下面是我在网络上找到的解释,看着比较好理解。

        可以看到这是一个native(本地)方法,也就是说是用C++写的,所以其效率比非native方法更高。在JVM里对@HotSpotIntrinsicCandidate注解的进行了手写,这里要提一个叫JNI(Java Native Interface)的东西,普通的native方法通俗的讲就是编译后还要通过JNI再次编译成.cpp文件才能执行.而有 @HotSpotIntrinsicCandidate这个注解的方法在JVM里就是用.cpp文件写好的,所以就跳过了JNI阶段,所以速度就能提升,这也是System.arraycopy()速度快的原因。

ArrayList删除机制

        ArrayList在删除时都是先找出这个元素,然后把这个元素后面的全部元素向前移动,随后为最后一位赋值为空,看一下图解,就很好理解:

        再看一下源码:

    public E remove(int index) {
//这就是用来防止数组越界的校验
        Objects.checkIndex(index, size);
        final Object[] es = elementData;

        @SuppressWarnings("unchecked") E oldValue = (E) es[index];
//调用FastRemove方法
        fastRemove(es, index);

        return oldValue;
    }

    private void fastRemove(Object[] es, int i) {
        modCount++;
        final int newSize;
        if ((newSize = size - 1) > i)
//这个操作可以理解为,将es数组中的i+1向后位置的元素,往前移一位;
//实际上是进行了一个深拷贝,重新整体放置了一下元素
//这时数组的长度不会改变,以前是10,删除一个元素,现在有9个元素,但是长度还是10
            System.arraycopy(es, i + 1, es, i, newSize - i);
        es[size = newSize] = null;
    }

        这里想强调一下,修改元素set方法,不是先remove后add,是直接找到这个index,然后进行赋值,简单粗暴哈,所以set方法不会造成大量的资源浪费,不要和remove混为一谈。 

ArrayList新增元素

        ArrayList如果在数组中有数据的情况下,在某index处新增元素,那么,这个怎么操作的,带入删除元素的想法来模拟一下,那就是先把某index处以及往后的元素向后移动一位,再把index处赋上新值,那么实际是怎么样的,看一下源码:

    public void add(int index, E element) {
//防止下标越界校验
        rangeCheckForAdd(index);
        modCount++;
        final int s;
        Object[] elementData;
//如果长度已经满了,那就扩容
        if ((s = size) == (elementData = this.elementData).length)
            elementData = grow();
//然后把index以及往后的元素向后移动一位,实际上也是深拷贝,重新放置元素,把index处置为null
        System.arraycopy(elementData, index,
                         elementData, index + 1,
                         s - index);
        elementData[index] = element;
//长度加一
        size = s + 1;
    }

ArrayList查询逻辑

        底层为数组,就直接取索引即可,所以查询很快,看一下源码:

    public E get(int index) {
//防止下标越界
        Objects.checkIndex(index, size);
//直接取index处的值
        return elementData(index);
    }

ArrayList增删效率和查询效率相较于LinkedList比怎么样?

        在上面两部分,可以看出ArrayList增删都会进行深拷贝数组,比较耗费资源,这也从根本上说明了,它不适合作为队列来使用,因为太耗费资源了;那么LinkedList

是怎么样操作的?看一下LinkedList源码

//可以看出LinkedList都是Node节点操作
    void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
//看到这里只需要浪两步操作,即可添加
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

        所以,看到源码,LinkedList的底层是Node节点构成的双向链表,它的增删只需要两步,1、找到元素,2、调整节点,即可。

        但是链表的查询就很慢了,不管是哪个数,链表都要从头遍历,很慢,不如数组。

        这里就印证了大家都说的,ArrayList查询快,LinkedList增删快

ArrayList线程安全吗?

        No,在add,remove时都没有加锁,如果在操作ArrayList时候,尽量防止对列表进行增删操作和修改操作。

        想要线程安全则使用Vector,Collections.synchronizedList(),CopyOnWriteArrayList原理都是给方法套个synchronized。

        Ok了,这就是对常见的ArrayList面试题的源码解析,耐心看完应该对ArrayList没啥太大问题,至于深拷贝在JVM中怎么操作的,怎么就那么快,这个问题暂时还挖掘不了那么深的层次,记住就好了。

最后我还整理汇总了⼀些 Java ⾯试相关的⾼质量 PDF 资料和免费Idea账号

公众号:Java小白,回复“⾯试” 和“idea破解”即可获取!

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

派大星的无情铁锤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值