深入Java集合:ArrayList 源码解析 - JDK8

目录

一、ArrayList概述

二、ArrayList 的实现

1. 属性

2. 方法

2.1 构造方法

2.2 get方法

2.3 add 方法

2.4 扩容机制

2.6 ensureCapacity 方法

2.7 set 方法

2.8 remove 方法

2.9 size 方法

2.10 trimToSize 方法

2.11 indexOf 方法和 lastIndexOf

2.12 其他方法

三、Vector

3.1 属性

3.2 构造

3.3 grow方法

四、总结


一、ArrayList概述

       一上来,先来看看源码中的这一段注释,我们可以从中提取到一些关键信息

/**
 * Resizable-array implementation of the <tt>List</tt> interface.  Implements
 * all optional list operations, and permits all elements, including
 * <tt>null</tt>.  In addition to implementing the <tt>List</tt> interface,
 * this class provides methods to manipulate the size of the array that is
 * used internally to store the list.  (This class is roughly equivalent to
 * <tt>Vector</tt>, except that it is unsynchronized.)
 *
 * <p>The <tt>size</tt>, <tt>isEmpty</tt>, <tt>get</tt>, <tt>set</tt>,
 * <tt>iterator</tt>, and <tt>listIterator</tt> operations run in constant
 * time.  The <tt>add</tt> operation runs in <i>amortized constant time</i>,
 * that is, adding n elements requires O(n) time.  All of the other operations
 * run in linear time (roughly speaking).  The constant factor is low compared
 * to that for the <tt>LinkedList</tt> implementation.
 *
 * <p>Each <tt>ArrayList</tt> instance has a <i>capacity</i>.  The capacity is
 * the size of the array used to store the elements in the list.  It is always
 * at least as large as the list size.  As elements are added to an ArrayList,
 * its capacity grows automatically.  The details of the growth policy are not
 * specified beyond the fact that adding an element has constant amortized
 * time cost.
 *
 * <p>An application can increase the capacity of an <tt>ArrayList</tt> instance
 * before adding a large number of elements using the <tt>ensureCapacity</tt>
 * operation.  This may reduce the amount of incremental reallocation.
 *
 * <p><strong>Note that this implementation is not synchronized.</strong>
 * If multiple threads access an <tt>ArrayList</tt> instance concurrently,
 * and at least one of the threads modifies the list structurally, it
 * <i>must</i> be synchronized externally.  (A structural modification is
 * any operation that adds or deletes one or more elements, or explicitly
 * resizes the backing array; merely setting the value of an element is not
 * a structural modification.)  This is typically accomplished by
 * synchronizing on some object that naturally encapsulates the list.
 *
 * If no such object exists, the list should be "wrapped" using the
 * {@link Collections#synchronizedList Collections.synchronizedList}
 * method.  This is best done at creation time, to prevent accidental
 * unsynchronized access to the list:<pre>
 *   List list = Collections.synchronizedList(new ArrayList(...));</pre>
 *
 * <p><a name="fail-fast">
 * The iterators returned by this class's {@link #iterator() iterator} and
 * {@link #listIterator(int) listIterator} methods are <em>fail-fast</em>:</a>
 * if the list is structurally modified at any time after the iterator is
 * created, in any way except through the iterator's own
 * {@link ListIterator#remove() remove} or
 * {@link ListIterator#add(Object) add} methods, the iterator will throw a
 * {@link ConcurrentModificationException}.  Thus, in the face of
 * concurrent modification, the iterator fails quickly and cleanly, rather
 * than risking arbitrary, non-deterministic behavior at an undetermined
 * time in the future.
 *
 * <p>Note that the fail-fast behavior of an iterator cannot be guaranteed
 * as it is, generally speaking, impossible to make any hard guarantees in the
 * presence of unsynchronized concurrent modification.  Fail-fast iterators
 * throw {@code ConcurrentModificationException} on a best-effort basis.
 * Therefore, it would be wrong to write a program that depended on this
 * exception for its correctness:  <i>the fail-fast behavior of iterators
 * should be used only to detect bugs.</i>
 *
 * <p>This class is a member of the
 * <a href="{@docRoot}/../technotes/guides/collections/index.html">
 * Java Collections Framework</a>.
 *
 * @author  Josh Bloch
 * @author  Neal Gafter
 * @see     Collection
 * @see     List
 * @see     LinkedList
 * @see     Vector
 * @since   1.2
 */

       ArrayList是List接口的可变数组的实现。实现了所有可选列表操作,并允许所有元素,包括null。除了实现List接口,这个类提供了操作数组大小的方法,用于内部存储列表。(这个类大致相当于Vector,除了它是不同步即线程不安全的。)

       size、isEmpty、get、set,iterator,listIterator 操作以恒定时间运行。add操作在摊销恒定时间内运行,即添加n个元素需要O(n)时间。所有其他操作都在线性时间内运行(粗略地说)。与LinkedList 实现相比,常数因子较低。

       每个ArrayList实例具有一个容量。这个容量是用于存储列表中元素的数组的大小。它总是
