单走一个ArrayList源码分析


话不多说,先上图:

我们在源码中可以看到整个ArrayList的继承与实现。可以看到它是实现了List接口,是基于数组实现的。那我们都知道数组要在创建的时候就确定长度,那这样势必会影响程序的性能和资源分配,那么作为近乎最常用的Java集合类–ArrayList又是怎么解决这些问题的呢?

下面让我们走进它的源码一探究竟,按照源码的顺序做一番思考~

壹·相关变量

  /**
     * 默认初始化容量,即不定义数组的长度时默认长度为10
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 用于空实例的共享数组实例,容量为0的时候给数组变量赋值
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 用于默认大小的空实例的共享空数组实例. 我们将其与EMPTY_ELEMENTDATA区分开来,以便知道添加
     * 第一个元素时要膨胀多少
     * 容量为默认的时候给数组变量赋值
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
   /**
    * transient关键字表示变量不能自动序列化,用来关闭变量的serialization(持久化)机制
    * ArrayList中的元素都储存在elementData数组中
    */
    transient Object[] elementData; 
   /**
     * 数组列表的大小
     *
     * @serial
     */
    private int size;

正因为elementData是Object类型的数组,所以添加元素的时候都会向上转型为object,这才成就了ArrayList动态数组的特性。

在JDK1.7的时候,当elementData为空时会赋给一个空数组,但如果程序中含有很多个空的ArrayList便会造成资源的浪费,因此在JDK1.8中引入了两个空数组,static表示被类中所有对象共享,都会指向这两个数组,final锁定数组地址不变,并且被所有的对象共享,因此就解决了1.7的问题。

那么这两个空数组是如何操作的呢?我们接着往后面看

贰·相关构造方法

  /**
     * 使用指定的初始容量构造一个空列表,如果使用带容量大小的构造函数,当参数大于0的时候,则会创建
     * 一个和参数大小相同的Object数组,若等于0,则赋值为空数组,若小于0,抛出异常
     */
    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);
        }
    }

    /**
     * 构造一个初始容量为10的空列表,即当定义为无参的时候默认数组长度为10
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 按照集合迭代器返回的顺序构造一个包含指定集合元素的列表。
     *toArray将集合转化为数组然后赋值给存储元素的数组
     * @param c 要将其元素放入此列表中的集合
     * @throws NullPointerException 如果指定的集合为null,则抛出NullPointerException
     */
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray 可能错误的不返回 Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 替换为空数组
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

叁·CURD实现原理

增加元素

    /**
     * 将指定的元素添加到列表的末尾。
     *
     * @param e 元素添加到此列表
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
    //调用ensureCapacityInternal方法对数组长度进行检查,不足的话则进行扩容
        ensureCapacityInternal(size + 1);  // 增量modCount!!
        //对数组中第N+1个元素赋值
        elementData[size++] = e;
        return true;
    }

增加元素的过程中涉及到了ArrayList的扩容机制,那这个机制是如何进行的呢?


   //3.判断最小容量和当前数组实际长度的大小,若最小容量大于实际长度,则调用grow方法进行扩容
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
  /**2.判断elementData是否是ArrayList属性中定义的其中一个空数组
   *DEFAULTCAPACITY_EMPTY_ELEMENTDATA
   *若是则以默认为10的容量和赋值的容量的最大值作为数组的最小容量,否则以赋值的容量为最小容量
   */

   private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
  //1.先是调用了此方法对数组长度进行判断
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    /**
     * 要分配的数组的最大大小。有些虚拟机在数组中保留一些头词。尝试分配更大的数组可能会导致
     * OutOfMemoryError:请求的数组大小超过VM限制
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    
    /**
     *4.增加容量,以确保至少可以保存由最小容量参数指定的元素数量
     * @param minCapacity要求的最小容量
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //位运算右移一位表示除以2,则扩容为原来的1.5倍
        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);
    }

    //5.如果最小容量大于定义的最大大小,则以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;
    }

通过以上五个步骤,我们就拿到了扩容以后的新数组,之后便是将原本数组中的元素拷贝进新数组了,从grow方法中我们可以看到是调用的Arrays.copyOf方法

//1.
    public static <T> T[] copyOf(T[] original, int newLength) {
        return (T[]) copyOf(original, newLength, original.getClass());
    }
//2.
    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] //以新长度创建一个新的object数组,名为copy
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }
/**3.实现数组复制,其中各个参数含义为:src:原数组;srcPos:源数组要复制的起始位置; 
 *dest:目的数组; destPos:目的数组放置的起始位置; length:复制的长度
 */
   public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

至此,ArrayList最主要的特性,扩容机制就结束了,但笔者之前提出来的问题你有没有答案了呢?就是这两个空数组是到底如何操作呢?

这个问题在源码中已经有了答案,当ArrayList使用无参构造器的时候,空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA会赋值给elementData,当使用有参构造器时,便会赋值给其EMPTY_ELEMENTDATA。这两个空数组的区别便在于第一次赋值的时候,如果小于10作为最小容量的会是谁,并以此决定了扩容方式。

获取元素

    /**
     * 返回列表中指定位置的元素。
     *
     * @param  index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
   public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

    /**
     * 检查给定的索引是否在范围内。如果不是,则抛出适当的运行时异常。这个方法不会检查索引是否为
     * 负数:它总是在数组访问之前立即使用,如果index为负数,将抛出ArrayIndexOutOfBoundsException
     * 异常。
     */
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    /**
     * 构造一个IndexOutOfBoundsException详细消息。在错误处理代码的许多可能重构中,这种“概述”在服务器和客户端vm上都表现得最好。
     */
    private String outOfBoundsMsg(int index) {
        return "Index: "+index+", Size: "+size;
    }

