集合源码解析之ArrayList

集合源码解析之ArrayList

        ArrayList是开发中最常使用到的集合,要想深入了解绕不过源码,本篇文章简单讲解其源码,领略ArrayList的风采...
                                                                       ps: 本文以openjdk8为主.

概述

  1. ArrayList是一种可以动态操作的集合类,基于数组实现.
  2. ArrayList允许空值和重复元素,该类封装了一个动态再分配的Obj数组.
  3. 当往ArrayList里添加元素数量大于其底层数组容量时,会通过扩容机制重新生成一个更大的数组.
  4. ArrayList是非线程安全的,多线程操作下需要手动保证该集合类的同步性

源码分析

继承结构

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

ArrayList实现了List<E>``RandomAccess``Cloneable``java.io.Serializable接口,继承了AbstractList类.其中RandomAccess代表了它具备快速随机访问的能力.由于它是基于数组实现,天生带下标,可以实现O(1)复杂度的随机访问.

在这里插入图片描述

为什么要让AbstractList先实现List,然后在让ArrayList继承AbstractList?为什么不让ArrayList直接实现List?

ArrayList继承AbstractList是一种思想, 现在AbstractList里实现一些通用的方法,而具体的实现类ArrayList继承AbstractList,拿到通用的方法,再实现一些自己特定的方法.这样就算后面有多个List实现,都可以通过继承AbstractList来获取通用方法,减少代码的重复.

既然ArrayList已经继承了AbstractList,而AbstractList已经实现了List,那么为什么ArrayList还要再实现List?
在这里插入图片描述

有道翻译:
这是记住此类真正实现该接口的一种方法。
它不会有任何不良影响,并且可以帮助您理解代码,而无需遍历给定类的完整层次结构。

另一种可能:

    public static void main(String[] args) {
        System.out.println("Test1: " + Arrays.toString(Test1.class.getInterfaces()));
        System.out.println("Test2: " + Arrays.toString(Test2.class.getInterfaces()));
    }

    interface TestInterface { void test(); }
    abstract class AbstractTestInterface implements TestInterface {}
    class Test1 extends AbstractTestInterface {
        @Override
        public void test() { System.out.println("test1"); }
    }

    class Test2 extends AbstractTestInterface implements TestInterface {
        @Override
        public void test() {System.out.println("test2"); }
    }

输出结果:
在这里插入图片描述

从结果就可以发现,因为Test1本身没有再次实现接口,因此在获取Test1的接口时为空数组.
所以在实现代理时就会报错,如下:

    public static void main(String[] args) {
        new Test().testProxy();
    }

    private void testProxy() {
        try {
            TestInterface proxy1 = createProxy(new Test1());
            proxy1.test();
        } catch (Exception e) {
            e.printStackTrace();
        }
        TestInterface proxy2 = createProxy(new Test2());
        proxy2.test();
    }

    private static <T> T createProxy(final T obj) {
        return (T) Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj
                .getClass().getInterfaces(), (proxy, method, args) -> method.invoke(obj, args));
    }

在生成动态代理时调用class.getInterfaces(), 不实现接口调用函数会报错.
在这里插入图片描述

可能作者是为了更方便实现代理而精心设计的.

    ps: Set和Map集合同理...

类中属性

属性比较少,直接看源码

    /** 版本号,用于校验正反序列化时的一致性 */
    private static final long serialVersionUID = 8683452581122892189L;

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

    /** 空对象数组 */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /** 默认空对象数组 */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /** ArrayList的核心,数据都是存在这的,注意这个数组自定义了序列化函数 */
    transient Object[] elementData;

    /** 实际元素数量 */
    private int size;

    /** 数组的最大容量
     * 尝试分配更大的数组可能会导致OutOfMemoryError:请求的数组大小超过VM限制
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    
    /** AbstractList里的属性, 记录集合 涉及到结构变化的次数(add/remove/clear等) */
    protected transient int modCount = 0;

构造函数