至少与列表大小相同。当元素被添加到数组列表时,容量自动增加。除了添加要素具有恒定的摊余时间成本这一事实之外,没有详细说明增长政策。

       在使用ensurecapity 操作添加大量元素之前,应用程序可以增加ArrayList 实例的容量。这可以减少递增式再分配的数量。

       请注意,此实现是不同步的。如果多个线程同时访问一个ArrayList实例,并且至少有一个线程在结构上修改了该列表,则必须在外部进行同步。(结构修改是添加或删除一个或多个元素,或显式调整支持数组大小的任何操作;仅设置元素的值不是结构修改。)这通常通过在自然封装列表的某个对象上进行同步来实现。

       如果不存在此类对象,则应使用集合“包装”列表。 Collections.synchronizedList() 方法。这最好在创建时完成,以防止意外不同步地访问列表:

List list = Collections.synchronizedList(new ArrayList(...));

       通过此类iterator和listIterator方法返回的迭代器是故障快速的:如果在迭代器被创建后的任何时间,列表以任何方式(除迭代器自己的remove或add方法外)在结构上修改,迭代器将抛出ConcurrentModificationException。因此,面对并发修改,迭代器会快速地、干净利落地失败,而不是在未来的不确定时间冒着任意、不确定性行为的风险。

       请注意,无法原样保证迭代器的快速故障行为,因为一般来说,在存在非同步并发修改的情况下,不可能做出任何硬保证。快速失效迭代器以最大努力的方式抛出ConcurrentModificationException。因此,编写依赖于此异常的正确性的程序是错误的:迭代器的快速故障行为应该只用于检测错误。

       此类是Java集合框架的成员。

       作者:Josh Bloch、Neal Gafter

       请参见:Collection, List, LinkedList, Vector, Serialized Form 类

       从以下版本开始:JDK 1.2

文档注释中结实的Arraylist的具有的功能和特点

  • 可调整大小的数组实现集合接口。实现所有可选的集合操作

  • 解释arraylist不支持并发

  • 不支持边迭代边操作

  • 列表迭代器(listIterator)操作

  • 面对并发修改时,迭代器会快速而干净地失败

  • Collections.synchronizedList 从而达到数据安全

  • 迭代器的快速失败行为应仅用于检测错误

二、ArrayList 的实现

对于 ArrayList 而言,它实现 List 接口、底层使用数组保存所有元素。其操作基本上是对数组的操作。下面我们来分析 ArrayList 的源代码:

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

ArrayList 继承于AbstartList。另外实现了List、RandomAccess、Cloneable、java.io.Serializable等接口,也就是告诉我们ArrayList 具备List 的特性,同时支持随机访问,支持对象拷贝,原生支持序列化。

AbstartList是一个抽象类,继承Abstractlist 获得一些 addAll、sort、sublist等基本操作,还提供了两个迭代器的实现类,既继承了AbstractCollection抽象类,拥有AbstractCollection的默认实现方法,同时也实现了List接口,为List接口方法添加了默认实现和拓展。

1. 属性

    // 用来序列的标识符/反序列化的对象序列化类
    private static final long serialVersionUID = 8683452581122892189L;

    // ArrayList初始容量大小为10
    private static final int DEFAULT_CAPACITY = 10;

    // 空数组常量,用于空实例的共享空数组实例
    private static final Object[] EMPTY_ELEMENTDATA = {};

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

    // 存放ArrayList数组元素的数组,从这可以发现 ArrayList 的底层实现就是一个 Object数组
    transient Object[] elementData; // non-private to simplify nested class access

    // 该属性设置容量大小,ArrayList 所包含的元素个数
    private int size;

