Java 集合深入理解 :ArrayList源码解析,及动态扩容机制

Posted 踩踩踩从踩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 集合深入理解 :ArrayList源码解析,及动态扩容机制相关的知识,希望对你有一定的参考价值。

简介:

  • ArrayList是java集合框架很常用数据结构;
  • 实现list接口,继承 AbstractList ,基于数组实现容量大小动态变化
  • 支持指针快速访问、浅拷贝
  • 允许 null 元素的存在
  • 实现RandomAccess、Cloneable、Serializable 接口 ,arraylist是支持可序列化的。

关键方法时间复杂度

  • get() 直接读取第几个下标,复杂度 O(1)
  • add(E) 添加元素,直接在后面添加,复杂度O(1)
  • add(index, E) 添加元素,在第几个元素后面插入,后面的元素需要向后移动,复杂度O(n)
  • remove()删除元素,后面的元素需要逐个移动,复杂度O(n)

代码示例

public static void main(String[] args) {
		
		System.out.println("--------------ArrayList--------------");
		ArrayList<Integer> v=new ArrayList<Integer>();
		v.add(1);
		v.add(2);
		v.add(3);
		v.add(7);
		v.add(5);
		v.add(6);
		
		v.stream().forEach(m->{
			System.out.println(m);
		});
	}
--------------ArrayList--------------
1
2
3
7
5
6

动态数组实现的集合,能保证数据插入顺序和取出顺序,不保证多线程情况下数据的安全性

全篇注释

/**
* 可调整大小的数组实现集合接口。实现所有可选的集合操作
*,并允许所有元素,包括null元素。除了实现list接口
*这个类提供了一些方法来操纵数组的大小
*在内部用于存储集合数据(这个类大致相当于
*Vector,但不同步(unsynchronized)。)
*The size,isEmpty,get,set,迭代器(iterator)和列表迭代器(listIterator)操作
*以常量运行时间。add操作 摊销固定时间内运行,
*也就是说,添加n个元素需要O(n)个时间。所有其他操作
*以线性时间运行(粗略地说)。常数因子较低
*对于LinkedList实现。
*<p>每个<tt>ArrayList</tt>实例都有一个<i>容量(capacity)</i>。容量为
*用于存储列表中元素的数组的大小。总是这样
*至少和集合列表大小一样大。当元素添加到ArrayList时,
*它的容量自动增长。增长政策的细节并不清楚
*在添加一个元素的过程中有一个固定的摊销
*时间成本。
*<p>应用程序可以增加<tt>ArrayList</tt>实例的容量
*在使用ensureCapacity添加大量元素之前
*操作。这可能会减少增量重新分配的数量。
*<p><strong>请注意,此实现不同步。</strong>
*如果多个线程同时访问<tt>ArrayList</tt>实例,
*至少有一个线程在结构上修改列表,它
*<i>必须从外部同步(一个结构修改是
*添加或删除一个或多个元素或显式调整背衬阵列的大小;
* 仅仅设置一个元素的值是不够的
*这通常是通过在自然封装列表的某个对象上进行同步。
*如果不存在这样的对象,则应该使用
*{@link Collections#synchronizedList集合.synchronizedList}
*方法。这最好在创建时完成,以防止意外
*对列表的非同步访问:<pre>
*List=Collections.synchronizedList(新ArrayList(…))</预处理>

*这个类迭代器的{@link#iterator()iterator}和
*{@link#lisiterator(int)lisiterator}方法是应用于快速故障的:</a>
*如果在迭代器运行后的任何时候列表在结构上被修改
*以任何方式创建,除了通过迭代器自己的
*{@link ListIterator#remove()remove}或
*{@link ListIterator#add(Object)add}方法,迭代器将抛出
*{@link ConcurrentModificationException}。因此,面对
*并发修改时,迭代器会快速而干净地失败,而不是
*而不是冒着武断的、不确定的行为
*未来的时间。
*<p>请注意,不能保证迭代器的快速故障行为
*一般说来,不可能在未来作出任何硬性保证
*存在未同步的并发修改。故障快速迭代器
*尽最大努力抛出{@code ConcurrentModificationException}。
*因此,编写依赖于此的程序是错误的
*其正确性例外:<i>迭代器的快速失败行为
*应仅用于检测错误。</i>
*<p>这个类是
*<a href=“{@docRoot}/./technotes/guides/collections/index.html”>
*Java集合框架</a>。
 * @author  Josh Bloch
 * @author  Neal Gafter
 * @see     Collection
 * @see     List
 * @see     LinkedList
 * @see     Vector
 * @since   1.2
 */