ArrayList有三个构造函数:

  • 无参构造器
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

将elementData赋值为默认空数组, 实际上jdk8里的ArrayList进行了延迟初始化==(具体初始化在后面解释)==,而jdk8之前的版本是直接进行初始化

    // by openjdk7
    public ArrayList() {
    // 调用有参构造器进行初始化默认容量
        this(10);
    }
  • 有参构造器之一
    没什么逻辑,直接上源码
    public ArrayList(int initialCapacity) {
        // 判断下这个初始容量是否大于0
        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(Collection<? extends E> c) {
        // c.toArray可能返回的不是Object对象  see 6260652
        elementData = c.toArray();

        if ((size = elementData.length) != 0) {
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

这里涉及到JDK bug库6260652编号,附上官网链接,看下述例子:

    class A {}
    class B extends A {}
    public void Test() {
        B[] b = {new B(), new B()};
        // 子类数组转换成父类数组是允许,向上转型
        A[] a = b;
        System.out.println(a[0]);
        // 由于存储的都是子类型B,java是不允许向下转型的,因此这里会报错ArrayStoreException
        a[0] = new A();
    }

这里说明假如我们有个Obj数组,并不代表着我们可以把Obj对象存进去,这取决于数组中存储元素的实际类型.
回到源码c.toArray()可能会发生类似于上述例子的A[] a = b,为了防止这种情况,源码中进行了if判断来防止错误的数组对象类型导致异常,Arrays.copyOf(elementData, size, Object[].class)重新创建了个Obj数组,可以任意存储对象.

核心函数

添加函数
    // 末尾添加一个元素
    public boolean add(E e);
    
    // 指定下标位置添加一个元素
    public void add(int index, E element);
boolean add(E e)
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // size++ 真实数据量+1
        elementData[size++] = e;
        return true;
    }

ensureCapacityInternal是检查扩容函数,size + 1添加前先检查下数组中是否能放下.

    /**
     * 检测扩容时调用的中间方法
     *
     * @param minCapacity
     */
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }
  1. 先确认最小容量:elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA即判断初始化的elementData是不是空数组,然后找出默认容量和参数容量大的.对应无参构造器
    延迟初始化
  2. 调用ensureExplicitCapacity函数,此函数才是判断容量是否够用的函数,如果不够用才会调用grow来进行扩容
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // overflow conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

ensureExplicitCapacity里只有检测到真实容量不够的时候才会扩容, 而grow才是真实扩容的函数,而modCount属性作用比较泛,例如fast fail检查,后续会讲到.

    private void grow(int minCapacity) {
        // 复制扩容前的容量
        int oldCapacity = elementData.length;

        // 生成新的大小  每次扩容为原有大小的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);

        // 这里适用于elementData为空数组的时候,即数组初始化时
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;

        // 当newCapacity超过最大限制,调用hugeCapacity把最大值赋予当newCapacity超过最大限制
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);

        // 开始进行 扩容
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    
        private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        // 超过最大限,实际上还是把int最大值给了,再大就溢出了,但给予MAX_VALUE,vm就超过限制了..
        return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
    }
void add(int index, E element)
    public void add(int index, E element) {
        rangeCheckForAdd(index); // check index
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                size - index);
        elementData[index] = element;
        size++;
    }
    
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

在指定位置插入时,第一步先调用rangeCheckForAdd来进行越界检查,触发扩容机制ensureCapacityInternal.
再调用System.arraycopyindex后所有元素往后挪一位.ArrayList随机插入的效率慢的原因之一

注意: 当调用无参构造器进行构建ArrayList时,容量会在第一次调用add函数时进行初始化.
获取函数

较为简单,直接上源码

    public E get(int index) {
        // 检测是否下标越界
        rangeCheck(index);

        // 获取元素
        return elementData(index);
    }

    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    
    E elementData(int index) { 
        // 内部使用时以Obj, 获取时进行向下转型 Object -> element
        return (E) elementData[index]; 
    }