ArrayList 的属性非常少,就只有这些。其中最重要的莫过于 elementData 了,说明底层使用数组实现,ArrayList 所有的方法都是建立在 elementData 之上。接下来,我们就来看一下一些主要的方法吧。

2. 方法

2.1 构造方法

ArrayList 提供了三种方式的构造器

  • 可以构造一个默认初始容量为 10 的空列表

  • 构造 一个指定初始容量的空列表

  • 构造一个包含指定 collection 的元素的列表,这些元素按照该 collection 的迭代器返回它们的顺序排列的。

    // 这个构造需要传一个int值,表示初始容量
	public ArrayList(int initialCapacity) {
        // 判断传入的指定初始容量是否大于0
        if (initialCapacity > 0) {
            // 大于0, 将存放ArrayList元素的数组, 初始化成长度为传入的指定初始容量的列表
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            // 等于0, 将空实例的共享空数组实例, 赋值给存放ArrayList元素的空列表
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            // 小于0, 抛出非法参数异常:非法容量
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

	// 空构造:构造一个初始容量为10的空列表
    public ArrayList() {
        // 将默认大小的空实例的共享空数组实例, 赋值给存放ArrayList元素的数组
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        // 当添加第一个元素时, 触发扩容, 容量为初始容量大小10
    }

	// 构造一个包含指定集合元素的列表, 按照集合的迭代器返回的顺序
    public ArrayList(Collection<? extends E> c) {
        // 将列表c 转换为数组, 赋值给存储ArrayList元素的数组
        elementData = c.toArray();
        // 将存储ArrayList元素的数组的长度, 赋值给列表元素个数, 并判断是否不等于0
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            // c.toArray可能(错误地)不返回Object[]
            // 数组元素的个数不等于0, 判断elementData数组的类型是否和Object[]的类型一致
            if (elementData.getClass() != Object[].class)
                // 如果出现类型不一致, 拷贝一个新的elementData数组
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            // 如果数组的元素的个数等于0, 替换成空数组
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

从构造方法中我们可以看见,默认情况下,elementData 是一个大小为 0 的空数组,在添加一个元素时触发扩容,扩充为初始容量10;当我们指定了初始大小的时候,elementData 的初始大小就变成了我们所指定的初始大小了。

2.2 get方法

    // 通过下标获取某个元素
    public E get(int index) {
        // 检查下标是否越界
        rangeCheck(index);
        // 返回对应元素
        return elementData(index);
    }
    
    // 检查下标是否越界,如果大于等于数组元素个数,抛出越界异常
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    
    // 通过下标位置访问元素操作
    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }

因为 ArrayList 是采用数组结构来存储的,所以它的 get 方法非常简单,先是判断一 下有没有越界,之后就可以直接通过数组下标来获取元素了,所以 get 的时间复杂度是 O(1)。

2.3 add 方法

在ArrayList中add方法有两个重载,add(E e) 和 add(int index, E element)。add(E e) 是直接添加,默认添加到尾部;add(int index, E element) 是添加到指定下标位置

    // 添加一个元素e到最后一个元素后面
    public boolean add(E e) {
        // 先检查是否需要扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!! 增加修改值
        // 把element赋值给下标为size的元素(即末尾),且让size自增1
        elementData[size++] = e;
        // 添加成功,返回true
        return true;
    }
    
    // 在指定下标位置添加一个元素element
    public void add(int index, E element) {
        // 检查是否添加的位置的下标是否合法
        rangeCheckForAdd(index);
        // 先检查是否需要扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!! 增加修改值
        // 移动指定位置后面的元素
        // 把elementData中下标index开始的size-index个元素拷贝至elementData中的index + 1开始的size-index个元素
        System.arraycopy(elementData, index, elementData, index + 1, size - index);
        // 把element赋值给下标为index的元素
        elementData[index] = element;
        // 加入了一个元素,让size自增1
        size++;
    }
    
    // 检查是否添加的位置的下标是否合法
    private void rangeCheckForAdd(int index) {
        // 如果下标大于size或者下标小于0,抛出下标越界异常
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    
    // 下标越界的详细信息
    private String outOfBoundsMsg(int index) {
        return "Index: "+index+", Size: "+size;
    }
​
    // 添加一个集合的全部元素到列表中
    public boolean addAll(Collection<? extends E> c) {
        // 先将集合c转换为数组a
        Object[] a = c.toArray();
        // 新元素的个数为数组a的长度
        int numNew = a.length;
        // 调用确认内部容量的方法,判断是否扩容minCapacity = size + numNew
        ensureCapacityInternal(size + numNew);  // Increments modCount
        // 拷贝数组的所有元素到elementData从下标size开始
        System.arraycopy(a, 0, elementData, size, numNew);
        // size加上新元素的个数
        size += numNew;
        // 返回新元素是否为空,如果不为空,返回true,表示添加成功
        return numNew != 0;
    }
    
    // 把一个集合添加到集合中的指定位置
    public boolean addAll(int index, Collection<? extends E> c) {
        // 检查是否添加的位置的下标是否合法
        rangeCheckForAdd(index);
        // 先将集合c转换为数组a
        Object[] a = c.toArray();
        // 新元素的个数为数组a的长度
        int numNew = a.length;
        // 调用确认内部容量的方法,判断是否扩容minCapacity = size + numNew
        ensureCapacityInternal(size + numNew);  // Increments modCount
        // 需要移动的元素的个数
        int numMoved = size - index;
        // 判断这个个数是否大于0,如果不大于0,说明是在尾部;大于0,说明是在中间,
        if (numMoved > 0)
            // 移动下标index开始的元素到最后的,移动numNew个位置
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);
        // 在尾部添加,直接将数组a,拷贝到elementData下标index开始
        System.arraycopy(a, 0, elementData, index, numNew);
        // size加上新元素的个数
        size += numNew;
        // 返回新元素是否为空,如果不为空,返回true,表示添加成功
        return numNew != 0;
    }

ArrayList 的 add 方法也很好理解,在插入元素之前,它会先检查是否需要扩容,然后再把元素添加到数组中最后一个元素的后面。在 ensureCapacityInternal 方法中, 我们可以看见,如果当 elementData 为默认空数组时,它会使用默认的大小去扩容。所以说,通过无参构造方法来创建 ArrayList 时,它的大小其实是为 0 的,只有在使用到 的时候,才会通过 grow 方法去创建一个大小为 10 的数组。

第一个 add 方法的复杂度为 O(1),虽然有时候会涉及到扩容的操作,但是扩容的次 数是非常少的,所以这一部分的时间可以忽略不计。如果使用的是带指定下标的 add 方法,则复杂度为 O(n),因为涉及到对数组中元素的移动,这一操作是非常耗时的,底层使用的是System.arraycopy()方法,移动指定位置后面的元素。

2.4 扩容机制

    // 隐式确保容量:在往列表中添加元素时会被调用:ensureCapacityInternal(size + 1);
    private void ensureCapacityInternal(int minCapacity) {
        // 判断elementData是不是默认的空数组
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // 最小容量 = 默认初始容量和传入的所需最小容量的大值
            // 如果是通过无参构造方法来创建 ArrayList 时,它的大小其实是为 0 的,满足此条件则最小容量为10
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        
        // 调用显式确保容量的方法:ensureExplicitCapacity(最小容量)
        ensureExplicitCapacity(minCapacity);
    }
​
    // 显式确保容量
    private void ensureExplicitCapacity(int minCapacity) {
        // protected transient int modCount = 0; 定义在AbstractList类中的属性,用于记录集合修改值
        // int expectedModCount = modCount; AbstractList类中List迭代Itr时定义了期望的修改值
        // 在集合修改时触发++操作,修改值加1,目的是检测期望值与集合修改值是否一致,
        // 不一致说明被意外修改,会抛出ConcurrentModificationException异常,这提供了失败快速行为,在迭代期间面对并发修改。
        modCount++;
​
        // 当所需要的最小容量 - elementData数组的常量如果大于0,就触发扩容 grow方法,以所需要的最小容量扩容
        // overflow-conscious code 有溢出意识的代码
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

ensureCapacityInternal()方法中判断elementData数组是否为空,也就是判断是否是第一次添加元素,如果是第一次添加元素,则设置初始化大小为默认容量10,否则为传入的参数。这个方法的目的就是获取初始化数组容量。获取到初始化容量后调用ensureExplicitCapacity(minCapacity)方法,判断是否需要扩容,如果需要扩容,调用grow 方法,通过下面的grow 方法解读,我们看到ArrayList 每次扩容都是扩 1.5 倍。

注意:在对列表进行修改的方法中都会有一个 modCount++ 操作,该变量定义在AbstractList类中的属性,是用来记录当前集合被修改的次数,AbstractList类中List迭代Itr时定义了expectedModCount(期望的修改值),主要为了当多个线程同时操作该数组出现线程安全问题的时候及时抛出错误。

2.5 grow 方法

    /*
     * 要分配的数组的最大大小。一些虚拟机在数组中保留一些头字。
     * 尝试分配更大的数组可能会导致OutOfMemoryError:请求的数组大小超出VM限制
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
​
    /**
     * 增加容量,以确保它至少可以容纳由最小容量参数指定的元素数。
     * 参数:minCapacity -- 所需的最小容量
     */
    private void grow(int minCapacity) {
        // overflow-conscious code 有溢出意识的代码
        // oldCapacity旧的容量 = elementData数组的长度
        int oldCapacity = elementData.length;
        // newCapacity新的容量 = 旧容量 + 旧容量右移1(相当于/2,位运算的效率高),也就是说新容量为旧容量的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 判断(新容量 - 所需的最小容量)是否是小于0,如果小于0,也就是当所需的最小容量大于新容量,就把最小容量赋给新容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 判断(新容量 - 数组的最大大小)是否是大于0,也就是说是否新的容量,超过了最大大小
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            // 超过最大大小,则新容量的值为通过大容量处理得到的值,hugeCapacity(所需最小容量),
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win: ==> minCapacity通常接近大小
        // 把元素重新拷贝到一个新容量长度的数组中去,再赋值给elementData,完成扩容
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    
    // 处理大容量
    private static int hugeCapacity(int minCapacity) {
        // 当需要的最小容量小于0,说明整形溢出了,所以就触发OOM错误了
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        // 如果所需的最小容量大于 > 最大大小,返回Integer.MAX_VALUE最大的整数,否则返回最大大小
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

grow 方法是在数组进行扩容的时候用到的,从中我们可以看见,ArrayList 每次扩容都是扩 1.5 倍,然后调用 Arrays 类的 copyOf 方法,把元素重新拷贝到一个新的数组中去。

扩容机制:当往数组中添加元素入add、addAll等方法时,会ensureCapacityInternal

扩展:

ArrayList的自动扩容机制底层借助于System实现,这里调用的是Arrays.copyOf()方法扩容的,而在Arrays.copyOf() 在底层调用的是System.arraycopy()。

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

五个参数分别为:

  • src:源数组

  • srcPos:从源数组的下标开始复制

  • dest:目标数组

  • destPos:目标数组的下标开始

  • length:复制多少个元素

2.6 ensureCapacity 方法

    /**
     * 默认初始容量.
     */
    private static final int DEFAULT_CAPACITY = 10;
    /**
     * 用于默认大小空实例的共享空数组实例。如 ArrayList list = new ArrayList<>() list就等于{}.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}
    // 存放ArrayList数组元素的数组,从这可以发现 ArrayList 的底层实现就是一个 Object数组
    transient Object[] elementData; 

	/**
     * 如有必要,增加此ArrayList实例的容量,以确保它至少可以容纳由最小容量参数指定的元素数。
     * 参数:minCapacity -- 所需的最小容量
     */
	// 确保容量
    public void ensureCapacity(int minCapacity) {
        // 最小扩展 = (存放数组元素的数组 != 默认空数组) ? 0 : 默认容量10
        // 以上是说如果当elementData不是默认空数组时,最小的扩展值为0;
        // 如果是默认的空数组,那么此时的最小扩展的值为10默认初始容量
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;
		// 判断所需的最小容量是否是大于最小扩展值的,如果大于,那么我们调用ensureExplicitCapacity,做是否要扩容的操作,否则不需要做确认容量的操作
        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }

如果要增加的数据量很大,应该使用ensureCapacity方法,该方法的作用是预先设置Arraylist的大小,这样可以大大提高初始化速度。

可通过下面这段代码加以理解:

package com.openlab.exer;

import java.util.ArrayList;

public class ArrayListTest {
	public static void main(String[] args) {
		final int N = 1000000;
		Object obj = new Object();
		
		//没用调用ensureCapacity()方法初始化ArrayList对象
		ArrayList list = new ArrayList();
		long startTime = System.currentTimeMillis();
		for(int i=0;i<=N;i++){
			list.add(obj);
		}
		long endTime = System.currentTimeMillis();
		System.out.println("没有调用ensureCapacity()方法所用时间:" + (endTime - startTime) + "ms");
		
		//调用ensureCapacity()方法初始化ArrayList对象
		list = new ArrayList();
		startTime = System.currentTimeMillis();
		list.ensureCapacity(N);//预先设置list的大小
		for(int i=0;i<=N;i++){
			list.add(obj);
		}
		endTime = System.currentTimeMillis();
		System.out.println("调用ensureCapacity()方法所用时间:" + (endTime - startTime) + "ms");
	}
}

2.7 set 方法

    /**
   	 * 用指定的元素替换此列表中指定位置的元素。
   	 * @param index 要替换的元素的索引
     * @param element 要存储在指定位置的元素
     * @return 返回先前指定位置的元素
     * @throws IndexOutOfBoundsException {@inheritDoc}
   	 */
    public E set(int index, E element) {
        // 检查下标是否合法
        rangeCheck(index);
		
        // 使用变量oldValue 记录原来的指定下标的元素,后面return用
        E oldValue = elementData(index);
        // 把element赋值给指定下标的元素
        elementData[index] = element;
        // 返回先前指定位置的元素
        return oldValue;
    }
    
	// 判断下标是否越界
   	private void rangeCheck(int index) {
        // 判断下标是否大于size,如果大于抛出下标越界异常
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
	
	// 下标越界的详细信息
    private String outOfBoundsMsg(int index) {
        return "Index: "+index+", Size: "+size;
    }

set 方法的作用是把下标为 index 的元素替换成 element,跟 get 非常类似,所以就不在赘述了,时间复杂度度为 O(1)。

2.8 remove 方法

    /**
     * 删除此列表中指定位置的元素。将任何后续元素向左移动(从其索引中减去一个)
     * @param index 要删除的元素的索引
     * @return 返回从列表中删除的元素
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
	// 以下标删除元素
    public E remove(int index) {
        // 检查下标是否越界
        rangeCheck(index);
		
        // 操作值+1
        modCount++;
        // 使用变量oldValue 记录原来的指定下标的元素,后面return用
        E oldValue = elementData(index);
		
        // 要移动的元素的个数
        int numMoved = size - index - 1;
        // 如果移动的元素个数大于零,即不是删除尾部元素时,该下标元素后的元素前移
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // 把最后的一个元素(size - 1)的元素置空等待gc回收,且size - 1即--size
        elementData[--size] = null; // clear to let GC do its work // 置空让GC来回收
		
        // 返回旧的元素
        return oldValue;
    }

    /**
	 * 从该列表中删除指定元素的第一个匹配项(如果存在)。如果列表中不包含该元素,它将保持不变。
	 * 更正式地,移除具有最低索引i的元素。
	 * 如:o==null ? get(i)==null : o.equals(get(i))(如果存在这样的元素)
	 * 如果该列表包含指定元素,返回true(或者等效地,如果此列表因调用而更改)
     */
	// 以元素删除元素
    public boolean remove(Object o) {
        // 判断o是不是null
        if (o == null) {
            // o是null,则从下标0开始到最后一个遍历,找到为null的元素,调用快速删除方法
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    // 删除之后 返回true
                    return true;
                }
        } else {
            // o不是null,则从下标0开始到最后一个遍历
            for (int index = 0; index < size; index++)
                // 使用equals比较两个对象的内存地址,如果相等,调用快速删除方法删除该方法
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    // 删除之后 返回true
                    return true;
                }
        }
        //如果两种情况都没返回,则返回false
        return false;
    }

    /*
     * 私有的remove方法跳过边界检查,不返回移除的值
     */
    private void fastRemove(int index) {
        // 操作值加1
        modCount++;
        // 要移动的元素的个数
        int numMoved = size - index - 1;
        // 如果移动的元素个数大于零,即不是删除尾部元素时,该下标元素后的元素前移
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // 把最后的一个元素(size - 1)的元素置空等待gc回收,且size - 1所以这里用--size
        elementData[--size] = null; // clear to let GC do its work
    }
	
	// 范围删除多个元素, [fromIndex, toIndex)
	// 注意这个方法是protected的,只能创建子类使用
	// fromIndex:起始下标,toIndex:结束下标
	protected void removeRange(int fromIndex, int toIndex) {
        // 操作值加1
        modCount++;
        // 要移动的元素的个数
        int numMoved = size - toIndex;
        // 将下标从toIndex开始的元素,移动至下标fromIndex开始
        System.arraycopy(elementData, toIndex, elementData, fromIndex,
                         numMoved);
		// 新的size等于原size - (结束下标 - 起始下标)
        // clear to let GC do its work
        int newSize = size - (toIndex-fromIndex);
        // 遍历从下标newSize开始往后的元素,置为null,等待GC回收
        for (int i = newSize; i < size; i++) {
            elementData[i] = null;
        }
        // size就等于新的newSize
        size = newSize;
    }

remove 方法与 add 带指定下标的方法非常类似,也是调用系统的 arraycopy 方法来移动元素,时间复杂度为 O(n)

2.9 size 方法

    // 返回元素的个数
    public int size() {
        return size;
    }

size 方法非常简单,它是直接返回 size 的值,也就是返回数组中元素的个数,时间复杂度为 O(1)。这里要注意一下,返回的并不是数组的实际大小。

2.10 trimToSize 方法

	/**
   	 * 将此ArrayList实例的容量修剪为列表的当前大小。应用程序可以使用此操作最小化ArrayList实例的存储。
     */
    public void trimToSize() {
        // 操作值加1
        modCount++;
        // 判断size是不是小于数组的长度
        if (size < elementData.length) {
            // elementData数组 = 如果size为0,是空数组,如果不为0,拷贝一个长度为size的原来的数组
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

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

2.11 indexOf 方法和 lastIndexOf

	// 返回此列表中指定元素第一次出现的索引,如果此列表不包含该元素,则返回-1。
	// 更正式地说,它返回最低的索引i,使得(o == null ? get(i) == null : o.equals(get(i))),
	// 或者如果没有这样的索引,则返回-1
	public int indexOf(Object o) {
        // 判断如果o 等于null,则遍历列表从下标为0的元素到最后
        if (o == null) {
            for (int i = 0; i < size; i++)
                // 判断下标为i的元素是否为空,为空,则返回下标i
                if (elementData[i]==null)
                    return i;
        } else {
            // 如果o不等于null,则遍历列表从下标为0的元素到最后
            for (int i = 0; i < size; i++)
                // 使用equals方法判断o和下标为i的元素是否相等,相等return 下标i
                if (o.equals(elementData[i]))
                    return i;
        }
        // 以上都不满足,返回-1
        return -1;
    }

	// 返回此列表中指定元素最后一次出现的索引,如果此列表不包含该元素,则返回-1。
	// 更正式地说,它返回最高的索引i,使得(o == null ? get(i) == null : o.equals(get(i))),
	// 或者如果没有这样的索引,则返回-1
	public int lastIndexOf(Object o) {
        // 判断如果o 等于null,则列表从最后一个下标向前遍历到最后
        if (o == null) {
            for (int i = size-1; i >= 0; i--)
                // 判断下标为i的元素是否为空,为空,则返回下标i
                if (elementData[i]==null)
                    return i;
        } else {
            // 如果o 等于null,则列表从最后一个下标向前遍历到最后
            for (int i = size-1; i >= 0; i--)
                // 使用equals方法判断o和下标为i的元素是否相等,相等return 下标i
                if (o.equals(elementData[i]))
                    return i;
        }
        // 以上都不满足,返回-1
        return -1;
    }

indexOf 方法的作用是返回第一个等于给定元素的值的下标。它是通过遍历比较数组 中每个元素的值来查找的,所以它的时间复杂度是 O(n)。 lastIndexOf 的原理跟 indexOf 一样,而它仅仅是从后往前找起罢了。

2.12 其他方法

isEmpty()、contains(Object o)、clone()、toArray()、toArray(T[] a)、clear()

    // 判断是否为空
	public boolean isEmpty() {
        // 判断size是否为0
        return size == 0;
    }
    // 判断是否包含元素o
    public boolean contains(Object o) {
        // 调用indexof方法的返回值大于等于零说明存在该元素
        return indexOf(o) >= 0;
    }

	// 返回此ArrayList实例的浅拷贝
	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);
        }
    }
	
	// 返回的时包含此列表中所有元素的数组 也就是转换为object数组,但是无法将Object[] 直接转化为具体类型的数组
	public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

	// 将list转化为自定义的需要的类型的数组
    public <T> T[] toArray(T[] a) {
        // 如果a的长度小于size
        if (a.length < size)
            // 创建a的运行时类型的新数组,但我的内容
            // Make a new array of a's runtime type, but my contents:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        // 拷贝elementData的所有元素到a
        System.arraycopy(elementData, 0, a, 0, size);
        // 当a的长度大于size时,将第size个元素置空
        if (a.length > size)
            a[size] = null;
        // 返回数组a
        return a;
    }
	
	// 清空列表
	public void clear() {
        // 操作值++
        modCount++;
		
        // 循环遍历,将每一个元素置空,等待GC回收
        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;
		// 把size置0
        size = 0;
    }

直接调用Arrays的copyOf方法返回了一个ArrayList内部elementsData数组的复制数组,但是无法将Object[] 直接转化为具体类型的数组可以转换但比较麻烦。重载方法toArray(T[] a)是拿到某个具体类型的对象数组,但是在如果数组a长度大于arrayList的size,并不改变a的length,而是直接将对应大小的数组copy到a中,然后这里底层居然直接简单粗暴的令a[size]=null,就很令人迷惑,个人绝对这段代码不优。

三、Vector

本来是想把 Vector 当成新一篇来讲的,结果看了一下源码之后发现并没有什么好讲, 因为很多方法都跟 ArrayList 一样,只是多加了个 synchronized 来保证线程安全罢了。 如果照着 ArrayList 的方式再将一次就显得没意思了,所以只把 Vector 与 ArrayList 的不同点提一下就可以了。

3.1 属性

Vector 比 ArrayList 多了一个属性:

// 每次扩容的空间,用于扩容
protected int capacityIncrement;

这个属性是在扩容的时候用到的,它表示每次扩容只扩 capacityIncrement 个空间就足够了。该属性可以通过构造方法给它赋值。先来看一下构造方法:

3.2 构造

	// 双参数的构造,第一个参数为初始容量,第二个参数为每次扩容的空间,用于给类中的属性赋值,用于扩容
	public Vector(int initialCapacity, int capacityIncrement) {
        super();
        // 如果初始容量小于0
        if (initialCapacity < 0)
            // 抛出IllegalArgumentException异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        // elementData创建一个传入的初始化容量大小的Object数组
        this.elementData = new Object[initialCapacity];
        // 通过构造方法给每次扩容的空间赋值
        this.capacityIncrement = capacityIncrement;
    }
    
	// 单参的构造,参数为初始容量
    public Vector(int initialCapacity) {
        // capacityIncrement值赋为0
        this(initialCapacity, 0);
    }
	// 无参构造
	public Vector() {
        // 默认调用单参的构造,且初始大小为10
        this(10);
    }

从构造方法中,我们可以看出 Vector 的默认大小也是 10,而且它在初始化的时候就已经创建了数组了,这点跟 ArrayList 不一样。再来看一下 grow 方法

3.3 grow方法

	private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        // 当没有指定capacityIncrement(即使用无参构造或单参构造时,默认为0)
        // 此时新的容量为oldCapacity + oldCapacity
        // 当指定capacityIncrement使用两个参数的构造,则每次扩容的新的容量为oldCapacity + capacityIncrement
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        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();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

从 grow 方法中我们可以发现,newCapacity 默认情况下是两倍的 oldCapacity,而当指定了 capacityIncrement 的值之后,newCapacity变成了 oldCapacity+capacityIncrement。其他的都和ArrayList一样

四、总结

1、ArrayList 创建时的大小为 0;当加入第一个元素时,进行第一次扩容时,默认容 量大小为 10。

2、ArrayList 每次扩容都以当前数组大小的 1.5 倍去扩容。

3、Vector 创建时的默认大小为 10。

4、Vector 每次扩容都以当前数组大小的 2 倍去扩容。当指定了 capacityIncrement 之 后,每次扩容仅在原先基础上增加 capacityIncrement 个单位空间。

5、ArrayList 和 Vector 的 add、get、size 方法的复杂度都为 O(1),remove 方法的复 杂度为 O(n)。

6、ArrayList 是非线程安全的,Vector 是线程安全的

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Golang_HZ

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

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

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

打赏作者

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

抵扣说明:

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

余额充值