目录
一、ArrayList介绍
1.1 简介
1.2 继承体系
二、源码剖析
2.1 成员属性
2.2 构造方法
2.2.1 带int类型的构造方法:ArrayList(int initialCapacity)
2.2.2 无参构造方法:ArrayList()
2.2.3 Collection型构造方法:ArrayList(Collection c)
2.3 常用方法
2.3.1 add()方法
2.3.2 get(int index)方法
2.3.3 remove()方法
2.3.4 set()方法
2.3.5 size()方法
2.3.6 indexOf(Object o) 方法
三、ArrayList的优缺点
四、总结
一、ArrayList介绍
前言:作为一个常用的List接口实现类,日常开发过程中使用率非常高,因此有必要对其原理进行分析。
注:本文jdk源码版本为jdk1.8.0_172
1.1 简介
ArrayList 的底层是数组,相当于动态数组。默认容量为10,它具有动态扩容的能力,线程不安全,元素可以为null。
与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。
ArrayList继承于 AbstractList ,实现了 List, RandomAccess, Cloneable, java.io.Serializable 这些接口。
ArrayList简介:
- ArrayList 是从JDK1.2 引入的。
- ArrayList是可调整大小的数组,实现了List接口。 实现所有可选列表操作,并允许所有元素包括null 。 除了实现List 接口之外,该类还提供了一些方法来操纵内部使用的存储列表的数组的大小。 (这个类是大致相当于Vector,不同之处在于它是不同步的)。
- ArrayList内部封装一个数组,用数组来存储数据。内部数组的默认初始容量 10,存满后 1.5 倍增长
- 从 JDK1.8开始, ArrayList 一开始创建一个长度为 0 的数组,当添加第一个元素时再创建一个始容量为 10 的 数组。
1.2 继承体系
java.lang.Object
↳ java.util.AbstractCollection<E>
↳ java.util.AbstractList<E>
↳ java.util.ArrayList<E>
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
- ArrayList实现了List, RandomAccess, Cloneable, java.io.Serializable等接口。
- ArrayList实现了List,提供了基础的添加、删除、遍历等操作。
- ArrayList实现了RandomAccess,提供了随机访问的能力。RandomAccess 是一个标志接口,表明实现这个接口的 List 集合是支持快速随机访问的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。
- ArrayList实现了Cloneable,即覆盖了函数clone(),可以被克隆。
- ArrayList实现了Serializable,这意味着ArrayList支持序列化,能通过序列化去传输。
二、源码剖析
2.1 成员属性
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// 序列化 id
private static final long serialVersionUID = 8683452581122892189L;
// 默认容量的大小为 10,也就是通过new ArrayList()创建时的默认容量。
private static final int DEFAULT_CAPACITY = 10;
// 空实例共享的一个空数组。如果通过new ArrayList(0)传入的容量为0时会使用这个空数组。
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认的空数组常量为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,
// 这个空数组是通过new ArrayList()创建时使用的,与EMPTY_ELEMENTDATA的区别是在添加第一个元素时使用这个空数组的会初始化为DEFAULT_CAPACITY(10)个元素,添加第一个元素的时候会重新初始为默认容量大小。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 真正存放元素的地方,存放元素的数组。
// 使用transient是为了不序列化这个字段,transient表示不可序列化
// 至于没有使用private修饰,后面注释是写的“为了简化嵌套类的访问”,但是实测加了private嵌套类一样可以访问。
// private表示是类私有的属性,只要是在这个类内部都可以访问,嵌套类或者内部类也是在类的内部,所以也可以访问类的私有成员。
transient Object[] elementData; // non-private to simplify nested class access
// 元素的数量,默认是0。存储真正元素的个数,而不是elementData数组的长度。
private int size;
......
// 最大数组容量为 Integer.MAX_VALUE – 8
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}
注意:被标记为transient的属性在对象被序列化的时候不会被保存,也就是它不会被序列化。
elementData设置成了transient,那ArrayList是怎么把元素序列化的呢?
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// 防止序列化期间有修改
int expectedModCount = modCount;
// 写出非transient非static属性(会写出size属性)
s.defaultWriteObject();
// 写出元素个数
s.writeInt(size);
// 依次写出元素
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
// 如果有修改,抛出异常
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// 声明为空数组
elementData = EMPTY_ELEMENTDATA;
// 读入非transient非static属性(会读取size属性)
s.defaultReadObject();
// 读入元素个数,没什么用,只是因为写出的时候写了size属性,读的时候也要按顺序来读
s.readInt();
if (size > 0) {
// 计算容量
int capacity = calculateCapacity(elementData, size);
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
// 检查是否需要扩容
ensureCapacityInternal(size);
Object[] a = elementData;
// 依次读取元素到数组中
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
查看writeObject()方法可知,先调用s.defaultWriteObject()方法,再把size写入到流中,再把元素一个一个的写入到流中。
一般地,只要实现了Serializable接口即可自动序列化,writeObject()和readObject()是为了自己控制序列化的方式,这两个方法必须声明为private,在java.io.ObjectStreamClass#getPrivateMethod()方法中通过反射获取到writeObject()这个方法。
在ArrayList的writeObject()方法中先调用了s.defaultWriteObject()方法,这个方法是写入非static非transient的属性,在ArrayList中也就是size属性。同样地,在readObject()方法中先调用了s.defaultReadObject()方法解析出了size属性。
elementData定义为transient的优势,自己根据size序列化真实的元素,而不是根据数组的长度序列化元素,减少了空间占用。
2.2 构造方法
2.2.1 带int类型的构造方法:ArrayList(int initialCapacity)
传入初始容量:
- 如果传入参数也就是初始容量大于0,则新建一个初始容量大小的数组;
- 如果传入的参数初始容量等于0,将EMPTY_ELEMENTDATA赋值给 elementData;
- 如果传入的参数初始容量小于0,将抛出异常。
/**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
public ArrayList(int initialCapacity) {
// 如果初始容量大于0,就新建一个数组存储元素
if (initialCapacity > 0) {
// 新建一个初始容量大小的数组
this.elementData = new Object[initialCapacity];
// 如果初始容量为0,使用空数组EMPTY_ELEMENTDATA
} else if (initialCapacity == 0) {
// 将EMPTY_ELEMENTDATA赋值给 elementData
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 如果传入初始容量小于0,则抛出异常 “非法的容量”
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
}
}
2.2.2 无参构造方法:ArrayList()
不传初始容量,初始化为DEFAULTCAPACITY_EMPTY_ELEMENTDATA空数组,会在添加第一个元素的时候扩容为默认的大小,即10。
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
// 没有指定初始容量,将成员变量elementData的值设为默认值
// 使用这个数组是在添加第一个元素的时候会扩容到默认大小10
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
2.2.3 Collection<? extends E>型构造方法:ArrayList(Collection<? extends E> c)
传入集合并初始化elementData,这里会使用拷贝把传入集合的元素拷贝到elementData数组中,如果元素个数为0,则初始化为EMPTY_ELEMENTDATA空数组。
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*
* 把传入集合的元素初始化到ArrayList中
*/
public ArrayList(Collection<? extends E> c) {
// 将参数中的集合转换为数组,赋值给 elementData
elementData = c.toArray();
// 如果数组的大小不等于 0
if ((size = elementData.length) != 0) {
// 检查c.toArray()返回的是不是Object[]类型,如果不是,重新拷贝成Object[].class类型
// c.toArray might (incorrectly) not return Object[] (see 6260652)
// 如果c.toArray()返回的数组类型不是Object[]类型的
// 则利用Arrays.copyOf()创建一个大小为size的、类型为Object[]的数组, 并将elementData中的元素复制到新的数组中
// 最后让elementData 指向新的数组
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
// 否则设置元素数组为空数组
} else {
// replace with empty array.
// 如果c的空集合,则初始化为空数组EMPTY_ELEMENTDATA
this.elementData = EMPTY_ELEMENTDATA;
}
}
总结:
- 将参数中的集合转换为数组,赋值给 elementData
- 给size进行赋值,size代表集合元素数量。判断参数是否为空,如果数组的大小不等于 0,则进一步判断是否转化为Object类型的数组,如果不是,则进行复制;
- 否则,设置元素数组为空数组
为什么c.toArray();返回的有可能不是Object[]类型呢?请看下面的代码:
public class ArrayTest {
public static void main(String[] args) {
Father[] fathers = new Son[]{};
// 打印结果为class [com.coolcoding.code.Son]
System.out.println(fathers.getClass());
List<String> strList = new MyList();
// 打印结果为class [java.lang.String]
System.out.println(strList.toArray().getClass());
}
}
class Father {}
class Son extends Father {}
class MyList extends ArrayList<String> {
/**
* 子类重写父类的方法,返回值可以不一样
* 但这里只能用数组类型,换成Object就不行
* 应该算是java本身的bug
*/
@Override
public String[] toArray() {
// 为了方便举例直接写死
return new String[]{"1", "2", "3"};
}
}
2.3 常用方法
2.3.1 add()方法
- add(E e) 方法 将指定的元素追加到此集合的末尾。平均时间复杂度为O(1)。
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
// 确认容量,插入元素之前,会检查是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 将元素添加到数组中最后一个元素的后面,最后将集合中实际的元素个数加 1
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
// 进一步确认ArrayList的容量,看是否需要进行扩容
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果elementData为空,则返回默认容量和minCapacity中的最大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 否则直接返回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;
// 扩容,相当于扩大为原来的1.5倍
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);
}
其实add方法整体逻辑还是比较简单。主要注意扩容条件:只要插入数据后整个数组中存储的数据数量size比此时的数组长度大就会进行扩容。因此如果在循环中使用ArrayList时需要特别小心,避免频繁扩容造成OOM异常。
流程:
(1)检查是否需要扩容;
(2)如果elementData等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA则初始化容量大小为DEFAULT_CAPACITY;
(3)新容量是老容量的1.5倍(oldCapacity + (oldCapacity >> 1)),如果加了这么多容量发现比需要的容量还小,则以需要的容量为准;
(4)创建新容量的数组并把老数组拷贝到新数组;
- add(int index, E element) 在集合元素序列 index 位置处插入。平均时间复杂度为O(n)。
/**
* Inserts the specified element at the specified position in this
* list. Shifts the element currently at that position (if any) and
* any subsequent elements to the right (adds one to their indices).
*
* @param index index at which the specified element is to be inserted
* @param element element to be inserted
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
// 检查下标索引是否越界
rangeCheckForAdd(index);
// 插入元素之前,会检查是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 将 index 位置及其后面的所有元素都向后移一位,将index位置空出来
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 将新元素插入到 index 位置处,元素个数加 1
elementData[index] = element;
// 元素个数自增
size++;
}
// 抛出越界异常
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
整体逻辑简单:越界检查 -> 确认容量 -> 元素后移 -> 插入元素。
流程:
(1)检查索引是否越界;
(2)检查是否需要扩容;
(3)把插入索引位置后的元素都往后挪一位;
(4)在插入索引位置放置插入的元素;
(5)大小加1;
- addAll(Collection<? extends E> c)方法,将集合c中所有元素添加到当前ArrayList中。就相当于求两个集合的并集,会保留重复的元素。
public boolean addAll(Collection<? extends E> c) {
// 将集合c转为数组
Object[] a = c.toArray();
int numNew = a.length;
// 检查是否需要扩容
ensureCapacityInternal(size + numNew);
// 将c中元素全部拷贝到数组的最后
System.arraycopy(a, 0, elementData, size, numNew);
// 大小增加c的大小
size += numNew;
// 如果c不为空就返回true,否则返回false
return numNew != 0;
}
流程:
(1)拷贝c中的元素到数组a中;
(2)检查是否需要扩容;
(3)把数组a中的元素拷贝到elementData的尾部;
2.3.2 get(int index)方法
获取指定索引位置的元素,时间复杂度为O(1)。
public E get(int index) {
// 检查下标是否越界
rangeCheck(index);
// 返回数组index位置的元素
return elementData(index);
}
private void rangeCheck(int index) {
// 索引值大于数组大小
if (index >= size)
// 抛出异常
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
E elementData(int index) {
return (E) elementData[index];
}
流程:
(1)检查索引是否越界,这里只检查是否越上界,如果越上界抛出IndexOutOfBoundsException异常,如果越下界抛出的是ArrayIndexOutOfBoundsException异常。
(2)返回索引位置处的元素;
2.3.3 remove()方法
- remove(int index) 方法是删除该集合中指定位置的元素,时间复杂度为O(1)。
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices).
*
* @param index the index of the element to be removed
* @return the element that was removed from the list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
// 检查下标索引是否越界
rangeCheck(index);
// 修改次数自增
modCount++;
// 返回被删除的元素值
E oldValue = elementData(index);
// 需要移动的元素的个数
int numMoved = size - index - 1;
// 如果index不是最后一位,则将index之后的元素往前挪一位
if (numMoved > 0)
// 将 index + 1 及其后面的元素向前移动一位,覆盖被删除的元素值
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 把数组最后一个元素赋值为null,将元素个数减一,帮助GC
elementData[--size] = null; // clear to let GC do its work
// 返回旧值
return oldValue;
}
remove逻辑还是比较简单,但是这里需要注意一点是ArrayList在remove的时候,并没有进行缩容。
流程:
(1)检查索引是否越界;
(2)获取指定索引位置的元素;
(3)如果删除的不是最后一位,则其它元素往前移一位;
(4)将最后一位置为null,方便GC回收;
(5)返回删除的元素。
可以看到,ArrayList删除元素的时候并没有缩容。
- remove(Object o) 方法是删除指定的元素,如果有重复的元素,则只会删除下标最小的元素,时间复杂度为O(n)。
/**
* Removes the first occurrence of the specified element from this list,
* if it is present. If the list does not contain the element, it is
* unchanged. More formally, removes the element with the lowest index
* <tt>i</tt> such that
* <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>
* (if such an element exists). Returns <tt>true</tt> if this list
* contained the specified element (or equivalently, if this list
* changed as a result of the call).
*
* @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) {
// 如果被移除元素为null
if (o == null) {
// 遍历整个数组,找到元素第一次出现的位置,并将其快速删除
for (int index = 0; index < size; index++)
// 如果要删除的元素为null,则以null进行比较,使用==
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
// 遍历整个数组,找到元素第一次出现的位置,并将其快速删除
for (int index = 0; index < size; index++)
// 如果要删除的元素不为null,则进行比较,使用equals()方法
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
// 方法是快速删除,删除跳过边界检查的方法,也不返回删除的元素值
private void fastRemove(int index) {
// 少了一个越界的检查
modCount++;
// 如果index不是最后一位,则将index之后的元素往前挪一位
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
// 将最后一个元素删除,帮助GC
elementData[--size] = null; // clear to let GC do its work
}
流程:
(1)找到第一个等于指定元素值的元素;
(2)快速删除;
fastRemove(int index)相对于remove(int index)少了检查索引越界的操作,因为源码中是通过循环遍历数组找的下标,不可能存在越界情况,可见jdk将性能优化到极致。
- retainAll(Collection<?> c)方法,求两个集合的交集。
public boolean retainAll(Collection<?> c) {
// 集合c不能为null
Objects.requireNonNull(c);
// 调用批量删除方法,这时complement传入true,表示删除不包含在c中的元素,这样就求出交集了
return batchRemove(c, true);
}
/**
* 批量删除元素
* complement为true表示删除c中不包含的元素
* complement为false表示删除c中包含的元素
*/
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
// 使用读写两个指针同时遍历数组
// 读指针每次自增1,写指针放入元素的时候才加1
// 这样不需要额外的空间,只需要在原有的数组上操作就可以了
int r = 0, w = 0;
boolean modified = false;
try {
// 遍历整个数组,如果c中包含该元素,则把该元素放到写指针的位置(以complement为准)
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// 正常来说r最后是等于size的,除非c.contains()抛出了异常
if (r != size) {
// 如果c.contains()抛出了异常,则把未读的元素都拷贝到写指针之后
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// 将写指针之后的元素置为空,帮助GC
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
// 新大小等于写指针的位置(因为每写一次写指针就加1,所以新大小正好等于写指针的位置)
size = w;
modified = true;
}
}
// 有修改返回true
return modified;
}
将集合与另一个集合求交集,整体逻辑比较简单的。通过读写指针进行操作,不用额外空间。注意complement为true,则将包含在c中的元素写入相应位置。这样就求出了交集,这里还要注意finally中的操作,异常与置空操作。
流程:
(1)遍历elementData数组;
(2)如果元素在c中,则把这个元素添加到elementData数组的w位置并将w位置往后移一位;
(3)遍历完之后,w之前的元素都是两者共有的,w之后(包含)的元素不是两者共有的;
(4)将w之后(包含)的元素置为null,方便GC回收;
- removeAll(Collection<?> c)求两个集合的单方向差集,只保留当前集合中不在c中的元素,本质就是讲List中c集合中的元素都删除。
public boolean removeAll(Collection<?> c) {
// 集合c不能为空
Objects.requireNonNull(c);
// 同样调用批量删除方法,这时complement传入false,表示保存不在c中的元素,这样就求出差集了
return batchRemove(c, false);
}
逻辑和retainAll刚好相反,complement为false,保存不包含在C中的元素,这样就求出差集了,注意这里是单向差集。
2.3.4 set()方法
set(int index, E element) 方法将index索引处的元素替换成element对象,返回被替换的旧元素。
/**
* Replaces the element at the specified position in this list with
* the specified element.
*
* @param index index of the element to replace
* @param element element to be stored at the specified position
* @return the element previously at the specified position
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E set(int index, E element) {
// 检查下标索引是否越界
rangeCheck(index);
// 指定下标处的旧值
E oldValue = elementData(index);
// 指定下标处赋新的值
elementData[index] = element;
// 返回旧值
return oldValue;
}
2.3.5 size()方法
返回集合中的元素个数。
/**
* Returns the number of elements in this list.
*
* @return the number of elements in this list
*/
public int size() {
return size;
}
2.3.6 indexOf(Object o) 方法
返回对象o在集合中第一次出现的位置的索引。
/**
* Returns the index of the first occurrence of the specified element
* in this list, or -1 if this list does not contain the element.
* More formally, returns the lowest index <tt>i</tt> such that
* <tt>(o==null ? get(i)==null : 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++)
// 如果 i 位置的原素为空
if (elementData[i]==null)
// 返回下标
return i;
// 否则,查找的元素不为空
} else {
// 循环遍历数组
for (int i = 0; i < size; i++)
// 找到第一个和要查找的元素相等的元素
if (o.equals(elementData[i]))
// 返回下标
return i;
}
// 返回 -1.则表示没有找到
return -1;
}
三、ArrayList的优缺点
1. 优点
- 能够自动扩容,默认每次扩容为原来容量大小的1.5倍。
- 根据下标遍历元素,效率高。
- 根据下标访问元素,效率高。
2. 缺点
- 线程不安全。
- 在中间插入或删除元素的时候需要移动元素,效率低。
四、总结
- ArrayList的底层数据结构为数组(数组是一组连续的内存空间),默认容量为10,线程不安全,可以存储null值;
- ArrayList扩容条件,只要增加容量大于现有容量就会进行扩容,扩容量为原来的1.5倍,但是ArrayList不会进行缩容;
- ArrayList支持随机访问,通过索引访问元素极快,时间复杂度为O(1);
- ArrayList添加元素到尾部极快,平均时间复杂度为O(1);
- ArrayList添加元素到中间比较慢,因为要搬移元素,平均时间复杂度为O(n);
- ArrayList从尾部删除元素极快,时间复杂度为O(1);
- ArrayList从中间删除元素比较慢,因为要搬移元素,平均时间复杂度为O(n);
- ArrayList支持求并集,调用addAll(Collection<? extends E> c)方法即可;
- ArrayList中有求交集(retainAll)和求差集(removeAll),注意这里的差集是单向交集;
- ArrayList支持求交集,调用retainAll(Collection<? extends E> c)方法即可;
- ArrayList支持求单向差集,调用removeAll(Collection<? extends E> c)方法即可;