整段注释解释arrylist的整个功能

  • 可调整大小的数组实现集合接口。实现所有可选的集合操作
  • 解释arraylist不支持并发
  • 不支持变迭代边操作
  • 列表迭代器(listIterator)操作
  • 面对并发修改时,迭代器会快速而干净地失败
  • Collections.synchronizedList 从而达到数据安全
  • 迭代器的快速失败行为应仅用于检测错误

接口继承

  • 继承abstractlist 获得一些 addAll sort sublist等基本操作。
  • serializable 可被序列化
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{

成员属性

  • DEFAULT_CAPACITY 默认初始容量 为10 这是在 改变数组大小时初始化
 /**
     * Default initial capacity. 默认初始容量
     */
    private static final int DEFAULT_CAPACITY = 10;
  • EMPTY_ELEMENTDATA 空实例的共享空数组实例 在 构造方法中给定初始化容量,容量等于0时,创建该空数组实例
    /**
     * Shared empty array instance used for empty instances.  用于空实例的共享空数组实例
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA 将其与 EMPTY_ELEMENTDATA数据区分开来,这在无参构造方法中设置数据,并在添加第一个元素时,进行扩展为 DEFAULT_CAPACITY
  /**
     * *用于默认大小的空实例的共享空数组实例。我们
     *将其与 EMPTY_ELEMENTDATA数据区分开来,以了解何时
    *添加第一个元素
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
  • elementData 存储ArrayList元素的数组缓冲区
 /**
     * 存储ArrayList元素的数组缓冲区。
     *ArrayList的容量是此数组缓冲区的长度。任何
    * empty ArrayList 在elementData==DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     *当添加第一个元素时,将扩展到DEFAULT_CAPACITY。
     */
    transient Object[] elementData; // 非私有以简化嵌套类访问
  • size 包含的元素 并不是整个length的长度
    /**
     * The size of the ArrayList (the number of elements it contains). ArrayList的大小(它包含的元素数)
     * @serial
     */
    private int size;

transient 非私有以简化嵌套类访问

被transient修饰的变量不参与序列化和反序列化;而且arraylist是实现了Serializable说明他能够被序列化和反序列化传输的;后来查阅了一些资料了解到:
,假如elementData的长度为10,而其中只有5个元素,那么在序列化的时候只需要存储5个元素,而数组中后面5个元素是不需要存储的。于是将elementData定义为transient,避免了Java自带的序列化机制,并定义了两个方法writeObject和readObject,实现了自己可控制的序列化
什么是writeObject 和readObject?可定制的序列化过程

构造方法

ArrayList() 和ArrayList(int initialCapacity) 构造一个空的对象数组采用不同的默认数组。
在增加第一个元素时

  • 数组为EMPTY_ELEMENTDATA 基于用户设置大小值进行1.5倍扩容 , 如果Listlist = new ArrayList<>(0);那就是基于你设置的大小0开始扩容,依次是0,1 ,2,3 ,4, 6这种1.5倍数扩容
  /**
     * Constructs an empty list with the specified initial capacity.
     *   构造具有指定初始容量的空列表
     * @param  initialCapacity  the initial capacity of the list  指定 initialCapacity集合的初始容量
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative   如果指定的初始容量为复数
     */
    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的空列表。 默认构建是一个空数组
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

ArrayList 传入Collection 的构造方法

  • 将传入集合 直接转为数组赋值给elementData ,对于扩容 则也是 基于传入数据的容量进行 1.5倍数扩容
/**
   *构造一个包含指定元素的列表集合,按集合的迭代器。
     * @param c 要将其元素放入此列表的集合中
     * @throws NullPointerException if the specified collection is null
     */
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

trimToSize方法

该方法将数组大小压缩成为size, 这里有疑点 Arrays.copyOf(elementData, size) 方法把 主要作用将数组进行截取size的大小;从而达到将数组大小压缩成为size

 /**
   *将此<tt>ArrayList</tt>实例的容量修剪为
   *列表的当前大小。应用程序可以使用此操作最小化
   *<tt>ArrayList</tt>实例的存储。
     */
    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

clone浅复制

也就是复制出的对象还是原来那个对象

 /**
    *返回此<tt>ArrayList</tt>实例的浅层副本(这个
    *不会复制图元本身。)
     * @return a clone of this <tt>ArrayList</tt> instance
     */
    public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }
  

