ArrayList源码-常用方法

微笑、不失礼 提交于 2020-08-10 22:44:16

        ArrayList 这个类应该是平时代码中最常用的一种列表实现类,在此通过查看源码来进一步理解常用方法,同时了解一下不常用的方法,应该会有惊喜。这次换个方式来看,以方法熟悉度去看对应源码,有必要的在贴源码。

    1. ArrayList 头部注释

        此类从 java 1.2 开始引入。它是 List 接口的可调整大小数组实现。实现所有可选的列表操作,并允许所有元素,包括 null 。 除了实现 List 接口之外,此类还提供一些方法来操纵内部用于存储列表的数组的大小。此类与 Vector 大致等效,但它是不同步的。

        size,isEmpty,get, set ,iterator 和 listIterator 操作在恒定时间内运行。add 操作在“摊销固定时间”中运行,也就是说,添加n个元素需要O(n)时间。所有其他操作均以线性时间运行(大致而言)。 与 LinkedList 实现的常量因子相比,常量因子较低。

        每个 ArrayList 实例都有一个“容量”。 容量是用于在列表中存储元素的数组的大小。 它总是至少与列表大小一样大。 将元素添加到ArrayList后,其容量会自动增长。 除了添加元素具有固定的摊销时间成本外,没有指定增长策略的详细信息。在操作添加大量元素之前,应用程序可以使用 ensureCapacity 增加ArrayList 实例的容量,这可以减少增量重新分配的数量。

        请注意,此实现未同步。如果多个线程同时访问 ArrayList 实例,并且至少有一个线程在结构上修改了列表,则它必须在外部进行同步。 (结构修改是添加或删除一个或多个元素,或显式调整底层数组的大小的任何操作;仅设置元素的值不是结构修改。)这通常是通过同步某些自然封装列表的对象来实现的 。如果不存在这样的对象,则应使用 Collections.synchronizedList 方法“包装”列表。 最好在创建时完成此操作,以防止意外地不同步访问列表:

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

        此类的 iterator 迭代器和 listIterator 方法返回的迭代器是快速失败:如果在创建迭代器之后的任何时间对列表进行结构修改,除了通过迭代器自己的ListIterator#remove()或 ListIterator#add(Object)的其他方法,迭代器将抛出{@link ConcurrentModificationException}。 因此,面对并发修改,迭代器会快速干净地失败,而不会在未来的不确定时间内冒任意、不确定的行为的风险。请注意,迭代器的快速失败行为无法得到保证,因为通常来说,在存在不同步的并发修改的情况下,不可能做出任何严格的保证。 快速失败的迭代器会尽最大努力抛出 ConcurrentModificationException。 因此,编写依赖于此异常的程序来确保程序的正确性是错误的:迭代器的快速失败行为应仅用于检测错误。

    2. 常用方法

    2.1 构造方法

    最常用的就是空参构造方法,看一下源码实现:

/**
 * 构造一个初始容量为10的空列表
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
 * 共享的空数组实例,用于默认大小的空实例。
 * 我们将此与EMPTY_ELEMENTDATA区别开来,以了解添加第一个元素时需要扩充多少
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
 * 存储ArrayList元素的数组缓冲区。ArrayList的容量是此数组缓冲区的长度。
 * 添加第一个元素时,任何具有elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
 * 的空ArrayList都将扩展为 DEFAULT_CAPACITY。
 */
transient Object[] elementData; // non-private to simplify nested class access 非私有以简化嵌套类访问

        构造方法注释:构造一个初始容量为10的空列表,但是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 与 elementData  都不满足,elementData  变量注释有这样一个描述:添加第一个元素时,任何具有elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA  的空ArrayList都将扩展为 DEFAULT_CAPACITY。查看添加方法最终会调用如下方法:

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {//注意内部数组对象是默认空数组实例
        return Math.max(DEFAULT_CAPACITY, minCapacity);//取较大者,minCapacity在第一次添加是为 1
    }
    return minCapacity;
}
/**
 * Default initial capacity.
 */
private static final int DEFAULT_CAPACITY = 10;
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

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

        由上可知,无参构造方法创建的数组列表实例,底层数组的 length = 0,在添加第一个元素时会扩充数组容量(具体逻辑先不深入)为 10,此时这个列表的容量也就是 10。

    可指定初始化容量的构造方法:

/**
 * 构造一个具有指定初始容量的空列表。
 */
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);
    }
}
/**
 * 用于空实例的共享空数组实例。
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

        初始化容量大于0,直接创建一个指定大小的数组对象并赋值给底层数组变量;初始化容量等于0 时,使用共享空数组实例给底层数组变量赋值,注意,此时添加第一个元素时不会把容量扩充为10,只有内部数组是默认空数组实例才会把容量初始化为10。

    包含指定集合的构造方法

/**
 * 构造一个列表,该列表包含指定集合的元素,其顺序由集合的迭代器返回。
 */
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();//集合转数组,不同集合接口子类实现方式不同
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652) 存在数组类型不是 Object[] 的情况
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.空数组使用共享空数组实例
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

        有代码可知,传入的集合对象转换的数组对象,如果该数组对象大小等于0,该列表内部数组变量赋值为共享空数组实例,否则,该列表内部数组变量赋值为转换的数组对象,而且保证类型为 Object[]

        顺序和迭代器的关系还没搞明白。

    2.2 添加操作

        平时用到的有:列尾添加一个指定元素,在指定位置添加一个指定元素,添加指定集合的所有元素;还有一个在指定位置添加指定集合所有元素形式,暂时没用过。

    列尾添加一个指定元素

/**
 * 将指定的元素追加到此列表的末尾。
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {//确保内部容量足够
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {//计算容量,主要是将空参构造方法创建的列表初始化容量设置为10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {//确定一个精确的容量,最小容量大于内部数组大小时要扩容
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
/**
 * 增加容量以确保它至少可以容纳最小容量参数指定的元素数量。
 * 增量为原来大小的一半
 *  如果增加后的新容量小于期望的最小容量,则新容量值应该为最小容量
 *  如果增加后的新容量大于常量最大数组大小,则调用巨大容量方法返回值作为新容量值
 */
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:    
    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;
}
/**
 * 要分配的最大数组大小。 一些虚拟机在数组中保留一些头字。
 * 尝试分配更大的阵列可能会导致OutOfMemoryError:请求的阵列大小超出VM限制
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    在指定位置添加一个指定元素

/**
 * 将指定的元素插入此列表中的指定位置。
 * 将当前在该位置的元素(如果有)和任何后续元素右移(将其索引加1)。
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public void add(int index, E element) {
    rangeCheckForAdd(index);//首先检查下标是否越界,越界就要抛出约定的异常

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);//把指定位置及其后的元素右移一位
    elementData[index] = element;//把指定元素放到指定位置
    size++;
}
/**
 * add和addAll使用的rangeCheck版本。
 */
private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

    添加指定集合的所有元素

/**
 * 按照指定集合的Iterator返回的顺序,将指定集合中的所有元素追加到此列表的末尾。
 * 如果在操作进行过程中修改了指定的集合,则此操作的行为是不确定的。 
 *(这意味着如果指定的集合是此列表,并且此列表是非空的,则此调用的行为是不确定的。)
 * @return <tt>true</tt> if this list changed as a result of the call
 * @throws NullPointerException if the specified collection is null
 */
public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew); //异常会在此方法抛出
    size += numNew;
    return numNew != 0;//调用后列表发生变化返回true
}

    在指定位置添加指定集合

/**
 * 从指定位置开始,将指定集合中的所有元素插入此列表。
 * 将当前位于该位置的元素(如果有)和任何后续元素右移(增加其索引)。
 * 新元素将按照指定集合的迭代器返回的顺序显示在列表中。
 * @return <tt>true</tt> if this list changed as a result of the call
 * @throws IndexOutOfBoundsException {@inheritDoc}
 * @throws NullPointerException if the specified collection is null
 */
public boolean addAll(int index, Collection<? extends E> c) {
    rangeCheckForAdd(index);

    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount

    int numMoved = size - index;
    if (numMoved > 0)
        System.arraycopy(elementData, index, elementData, index + numNew,
                         numMoved);

    System.arraycopy(a, 0, elementData, index, numNew);
    size += numNew;
    return numNew != 0;
}

        有个疑问,在addAll 方法中,指定的集合转换成成数组后默认是 Object[] 类型,和构造方法中的指定集合转成数组有些不同,没有考虑转换的数组存在类型不是 Object [] 的可能,这个暂时没搞明白。

    2.3 查询操作

        这里先看一下根据指定下标查询元素,根据指定元素查询下标这两类方法,迭代器查询后面单独了解。

    根据下标查询

/**
 * 返回此列表中指定位置的元素。
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E get(int index) {
    rangeCheck(index);

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

    根据元素查询

/**
 * 返回指定元素在此列表中首次出现的索引如果此列表不包含该元素,则返回-1。
 * More formally, returns the lowest index <tt>i</tt> such that
 * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>,
 * or -1 if there is no such index.
 */
public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
} //在查找下标时,从0 开始由小到大遍历,返回首次出现的下标值;对 null 值使用 == 判断是否是指定对象,其他对象使用 equals 方法来判断是否是同一个对象。
/**
 * 返回指定元素在此列表中最后一次出现的索引;如果此列表不包含该元素,则返回-1。
 * More formally, returns the highest index <tt>i</tt> such that
 * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>,
 * or -1 if there is no such index.
 */