修改函数

较为更加简单

    public E set(int index, E element) {
        rangeCheck(index);
        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }
删除函数
    // 根据下标移除元素
    E remove(int index);
    
    // 删除列表指定的元素
    boolean remove(Object o); 
    
    // 清空list
    void clear();
E remove(int index)
    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);

        // 同时清除最后一个多余的,方便gc
        elementData[--size] = null; // clear to let GC do its work

        // 返回旧值
        return oldValue;
    }

remove(int index)整体逻辑就是移动元素,把index下面的往前移动一位,再清理最后一位多余的,把被删除的元素返回.

boolean remove(Object o)
    public boolean remove(Object o) {
        if (o == null) {
            // null值的删除依赖 ==
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            // 非null值的删除依赖 equals
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
    
        private void fastRemove(int index) {
        modCount++;
        // 计算要移动元素位数
        int numMoved = size - index - 1;
        if (numMoved > 0)
            // 移动元素
            System.arraycopy(elementData, index + 1, elementData, index,
                    numMoved);

        // 删除最后一个多余的,方便gc
        elementData[--size] = null; // clear to let GC do its work
    }

remove(Object o)逻辑和remove(int index)差不多,相对多了个步骤寻找下标,if (elementData[index] == null)这里可以看出,ArrayList是支持存入null值的.

void clear();
    public void clear() {
        modCount++;
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

把所有元素置为null方便gc,再重置实际元素数量.

Iterator迭代器
    private class Itr implements Iterator<E> {
        int cursor;       // 要返回的下一个元素的下标
        int lastRet = -1; // 上一次返回的元素 (删除的标志位)
        int expectedModCount = modCount; //用于判断集合是否修改过结构的标志
        
        boolean hasNext();
        
        E next();

        void remove();

        void forEachRemaining(Consumer<? super E> consumer);
    }

我们都知道Java中的foreach是个语法糖,编译成字节码后会被转成用迭代器遍历的方式.
来看看Itr,它是ArrayList内部维护的迭代器.

    for (String s : list) {
        System.out.println(s);
    }
    // 等价于
    Iterator var2 = list.iterator();
    while(var2.hasNext()) {
        String s = (String)var2.next();
        System.out.println(s);
    }

来看看hasNextnext函数

    public boolean hasNext() {
        // 判断是否到结尾
        return cursor != size;
    }

    public E next() {
        checkForComodification();
        int i = cursor;
        if (i >= size) // 判断是否越界
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length) // 再次判断越界,防止异步线程修改List
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i]; // 返回元素,并设置上一次返回的元素的下标
    }
    
    final void checkForComodification() {
        // fast-fail检测, 如若在迭代的同时进行
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }

next首先会触发checkForComodification,来检测在遍历的过程中是否其他线程修改了list并抛出异常,这就是fast-fail快速失败策略
通过判断modCount和expectedModCount是否一样来判断是否有进行修改.

因此在foreach时不能对元素进行add/remove操作,remove元素请使用Iterator方式.并发操作,需要对Iterator对象加锁.

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet); // 删除 modCount会发生变化
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount; // 需同步变化
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
快速失败机制

ArrayList 迭代器中的方法都是均具有快速失败的特性,当遇到并发修改的情况时,迭代器会快速失败,以避免程序在将来不确定的时间里出现不确定的行为。

总结

  • ArrayList本质上就是一个Object[]数组
  • ArrayList允许插入null,每次扩容1.5倍
  • ArrayList是通过System.arrayCopyArrays.copyOf来完成大部分操作的
  • ArrayList使用了fast-fail机制,iterator迭代时,如若add/remove时,每次迭代都会检测modCount是否有改变抛异常
  • ArrayList由于本质上是数组,所以它在数据查询方便会很快,在随机插入/删除性能会下降很多,因为每次操作都要移动很多元素
  • ArrayList是非线程安全的,很多操作非原子性的,例如elementData[size++] = e
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值