集合——ArrayList源码
ArrayList ,基于 []
数组实现的,支持自动扩容的动态数组。
可以从图中看出,ArrayList实现了4个接口,继承了一个抽象类
每个类它所实现的接口、继承的抽象类都是有它这么做的意义:
java.util.List
接口,提供数组的添加、删除、修改、迭代遍历等操作。java.util.RandomAccess
接口,表示 ArrayList 支持快速的随机访问。java.io.Serializable
接口,表示 ArrayList 支持序列化的功能。java.lang.Cloneable
接口,表示 ArrayList 支持克隆。
java.util.AbstractList
抽象类,提供了 List 接口的骨架实现,大幅度的减少了实现迭代遍历相关操作的代码。
属性:
elementData
属性:元素数组。其中,图中红色空格代表我们已经添加元素,白色空格代表我们并未使用。size
属性:数组大小。注意,size
代表的是 ArrayList 已使用elementData
的元素的数量,对于开发者看到的#size()
也是该大小。并且,当我们添加新的元素时,恰好其就是元素添加到elementData
的位置(下标)。当然, ArrayList 真正的大小是elementData
的大小。
/** * 元素数组。 * * 当添加新的元素时,如果该数组不够,会创建新数组,并将原数组的元素拷贝到新数组。之后,将该变量指向新数组。 * * The array buffer into which the elements of the ArrayList are stored. * The capacity of the ArrayList is the length of this array buffer. Any * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA * will be expanded to DEFAULT_CAPACITY when the first element is added. */ transient Object[] elementData; // non-private to simplify nested class access 不使用 private 修复,方便内嵌类的访问。 /** * 已使用的数组大小 * * The size of the ArrayList (the number of elements it contains). * * @serial */ private int size;
构造方法:
ArrayList 一共有三个构造方法:
1.ArrayList(int initialCapacity)根据传入的初始化容量,创建 ArrayList 数组。如果我们在使用时,如果预先指到数组大小,一定要使用该构造方法,可以避免数组扩容提升性能,同时也是合理使用内存。
/** * 共享的空数组对象。 * * 在 {@link #ArrayList(int)} 或 {@link #ArrayList(Collection)} 构造方法中, * 如果传入的初始化大小或者集合大小为 0 时,将 {@link #elementData} 指向它。 * * Shared empty array instance used for empty instances. */ private static final Object[] EMPTY_ELEMENTDATA = {}; public ArrayList(int initialCapacity) { // 初始化容量大于 0 时,创建 Object 数组 if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; // 初始化容量等于 0 时,使用 EMPTY_ELEMENTDATA 对象 } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; // 初始化容量小于 0 时,抛出 IllegalArgumentException 异常 } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } }
- 比较特殊的是,如果初始化容量为 0 时,使用
EMPTY_ELEMENTDATA
空数组。在添加元素的时候,会进行扩容创建需要的数组。
2.ArrayList(Collection<? extends E> c)
使用传入的 c
集合,作为 ArrayList 的 elementData。
public ArrayList(Collection<? extends E> c) { // 将 c 转换成 Object 数组 elementData = c.toArray(); // 如果数组大小大于 0 if ((size = elementData.length) != 0) { // defend against c.toArray (incorrectly) not returning Object[] // (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652) // <X> 如果集合元素不是 Object[] 类型,则会创建新的 Object[] 数组,并将 elementData 赋值到其中,最后赋值给 elementData 。 if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); // 如果数组大小等于 0 ,则使用 EMPTY_ELEMENTDATA 。 } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } }
- 比较让人费解的是,在
<X>
处的代码。它是用于解决 JDK-6260652 的 Bug 。它在 JDK9 中被解决,😈 也就是说,JDK8 还会存在该问题。
触发 JDK-6260652 的测试代码,然后分别在 JDK8 和 JDK13 下执行。代码如下:
public static void test02() { List<Integer> list = Arrays.asList(1, 2, 3); Object[] array = list.toArray(); // JDK8 返回 Integer[] 数组,JDK9+ 返回 Object[] 数组。 System.out.println("array className :" + array.getClass().getSimpleName()); // 此处,在 JDK8 和 JDK9+ 表现不同,前者会报 ArrayStoreException 异常,后者不会。 array[0] = new Object(); }
JDK8 执行如下图所示:
JDK13 执行如下图所示:
- 在 JDK8 中,返回的实际是
Integer []
数组,那么我们将 Object 对象设置到其中,肯定是会报错的。具体怎么修复的,看 JDK-6260652 的最末尾一段(Only use .toArray(Object[]))?????。
3.ArrayList()
使用最多的构造方法:
/** * 默认初始化容量 * * Default initial capacity. */ private static final int DEFAULT_CAPACITY = 10; /** * 共享的空数组对象,用于 {@link #ArrayList()} 构造方法。 * * 通过使用该静态变量,和 {@link #EMPTY_ELEMENTDATA} 区分开来,在第一次添加元素时。 * * Shared empty array instance used for default sized empty instances. We * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when * first element is added. */ private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /** * Constructs an empty list with an initial capacity of ten. */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
- 在我们学习 ArrayList 的时候,一直被灌输了一个概念,在未设置初始化容量时,ArrayList 默认大小为 10 。但是此处,我们可以看到初始化为
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
这个空数组。这是为什么呢?ArrayList 考虑到节省内存,一些使用场景下仅仅是创建了 ArrayList 对象,实际并未使用。所以,ArrayList 优化成初始化是个空数组,在首次添加元素时,才真正初始化为容量为 10 的数组。 - 那么为什么单独声明了
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
空数组,而不直接使用EMPTY_ELEMENTDATA
呢?在下文中,我们会看到DEFAULTCAPACITY_EMPTY_ELEMENTDATA
首次扩容为 10 ,而EMPTY_ELEMENTDATA
按照 1.5 倍扩容从 0 开始而不是 10 。😈 两者的起点不同,嘿嘿。
添加单个元素:
add(E e)
方法,顺序添加单个元素到数组。代码如下:
@Override public boolean add(E e) { // <1> 增加数组修改次数 modCount++; // 添加元素 add(e, elementData, size); // 返回添加成功 return true; } private void add(E e, Object[] elementData, int s) { // <2> 如果容量不够,进行扩容 if (s == elementData.length) elementData = grow(); // <3> 设置到末尾 elementData[s] = e; // <4> 数量大小加一 size = s + 1; }
<1>
处,增加数组修改次数modCount
。在父类 AbstractList 上,定义了modCount
属性,用于记录数组修改次数。<2>
处,如果元素添加的位置就超过末尾(数组下标是从 0 开始,而数组大小比最大下标大 1),说明数组容量不够,需要进行扩容,那么就需要调用grow()
方法,进行扩容。<3>
处,设置到末尾。<4>
处,数量大小加一。
add(int index, E element)
方法,插入单个元素到指定位置。代码如下:
public void add(int index, E element) { // 校验位置是否在数组范围内 rangeCheckForAdd(index); // 增加数组修改次数 modCount++; // 如果数组大小不够,进行扩容 final int s; Object[] elementData; if ((s = size) == (elementData = this.elementData).length) elementData = grow(); // 将 index + 1 位置开始的元素,进行往后挪 System.arraycopy(elementData, index, elementData, index + 1, s - index); // 设置到指定位置 elementData[index] = element; // 数组大小加一 size = s + 1; } private void rangeCheckForAdd(int index) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
数组扩容:
重点!!!不同构造初始化第一次扩容时:
在调用ArrayList(int initialCapacity)
初始化或调ArrayList()初始化,在第一次添加元素的时候都会调用扩容方法
(PS:此时,它们第一次的扩容方式不同:
前者一般情况下是1.5倍扩容, 特殊情况1.构造传参为0时使用的是最小长度1,另一种特殊情况,添加元素大于0.5倍长度的元素时扩容会大于1.5;
后者第一次初始化如果添加元素长度小于默认长度10,则直接使用10作为数组长度,如果大于10则直接扩容至刚到放下所有元素的长度,之后才1.5倍扩容,同样也不是绝对1.5倍扩容也存在上述第二种特殊情况)
grow()
方法,扩容数组,并返回它。整个的扩容过程,首先创建一个新的更大的数组,一般是 1.5 倍大小(为什么说是一般呢,稍后会看到,会有一些小细节),然后将原数组复制到新数组中,最后返回新数组。代码如下:
private Object[] grow() { // <1> return grow(size + 1); } private Object[] grow(int minCapacity) { int oldCapacity = elementData.length; // <2> 如果原容量大于 0 ,或者数组不是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,计算新的数组大小,并创建扩容 if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, /* minimum growth */ oldCapacity >> 1 /* preferred growth */); return elementData = Arrays.copyOf(elementData, newCapacity); // <3> 如果是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 数组,直接创建新的数组即可。 } else { return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; } }
<1>
处,调用#grow(int minCapacity)
方法,要求扩容后至少比原有大 1 。因为是最小扩容的要求,实际是允许比它大。<2>
处,如果原容量大于 0 时,又或者数组不是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,则计算新的数组大小,并创建扩容。ArraysSupport#newLength(int oldLength, int minGrowth, int prefGrowth)
方法,计算新的数组大小。简单来说,结果就是Math.max(minGrowth, prefGrowth) + oldLength
,按照minGrowth
和prefGrowth
取大的。- 一般情况下,从
oldCapacity >> 1
可以看处,是 1.5 倍扩容。但是会有两个特殊情况:1)初始化数组要求大小为 0 的时候,0 >> 1
时(>> 1 为右移操作,相当于除以 2
)还是 0 ,此时使用minCapacity
传入的 1 。2)在下文中,我们会看到添加多个元素,此时传入的minCapacity
不再仅仅加 1 ,而是扩容到elementData
数组恰好可以添加下多个元素,而该数量可能会超过当前 ArrayList 0.5 倍的容量。
<3>
处,如果是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
数组,直接创建新的数组即可。思考下,如果无参构造方法使用EMPTY_ELEMENTDATA
的话,无法实现该效果了。
既然有数组扩容方法,那么是否有缩容方法呢?在 trimToSize()
方法中,会创建大小恰好够用的新数组,并将原数组复制到其中。代码如下:
public void trimToSize() { // 增加修改次数 modCount++; // 如果有多余的空间,则进行缩容 if (size < elementData.length) { elementData = (size == 0) ? EMPTY_ELEMENTDATA // 大小为 0 时,直接使用 EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); // 大小大于 0 ,则创建大小为 size 的新数组,将原数组复制到其中。 } }
同时,提供 ensureCapacity(int minCapacity)
方法,保证 elementData
数组容量至少有 minCapacity
。代码如下:
public void ensureCapacity(int minCapacity) { if (minCapacity > elementData.length // 如果 minCapacity 大于数组的容量 && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA && minCapacity <= DEFAULT_CAPACITY)) { // 如果 elementData 是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的时候, // 需要最低 minCapacity 容量大于 DEFAULT_CAPACITY ,因为实际上容量是 DEFAULT_CAPACITY 。 // 数组修改次数加一 modCount++; // 扩容 grow(minCapacity); } }
- 比较简单,可以将这个方法理解成主动扩容。
添加多个元素:
addAll(Collection<? extends E> c)
方法,批量添加多个元素。在明确知道会添加多个元素时,推荐使用该该方法而不是添加单个元素,避免可能多次扩容。代码如下:
public boolean addAll(Collection<? extends E> c) { // 转成 a 数组 Object[] a = c.toArray(); // 增加修改次数 modCount++; // 如果 a 数组大小为 0 ,返回 ArrayList 数组无变化 int numNew = a.length; if (numNew == 0) return false; // <1> 如果 elementData 剩余的空间不够,则进行扩容。要求扩容的大小,至于能够装下 a 数组。 Object[] elementData; final int s; if (numNew > (elementData = this.elementData).length - (s = size)) elementData = grow(s + numNew); // <2> 将 a 复制到 elementData 从 s 开始位置 System.arraycopy(a, 0, elementData, s, numNew); // 数组大小加 numNew size = s + numNew; return true; }
<1>
处,如果elementData
剩余的空间不足,则进行扩容。要求扩容的大小,至于能够装下a
数组。在 前面「数组扩容」 ,已经知道,如果要求扩容的空间太小,则扩容 1.5 倍。<2>
处,将a
复制到elementData
从s
开始位置。
总的看下来,就是 add(E e)
方法的批量版本,优势就正如前面说的,避免可能多次扩容。
addAll(int index, Collection<? extends E> c)
方法,从指定位置开始插入多个元素。代码如下:
public boolean addAll(int index, Collection<? extends E> c) { // 校验位置是否在数组范围内 rangeCheckForAdd(index); // 转成 a 数组 Object[] a = c.toArray(); // 增加数组修改次数 modCount++; // 如果 a 数组大小为 0 ,返回 ArrayList 数组无变化 int numNew = a.length; if (numNew == 0) return false; // 如果 elementData 剩余的空间不够,则进行扩容。要求扩容的大小,至于能够装下 a 数组。 Object[] elementData; final int s; if (numNew > (elementData = this.elementData).length - (s = size)) elementData = grow(s + numNew); // 【差异点】如果 index 开始的位置已经被占用,将它们后移 int numMoved = s - index; if (numMoved > 0) System.arraycopy(elementData, index, elementData, index + numNew, numMoved); // 将 a 复制到 elementData 从 s 开始位置 System.arraycopy(a, 0, elementData, index, numNew); // 数组大小加 numNew size = s + numNew; return true; }
移除单个元素:
remove(int index)
方法,移除指定位置的元素,并返回该位置的原元素。代码如下:
public E remove(int index) { // 校验 index 不要超过 size Objects.checkIndex(index, size); final Object[] es = elementData; // 记录该位置的原值 @SuppressWarnings("unchecked") E oldValue = (E) es[index]; // <X>快速移除 fastRemove(es, index); // 返回该位置的原值 return oldValue; }
重点是 <X>
处,调用 #fastRemove(Object[] es, int i)
方法,快速移除。代码如下:
private void fastRemove(Object[] es, int i) { // 增加数组修改次数 modCount++; // <Y>如果 i 不是移除最末尾的元素,则将 i + 1 位置的数组往前挪 final int newSize; if ((newSize = size - 1) > i) // -1 的原因是,size 是从 1 开始,而数组下标是从 0 开始。 System.arraycopy(es, i + 1, es, i, newSize - i); //newSize - i 是 移动的元素的个数,从(i+1)开始的(newSize - i)元素前移
// 将新的末尾置为 null ,帮助 GC es[size = newSize] = null; }
<Y>
处,看起来比较复杂,胖友按照“如果 i 不是移除最末尾的元素,则将 i + 1 位置的数组往前挪”来理解,就很好懂了。
remove(Object o)
方法,移除首个为 o
的元素,并返回是否移除到。代码如下:
public boolean remove(Object o) { final Object[] es = elementData; final int size = this.size; // <Z> 寻找首个为 o 的位置 int i = 0; found: { if (o == null) { // o 为 null 的情况 for (; i < size; i++) if (es[i] == null) break found; } else { // o 非 null 的情况 for (; i < size; i++) if (o.equals(es[i])) break found; } // 如果没找到,返回 false return false; } // 快速移除 fastRemove(es, i); // 找到了,返回 true return true; }
- 和
remove(int index)
差不多,就是在<Z>
处,改成获得首个为o
的位置,之后就调用fastRemove(Object[] es, int i)
方法,快速移除即可。
移除多个元素:
removeRange(int fromIndex, int toIndex)
方法,批量移除 [fromIndex, toIndex)
的多个元素,注意不包括 toIndex
的元素噢。代码如下:
protected void removeRange(int fromIndex, int toIndex) { // 范围不正确,抛出 IndexOutOfBoundsException 异常 if (fromIndex > toIndex) { throw new IndexOutOfBoundsException( outOfBoundsMsg(fromIndex, toIndex)); } // 增加数组修改次数 modCount++; // <X> 移除 [fromIndex, toIndex) 的多个元素 shiftTailOverGap(elementData, fromIndex, toIndex); } private static String outOfBoundsMsg(int fromIndex, int toIndex) { return "From Index: " + fromIndex + " > To Index: " + toIndex; }
<X>
处,调用 #shiftTailOverGap(Object[] es, int lo, int hi)
方法,移除 [fromIndex, toIndex)
的多个元素。代码如下:
private void shiftTailOverGap(Object[] es, int lo, int hi) { // 将 es 从 hi 位置开始的元素,移到 lo 位置开始。 System.arraycopy(es, hi, es, lo, size - hi); // 将从 [size - hi + lo, size) 的元素置空,因为已经被挪到前面了。 for (int to = size, i = (size -= hi - lo); i < to; i++) es[i] = null; }
- 和
#fastRemove(Object[] es, int i)
方法一样的套路,先挪后置null
。 - 有一点要注意,ArrayList 特别喜欢把多行代码写成一行。所以,可能会有疑惑,貌似这里没有修改数组的大小
size
啊?答案在i = (size -= hi - lo)
,简直到精简到难懂。
removeAll(Collection<?> c)
方法,批量移除指定的多个元素。实现逻辑比较简单,但是看起来会比较绕。(PS:但是这里面采用的优化遍历技巧很实用,感觉不少场景都很适用,且面试也经常会有这种类似遍历场景)简单来说,通过两个变量 w
(写入位置)和 r
(读取位置),按照 r
顺序遍历数组(elementData
),如果不存在于指定的多个元素中,则写入到 elementData
的 w
位置,然后 w
位置 + 1 ,跳到下一个写入位置。通过这样的方式,实现将不存在 elementData
覆盖写到 w
位置。可能理解起来有点绕,当然看代码也会有点绕绕。代码如下:
boolean batchRemove(Collection<?> c, boolean complement, final int from, final int end) { // 校验 c 非 null 。 Objects.requireNonNull(c); final Object[] es = elementData; int r; // Optimize for initial run of survivors // <1> 优化,顺序遍历 elementData 数组,找到第一个不符合 complement ,然后结束遍历。 for (r = from;; r++) { // <1.1> 遍历到尾,都没不符合条件的,直接返回 false 。 if (r == end) return false; // <1.2> 如果包含结果不符合 complement 时,结束 if (c.contains(es[r]) != complement) break; } // <2> 设置开始写入 w 为 r ,注意不是 r++ 。 // r++ 后,用于读取下一个位置的元素。因为通过上的优化循环,我们已经 es[r] 是不符合条件的。 int w = r++; try { // <3> 继续遍历 elementData 数组,如何符合条件,则进行移除 for (Object e; r < end; r++) if (c.contains(e = es[r]) == complement) // 判断符合条件 es[w++] = e; // 移除的方式,通过将当前值 e 写入到 w 位置,然后 w 跳到下一个位置。 } catch (Throwable ex) { // Preserve behavioral compatibility with AbstractCollection, // even if c.contains() throws. // <4> 如果 contains 方法发生异常,则将 es 从 r 位置的数据写入到 es 从 w 开始的位置 System.arraycopy(es, r, es, w, end - r); w += end - r; // 继续抛出异常 throw ex; } finally { // <5> // 增加数组修改次数 modCount += end - w; // 将数组 [w, end) 位置赋值为 null 。 shiftTailOverGap(es, w, end); } return true; }
- 先看下每一小块的逻辑。然后,调试下,妥妥的就明白了。
complement
参数,翻译过来是“补足”的意思。怎么理解呢?表示如果elementData
元素在c
集合中时,是否保留。- 如果
complement
为false
时,表示在集合中,就不保留,这显然符合removeAll(Collection<?> c)
方法要移除的意图。 - 如果
complement
为true
时,表示在集合中,就暴露,这符合我们后面会看到的retainAll(Collection<?> c)
方法要求交集的意图。
- 如果
<1>
处,首先我们要知道这是一个基于 Optimize 优化的目的。我们是希望先判断是否elementData
没有任何一个符合c
的,这样就无需进行执行对应的移除逻辑。但是,我们又希望能够避免重复遍历,于是就有了这样一块的逻辑。总的来说,这块逻辑的目的是,优化,顺序遍历elementData
数组,找到第一个不符合complement
,然后结束遍历。<1.1>
处,遍历到尾,都没不符合条件的,直接返回false
。也就是说,丫根就不需要进行移除的逻辑。<1.2>
处,如果包含结果不符合complement
时,结束循环。可能有点难理解,我们来举个例子。假设elementData
是[1, 2, 3, 1]
时,c
是[2]
时,那么在遍历第 0 个元素1
时,则c.contains(es[r]) != complement => false != false
不符合,所以继续缓存;然后,在遍历第 1 个元素2
时,c.contains(es[r]) != complement => true != false
符合,所以结束循环。此时,我们便找到了第一个需要移除的元素的位置。当然,移除不是在这里执行
<2>
处,设置开始写入w
为r
,注意不是r++
。这样,我们后续在循环elementData
数组,就会从w
开始写入。并且此时,r
也跳到了下一个位置,这样间接我们可以发现,w
位置的元素已经被“跳过”了。<3>
处,继续遍历elementData
数组,如何符合条件,则进行移除。可能有点难理解,我们继续上述例子。遍历第 2 个元素3
时候,c.contains(es[r]) == complement => false == false
符合,所以将3
写入到w
位置,同时w
指向下一个位置;遍历第三个元素1
时候,c.contains(es[r]) == complement => true == false
不符合,所以不进行任何操作。<4>
处,如果 contains 方法发生异常,则将es
从r
位置的数据写入到es
从w
开始的位置。这样,保证我们剩余未遍历到的元素,能够挪到从从w
开始的位置,避免多出来一些元素。<5>
处,是不是很熟悉,将数组[w, end)
位置赋值为null
。
retainAll(Collection<?> c)
方法,求 elementData
数组和指定多个元素的交集。简单来说,恰好和 removeAll(Collection<?> c)
相反,移除不在 c
中的元素。代码如下:
public boolean retainAll(Collection<?> c) { return batchRemove(c, true, 0, size); }
查找单个元素:
indexOf(Object o)
方法,查找首个为指定元素的位置。代码如下:
public int indexOf(Object o) { return indexOfRange(o, 0, size); } int indexOfRange(Object o, int start, int end) { Object[] es = elementData; // o 为 null 的情况 if (o == null) { for (int i = start; i < end; i++) { if (es[i] == null) { return i; } } // o 非 null 的情况 } else { for (int i = start; i < end; i++) { if (o.equals(es[i])) { return i; } } } // 找不到,返回 -1 return -1; }
contains(Object o)
方法,就是基于该方法实现。代码如下:
public boolean contains(Object o) { return indexOf(o) >= 0; }
有时我们需要查找最后一个为指定元素的位置,所以会使用到 #lastIndexOf(Object o)
方法。代码如下:
public int lastIndexOf(Object o) { return lastIndexOfRange(o, 0, size); } int lastIndexOfRange(Object o, int start, int end) { Object[] es = elementData; // o 为 null 的情况 if (o == null) { for (int i = end - 1; i >= start; i--) { // 倒序 if (es[i] == null) { return i; } } // o 非 null 的情况 } else { for (int i = end - 1; i >= start; i--) { // 倒序 if (o.equals(es[i])) { return i; } } } // 找不到,返回 -1 return -1; }
获得指定位置的元素:
get(int index)
方法,获得指定位置的元素。代码如下:
public E get(int index) { // 校验 index 不要超过 size Objects.checkIndex(index, size); // 获得 index 位置的元素 return elementData(index); } E elementData(int index) { return (E) elementData[index]; }
- 随机访问
index
位置的元素,时间复杂度为 O(1) 。
设置指定位置的元素:
set(int index, E element)
方法,设置指定位置的元素。代码如下:
public E set(int index, E element) { // 校验 index 不要超过 size Objects.checkIndex(index, size); // 获得 index 位置的原元素 E oldValue = elementData(index); // 修改 index 位置为新元素 elementData[index] = element; // 返回 index 位置的原元素 return oldValue; }
转换成数组:
public Object[] toArray() { return Arrays.copyOf(elementData, size); } // Arrays.java public static <T> T[] copyOf(T[] original, int newLength) { return (T[]) copyOf(original, newLength, original.getClass()); }
- 注意,返回的是
Object[]
类型噢。
实际场景下,可能想要指定 T
泛型的数组,那么就需要使用到 toArray(T[] a)
方法。代码如下:
public <T> T[] toArray(T[] a) { // <1> 如果传入的数组小于 size 大小,则直接复制一个新数组返回 if (a.length < size) // Make a new array of a's runtime type, but my contents: return (T[]) Arrays.copyOf(elementData, size, a.getClass()); // <2> 将 elementData 复制到 a 中 System.arraycopy(elementData, 0, a, 0, size); // <2.1> 如果传入的数组大于 size 大小,则将 size 赋值为 null if (a.length > size) a[size] = null; // <2.2> 返回 a return a; }
- 分成 2 个情况,根据传入的
a
数组是否足够大。 <1>
处,如果传入的数组小于size
大小,则直接复制一个新数组返回。一般情况下,不会这么干。<2>
处,将elementData
复制到a
中。<2.1>
处,如果传入的数组大于size
大小,则将size
位置赋值为null
。额,有点没搞懂这个有啥目的。<2.2>
处,返回传入的a
。很稳。
- 考虑到
<1>
处,可能会返回一个新数组,所以即使<2>
返回的就是a
数组,最好使用还是按照a = list.toArray(a)
。
克隆:
public Object clone() { try { // 调用父类,进行克隆 ArrayList<?> v = (ArrayList<?>) super.clone(); // 拷贝一个新的数组 v.elementData = Arrays.copyOf(elementData, size); // 设置数组修改次数为 0 v.modCount = 0; return v; } catch (CloneNotSupportedException e) { // this shouldn't happen, since we are Cloneable throw new InternalError(e); } }
elementData
是重新拷贝出来的新的数组,避免和原数组共享。
总结:
- ArrayList 是基于
[]
数组实现的 List 实现类,支持在数组容量不够时,一般按照 1.5 倍自动扩容。同时,它支持手动扩容、手动缩容。 - ArrayList 随机访问时间复杂度是 O(1) ,查找指定元素的平均时间复杂度是 O(n) 。
- ArrayList 移除指定位置的元素的最好时间复杂度是 O(1) ,最坏时间复杂度是 O(n) ,平均时间复杂度是 O(n) 。--------最好时间复杂度发生在末尾移除的情况。
- ArrayList 移除指定元素的时间复杂度是 O(n) 。--------因为首先需要进行查询,然后在使用移除指定位置的元素,无论怎么计算,都需要 O(n) 的时间复杂度。
- ArrayList 添加元素的最好时间复杂度是 O(1) ,最坏时间复杂度是 O(n) ,平均时间复杂度是 O(n) 。--------最好时间复杂度发生在末尾添加的情况。
对时间复杂度的计算方式可以参考以下博客:
《算法复杂度分析(上):分析算法运行时,时间资源及空间资源的消耗》
《算法复杂度分析(下):最好、最坏、平均、均摊等时间复杂度概述》