public int lastIndexOf(Object o) {
    if (o == null) {
        for (int i = size-1; i >= 0; i--)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = size-1; i >= 0; i--)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}在查找下标时,从size-1 开始由大到小遍历,返回首次出现的下标值;
/**
 * 如果此列表包含指定的元素,则返回 true 。
 * More formally, returns <tt>true</tt> if and only if this list contains
 * at least one element <tt>e</tt> such that
 * <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>.
 */
public boolean contains(Object o) {
    return indexOf(o) >= 0; //调用 indexOf 方法,根据返回值是否大于等于0 来判断是否包含指定元素,不包含时 indexOf 返回 -1 小于 0 返回 false
}

    2.4 修改操作

/**
 * 用指定的元素替换此列表中指定位置的元素。返回被替换的元素
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E set(int index, E element) {
    rangeCheck(index);
    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

    2.5 删除操作

    删除指定位置元素

/**
 * 删除此列表中指定位置的元素。 将所有后续元素向左移动(其索引减1)。
 * 返回被删除的元素
 * @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; // clear to let GC do its work

    return oldValue;
}

    删除指定元素

/**
 * 如果存在指定元素,则从该列表中删除该元素的第一次出现,返回 true。
 * 如果列表不包含该元素,则它保持不变,返回 false。
 */
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;
}
/*
 * 私有 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
}

        该方法那边调用了私有的删除方法 fastRemove(int index) ,该方法与删除指定位置元素的方法区别就是 不需边界校验(遍历调用),也不需返回已删除的值,其他逻辑一致。

    清空列表

/**
 * 从此列表中删除所有元素。 该调用返回后,该列表将为空。
 */
public void clear() {
    modCount++;

    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}//内部数组每个位置都置为 null,但是数组大小没有改变;列表大小置为 0

    删除指定范围的元素

/**
 * 从此列表中删除索引在 fromIndex(包括)和 toIndex(不包括)之间的所有元素。
 * 将所有后续元素向左移动(减少其索引)。 此调用通过(toIndex-fromIndex)元素来缩短列表。 
 * (如果 toIndex == fromIndex},则此操作无效。)
 *
 * @throws IndexOutOfBoundsException if {@code fromIndex} or
 *         {@code toIndex} is out of range
 *         ({@code fromIndex < 0 ||
 *          fromIndex >= size() ||
 *          toIndex > size() ||
 *          toIndex < fromIndex})
 */
protected void removeRange(int fromIndex, int toIndex) {
    modCount++;
    int numMoved = size - toIndex;
    System.arraycopy(elementData, toIndex, elementData, fromIndex,
                     numMoved);//异常在此方法抛出

    // clear to let GC do its work
    int newSize = size - (toIndex-fromIndex);
    for (int i = newSize; i < size; i++) {
        elementData[i] = null;
    }
    size = newSize;
}

    删除包含的指定集合元素

/**
 * 从此列表中删除指定集合中包含的所有元素。
 * 因调用此方法使列表改变就返回true
 * @throws ClassCastException if the class of an element of this list
 *         is incompatible with the specified collection
 * (<a href="Collection.html#optional-restrictions">optional</a>)
 * @throws NullPointerException if this list contains a null element and the
 *         specified collection does not permit null elements 疑问 1
 * (<a href="Collection.html#optional-restrictions">optional</a>),
 *         or if the specified collection is null
 * @see Collection#contains(Object)
 */
public boolean removeAll(Collection<?> c) {
    Objects.requireNonNull(c); //检查指定集合是否为空
    return batchRemove(c, false);
}
public static <T> T requireNonNull(T obj) {
    if (obj == null)
        throw new NullPointerException();
    return obj;
}

private boolean batchRemove(Collection<?> c, boolean complement) {
    final Object[] elementData = this.elementData;
    int r = 0, w = 0;
    boolean modified = false;
    try {
        for (; r < size; r++)//遍历数组
            if (c.contains(elementData[r]) == complement) // 此处解答 疑问 1
                elementData[w++] = elementData[r];// 存放指定集合不包含的元素(complement = false
    } finally {
        // Preserve behavioral compatibility with AbstractCollection, 疑问 2,哪种行为?
        // even if c.contains() throws. 
        if (r != size) {//c.contains() 出现异常跳出循环才会符合条件
            System.arraycopy(elementData, r,
                             elementData, w,
                             size - r);
            w += size - r;
        }
        if (w != size) {//满足条件表示有元素包含在指定集合中,要删除,列表发生改变
            // clear to let GC do its work
            for (int i = w; i < size; i++)
                elementData[i] = null;
            modCount += size - w;
            size = w;
            modified = true;
        }
    }
    return modified;
}

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!