这部分源码不是很难,看着自带的注释基本就可看懂,这里就不做解释了。

删除元素

    /**
     * 删除列表中指定位置的元素。
     * 将所有后续元素向左移动(从它们的下标减去1)。
     *
     * @param index 要删除的元素的索引
     * @return 从列表中删除的元素
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    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; // 可以让GC完成它的工作

        return oldValue;
    }

    /**
     * 检查给定的索引是否在范围内。如果不是,则抛出适当的运行时异常。这个方法不会检查索引是否为
     * 负数:它总是在数组访问之前立即使用,如果index为负数,将抛出ArrayIndexOutOfBoundsException
     * 异常。
     */
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

根据对象找到指定元素并删除:

  /**
     * 从列表中删除第一个出现的指定元素(如果它存在的话)。如果列表中不包含该元素,则保持不变。更正式的做法是删除索引最低的元素
     * <tt>i</tt> such that
     * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
     * (如果存在这样的元素)。如果列表中包含了指定的元素,则返回true(或者等效地,如果列表因调用而改变)。
     *
     * @param o element to be removed from this list, if present
     * @return <tt>true</tt> if this list contained the specified element
     */
    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

    /*
     * Private remove方法,跳过边界检查,不返回已删除的值
     */
    private void fastRemove(int index) {
        modCount++;
        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
    }

肆·fail-fast(快速失败机制)

我们在ArrayList的crud操作中都会看到一个modCount变量,每次执行crud的时候这个变量都会发生改变,那么这个变量到底有什么用呢?

ArrayList的快速失败机制,是一种失败检测机制,当迭代集合的过程中,该集合却被修改了一次之后,就有可能会发生fail-fast,即抛出异常。

异常又是如何被抛出的呢?

我们知道,对于集合如list,map类,我们都可以通过迭代器来遍历,而Iterator只是一个接口,具体的实现还是要看具体的集合类中的内部类去实现Iterator并实现相关方法。


    public Iterator<E> iterator() {
        return new Itr();
    }

我们看到这会返回一个Itr类,而Itr类是ArrayList的内部类

  /**
     * AbstractList.Itr的优化版本
     */
    private class Itr implements Iterator<E> {
        int cursor;       // 要返回的下一个元素的索引
        int lastRet = -1; // 返回的最后一个元素的索引;如果没有则返回-1
        
        /**modCount是抽象类AbstractList中的变量,默认为0,而ArrayList 继承了AbstractList ,
         *所以也有这个变量,modCount用于记录集合操作过程中作的修改次数,与size还是有区别的,
         *并不一定等于size
         */
        int expectedModCount = modCount;
 
        public boolean hasNext() {
            return cursor != size;
        }
 
        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
 
        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
 
            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
 
        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }
 
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

可以看到next方法中会先调用checkForComodification方法

        final void checkForComodification() {
        /**在一开始,expectedModCount初始值默认等于modCount,在前面ArrayList扩容机制的分析中,
         *我们知道在ArrayList进行add,remove,clear等涉及到修改集合中的元素个数的操作时,
         *modCount就会发生改变(modCount ++),所以当另一个线程(并发修改)或者同一个线程遍历过程中,
         *调用相关方法使集合的个数发生改变,就会使modCount发生变化,所以如果不等的时候,表示着出现
         *了另一个线程并发执行了,因此抛出异常
         */
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

了解了快速失败机制之后,我们如何去避免它呢?

?在单线程中,我们调用remove方法的时候可以换成迭代器的remove方法而不是集合类的remove方法。


        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
 
            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;//在这里会更新modCount,保证二者的一致性
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

这种方式有一定的局限性,那还有没有更好的解决方式了呢?

?采用JUC中的类替代ArrayList。

比如使用 CopyOnWriterArrayList代替 ArrayList, CopyOnWriterArrayList在是使用上跟 ArrayList几乎一样, CopyOnWriter是写时复制的容器(COW),在读写时是线程安全的。该容器在对add和remove等操作时,并不是在原数组上进行修改,而是将原数组拷贝一份,在新数组上进行修改,待完成后,才将指向旧数组的引用指向新数组,所以对于 CopyOnWriterArrayList在迭代过程并不会发生fail-fast现象。但 CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。

伍·线程的不安全性

?什么是线程不安全

在说线程不安全之前,我们先看一下什么叫线程安全:

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。也就是我们常说的锁机制。

而线程不安全就是就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

举个栗子:

在上面的源码中我们可以看到ArrayList添加数据的时候可能分为这两个步骤:

  • 在elementData数组中存放元素
  • 扩容

在单线程情况下,如果原本的size为0,添加一个元素之后size变为1;

而在多线程情况下就有可能发生这样一个情况:
比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (因为线程A仅仅完成了第一个步骤),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。
那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。

既然线程是不安全的,但是ArrayList的高效率又是我们所需要的,那我们该如何解决呢?

一种寻求线程安全的解决方式就是ArrayList 的封装类

//也就是加上synchronizedList锁
Collections.synchronizedList(new ArrayList<>());

另外一种就是使用Vector类。
参考:

https://blog.csdn.net/weixin_43274704/article/details/113032508

https://blog.csdn.net/jiaochunyu1992/article/details/51177373

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值