toArray 方法

返回的时包含此列表中所有元素的数组 也就是转换为object数组

 /**
*返回包含此列表中所有元素的数组
*按正确的顺序(从第一个元素到最后一个元素)。
*<p>返回的数组将是“安全的”,因为没有对它的引用
*由此列表维护(换句话说,这个方法必须分配
*新阵列)。因此,调用者可以自由地修改返回的数组。
*<p>此方法充当了基于数组和基于集合之间的桥梁
*API。
*@return一个数组,数组中包含此列表中的所有元素
*正确的顺序
     */
    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

add方法

  • 每次添加元素到集合中时都会先确认下集合容量大小 (ensureCapacityInternal)并判断是否扩容
  • 给元素进行赋值 elementData size++ 位
/**
*将指定的元素追加到此列表的末尾。
*@param e元素添加到此列表
*@return<tt>true</tt>(由{@link Collection#add}指定)
*/
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

ensureCapacityInternal 确认内部容量方法

这里ensureCapacityInternal方法是公共方法,判断扩容的;在add(e)和add(int index, E element) 和 addAll 和readObject 方法一切需要扩容的方法上使用

调用calculateCapacity 方法 (计算容量)这里根据版本来判断,例如 jdk7版本 会有方法,在有些版本就直接写的逻辑

        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
  • 当集合为DEFAULTCAPACITY_EMPTY_ELEMENTDATA 空集合 时取 DEFAULT_CAPACITY 和
    minCapacity 的最大值也就是 10 (指定容量大小和无参构造时会 分别 弄一个不同的空数组)
  • 不直接赋值默认容量形式;主要源于 该方法也会被 addall方法调用 从代码可以看出 传入的数据可能size大于默认容量的
   public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount

ensureExplicitCapacity

  • 操作对 modCount 自增 1,记录操作次数
  • 当 minCapacity 大于 elementData 的长度,调用对grow方法集合进行扩容
  • 这个方法主要进行操作相加 ,变相的证明只有对容量进行数据添加或删除的才调用该方法
 private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

grow 函数

  • 获得老容量
 int oldCapacity = elementData.length;
  • 默认将扩容至原来容量的 1.5 倍,这里用右移一位就相当于老容量除以2
  int newCapacity = oldCapacity + (oldCapacity >> 1);

在这里插入图片描述

  • 但是扩容之后也不一定适用,有可能比较小,有可能比较大。
  • 1.5倍扩容小于所需的最小容量小于minCapacity, minCapacity赋值给newCapacity 例如首次添加数据就有可能出现
      if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
  • 大于MAX_ARRAY_SIZE( Integer.MAX_VALUE - 8),那就直接拿 newCapacity = (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE 来扩容。
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
  • 将新数组赋值给 elementData, Arrays.copyOf(elementData, newCapacity); 这里的作用就是将旧容量进行扩容
   elementData = Arrays.copyOf(elementData, newCapacity);

在这里插入图片描述

add(int index, E element)

在指定位置插入指定元素

  • rangeCheckForAdd 判断插入指针是否越界 等
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
  • 调用ensureCapacityInternal 方法确定容量是否足够,并是否扩容
 ensureCapacityInternal(size + 1);  // Increments modCount!!
  • System.arraycopy 进行复制数据 这里有几个参数
  • elementData 来源数组 index 开始复制原数据的指针 elementData 目标数组 index + 1 目标数组下标指针 size - index 复制多长数据
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
  • 最后对size进行加1
        size++;

remove(int index)方法

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

  • rangeCheck 判断inex是否超过数据的大小
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
  • 获取 当前指针下的数据
E oldValue = elementData(index); //elementData[index] 获取数据
  • 这里删除数据为保证顺序结构的特性 ,连续性 就需要移动删除指针后面的数据 ,这里求到需要移动的长度
int numMoved = size - index - 1;
  • 这里在于移动删除指针后面的数据到前一位
    if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
  • 这里主要是将原来数组指针下的数据设置为null,等待垃圾回收器进行回收
 elementData[--size] = null; // clear to let GC do its work

解决ConcurrentModificationException问题

从 迭代器(iterator)入手

重写了AbstractList.Itr的优化的版本

/**
*按正确的顺序返回此列表中元素的迭代器。
*<p>返回的迭代器是<a href=“#fail fast”><i>fail fast</i></a>。
*@按正确顺序返回此列表中元素的迭代器
*/
    public Iterator<E> iterator() {
        return new Itr();
    }
    /**
     * AbstractList.Itr的优化版本
     */
    private class Itr implements Iterator<E> {
        int cursor;       //要返回的下一个元素的索引
        int lastRet = -1; // 返回的最后一个元素的索引-1如果没有
        int expectedModCount = modCount;

        Itr() {}

        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();
            }
        }

      

迭代器分析

  • ArrayList 定义了一个内部类 Itr 实现了 Iterator 接口。
  • 在 Itr 内部有三个成员变量。 cursor:代表下一个要访问的元素下标。 lastRet:代表上一个要访问的元素下标。
    expectedModCount:代表对 ArrayList 修改次数的期望值,初始值为 modCount。

其中 hasNext 方法
下个索引已经是元素的大小(cursor != size)
next 方法

  • 判断 expectedModCount 和 modCount 是否相等。
  • 对 cursor 进行判断,看是否超过集合大小和数组长度。
  • 将 cursor 赋值给 lastRet ,并返回下标为 lastRet 的元素。
  • 将 cursor 自增 1,开始时,cursor = 0,lastRet = -1;每调用一次 next 方法, cursor 和 lastRet 都会自增 1。

remove 方法

  • 判断 lastRet 的值是否小于 0

  • 在检查 expectedModCount 和 modCount 是否相等

  • 接下来是关键,直接调用 ArrayList 的 remove 方法删除下标为 lastRet 的元素。然后将 lastRet 赋值给
    cursor ,将 lastRet 重新赋值为 -1,并将 modCount 重新赋值给 expectedModCount。

  • 调用 ArrayList 的 remove,而不是 Itr 的 remove。会将 D E 两个元素直接往前移动一位,最后一位置空,并且
    modCount 会自增 1。从 remove 方法可以看出 ,已经长度改变了所以会导致 校验时会抛出异常

 final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

解决办法 就是调用迭代器中 remove方法

因为在该方法中增加了 expectedModCount = modCount 操作。但是这个 remove 方法也有弊端。

  • 只能进行remove操作,add、clear 等 Itr 中没有。
  • 调用 remove 之前必须先调用 next。因为 remove 开始就对 lastRet 做了校验。而 lastRet 初始化时为
    -1。
  • next 之后只可以调用一次 remove。因为 remove 会将 lastRet 重新初始化为 -1
  • 多线程也会导致问题出现
  • 用另外线程安全的集合

具体可以看看 我的另一篇文章 :快速失败机制

在这里插入图片描述

java.util中list实现结构图

在这里插入图片描述
Java 集合深入理解 (十) :集合框架体系图

总结

整个arraylist其实比较简单的一个集合工具,虽然很简单,但是其中用了很多优化的东西 可以供给我们学习开发,以及数组动态扩容,总之解析完这篇源码还是很有收获的

以上是关于Java 集合深入理解 :ArrayList源码解析,及动态扩容机制的主要内容,如果未能解决你的问题,请参考以下文章

Java 集合深入理解 :ArrayList源码解析,及动态扩容机制

Java集合源码分析ArrayList

深入理解java集合框架之---------Arraylist集合

深入理解java集合框架之---------Arraylist集合 -----构造函数

深入浅出理解Java中的ArrayList集合

深入理解java集合框架之---------Arraylist集合 -----添加方法