一文浅析ArrayList
简介
ArrayList底层为动态数组结构,用一个Object数组存储数据,当数组容量不够时,会实现动态扩容机制。且增删慢,查找快。
实现了RandomAccess接口,具备快速随机访问的能力,因此遍历时,相比较迭代器而言,用for循环通过下标遍历更加快速(当数据量多到一定程度)。
实现了Iterable接口,支持用迭代器的方式访问元素。实现了Serializable接口,内部提供序列化和反序列化方法。
另外,ArrayList可以存储null,插入的元素允许重复且是有序的,非线程安全等。
ArrayList基础
ArrayList基本变量
//默认容量为10
private static final int DEFAULT_CAPACITY = 10;
//传入参数为0时,构造的数组。
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认构造时的数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// The array buffer into which the elements of the ArrayList are stored.
//真正存放数据的数组缓冲区,(缓冲区)则说明会有预留空间,不是直接存满,用transient修饰与序列化有关
transient Object[] elementData;
//数组最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//包含的元素个数(注意不是数组的长度)
private int size;
构造方法
public ArrayList() {
//不传入数组大小,默认空数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//传入0,也是空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
}
}
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[]
//如果传入集合转化后不是Object[]类型,size不为0时,转化成Object数组
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
//size为0,返回空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
此处有个小彩蛋,那就是第三个构造方法的toArray(),返回的就是Object[],为啥还需要判断是否是Object[].class类型呢?
具体需结合下面toArray()方法及另一篇blog,因内容有点牵扯到别的,故另写一篇,没放在ArrayList中。欲知详情,请看简述Arrays中的小彩蛋。
add()
public boolean add(E e) {
//先确保size+1后不会超出数组容量,否则需扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//修改对应位置的元素,赋值后将size+1
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
// 1.先调用calculateCapacity,计算当前数组所需容量;
// 2.再根据需要容量与其实际容量比较,看是否需扩容。
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//如果数组是空的,说明还没初始化,这是第一次添加,则返回默认容量(10)和size+1的最大值,即10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
//否则,说明数组已有长度,直接返回size+1,说明数组需要size+1的长度
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
//此方法用来判断当size+1时,所需要的的容量是否大于数组实际容量,即是否需要扩容。
//添加元素,数组结构变化,modCount要加1
modCount++;
//到此说明数组所需容量大于实际容量,需扩容
if (minCapacity - elementData.length > 0)
//扩容参数为:所需的最小容量
grow(minCapacity);
}
public void add(int index, E element) {
//先判断index是否小于0或越界
rangeCheckForAdd(index);
//同上,判断所需容量是否大于现有容量
ensureCapacityInternal(size + 1); // Increments modCount!!
//将elementData中从index开始,共size-index个元素,拷贝到elementData中index+1位置,(可理解为替换或覆盖),因上面已检查所需容量,所以这不会发生越界。
System.arraycopy(elementData, index, elementData, index + 1,size - index);
//赋值,将index位置赋值为element
elementData[index] = element;
size++;
}
1. 这里主要要理解System.arrayCopy方法,它是System类的一个native方法,主要作用就是将数组元素拷贝到另一个数组里去。
2. add(E element)方法因为是直接添加到数组末尾,因此不需要System.arrayCopy()方法进行一个数据的拷贝。而add(int index,E element)方法可能是从中间添加元素,所以需移动元素。
3. 另ArrayList是可以存放null的,且不止一个。
addAll()
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;
}
addAll方法,通过System.arrayCopy将转化成Object[]的集合直接添加到原集合的末尾,可用于求两集合的并集(未去重),返回值需判断添加的集合中参数个数是否为0.
注意:如果如下,b是为true的,因为存放2个null,元素个数为2,而存放一个null会抛出NPE。
List<Integer> asList = Arrays.asList(null, null);
boolean b = list.addAll(asList);
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];
}
1.get(int index)方法比较简单,因为ArrayList是实现了RandomAccess接口的,可以直接根据下标获取元素,虽然rangeCheck判断条件是>=size,但如果index小于0,在下一个方法还是会抛异常。
remove()
public E remove(int index) {
//检查下标
rangeCheck(index);
modCount++;
//先存储旧的值
E oldValue = elementData(index);
//计算需要移动的元素的个数,为0则说明删除的是最后一个元素,不用移动。
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,numMoved);
//移动后size减1,且该位置元素最后一个元素置null帮助GC
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
public boolean remove(Object o) {
//判断要删除的元素否为null
if (o == null) {
//直接根据下标遍历删除,删除成功返回true,
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;
}
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
}
1. 可以看到,类似add方法,remove方法也需要移动元素,因此ArrayList虽然随机访问较快,但增删较慢。
grow()
private void grow(int minCapacity) {
//该方法参数为存储当前所有元素所需要的容量
//先得到旧的容量
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:
//扩容后将元素copy到新的数组中
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;
}
1. ArrayList的扩容方法,默认扩充为原来容量的1.5倍,以此实现了动态数组的结构。
2.另外,在这的几个判断,在stackOverFlow上找到一篇有趣的提问,感兴趣的可以看看,大意就是说if(a - b < 0)和if(a < b)的区别。Difference between if (a - b < 0) and if (a < b)
toArray()
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
// 创建一个新的运行时类型的数组,而不是自存存储时的数组。
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
@Test
void testToArray() {
Object[] array = list.toArray();
Assertions.assertEquals(Object[].class, array.getClass());
Integer[] array1 = list.toArray(new Integer[0]);
Assertions.assertEquals(Integer[].class, array1.getClass());
Assertions.assertThrows(ArrayStoreException.class, ()->list.toArray(new String[0]));
}
toArray()方法主要用于将集合转化为数组,一般情况下可能第一种方便一点,这里返回的是Object[]类型,要注意不能向下强转。
实际建议还是用第二种较好,可以直接转成存放的数据类型的数组。另外注意toArray()方法尽管返回的是Object[],有时却不一定是Object[]类型,且该方法也用到到了System.arraycopy()和Arrays.copyOf(),两者还是有区别的,因篇幅原因,就不放在本篇了,详情可看简述Arrays中的小彩蛋。
removeAll()和retainAll()
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);
}
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)
// 主要覆盖操作,加上for循环中的判断非常经典
elementData[w++] = elementData[r];
} finally {
// even if c.contains() throws.
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
// 最后置null,帮助GC,且重新赋值size、modCount等
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;
}
两个方法都需用到batchRemove(),仅仅是传入的complement不同,故放在一起对比讲解。
这里主要要弄懂try中的for循环,需要多debug几次才行。我们可以先把complement去掉,默认为true,假设步骤如下:(下标从0开始)
1.c包含元素,进行值得覆盖,覆盖后w为1.r一直随for循环递增。
2.c不包含,跳过,r继续递增。
3.c包含,覆盖,此时w还是1,r一直是原数组下标。
意味着,当两集合有共有的元素时,就会就行值的覆盖,且是从0开始的(w只有覆盖了才会递增),而r一直递增,所以原集合中的每个元素都会判断到,可以理解为共有的元素整体往前挪,且从0开始挪。
到最后,w!=size,w可以为共有元素的个数。再从w开始,将后面元素置null帮助GC,留下的就是两集合中的交集。
同理:当complment为false时,就是不包含的元素就会挪动,相当于求两集合的差集。两方法如下图所示:
另外,可以看到finally中还有一个r与size的判断,主要为了防止contains抛出异常,通过Collection类中了解到主要是NPE和ClassCastException。抛出异常后进行一个数组的copy,再判断w。
序列化机制
ArrayList是实现了Serializable接口的,表明它可以被序列化,可是用来存储数据的Object[] 却用了transient修饰。我们来看它内部提供的相关序列化方法:
private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException{
int expectedModCount = modCount;
// 先调用默认的序列化方法
s.defaultWriteObject();
// 将size序列化
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;
// 先调用默认的反序列化方法
s.defaultReadObject();
// 反序列化size
s.readInt();
if (size > 0) {
//基于size实际存储的大小计算是否需扩容
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();
}
}
}
可以看到,ArrayList内部提供了序列化的方法,因为ArrayList中的数组长度是变化的,实际存储时,不一定每次都存满了(即size不一定等于length),因此,用transient修饰,则可以保证值序列化实际存储的元素,而不是整个数组,避免浪费时间和空间。
我们知道序列化如果没有重写writeObject方法的话,默认是会调用ObjectOutputStream中的defaultWriteObject方法的,如果重写了就利用反射直接调用重写的方法。
可是这既然重写了writeObject方法,内部还要调用默认的序列化方法呢?
need of defaultReadObject() and defaultWriteObject()?
不仅要调用,而且还要放在前面调用,有助于实现向后的兼容性。因为该方法主要用于读取和写入类的非transient修饰的字段。如果将来向类中添加一些没用transient修饰的字段,并且试图用旧版本的类反序列化它,那么defaultReadObject()方法可以忽略新添加的字段,类似地,如果用新版本反序列化旧的序列化对象,则新的非transient字段将从JVM获取默认值。(对象为null,boolean为false…等)
遍历
ArrayList有4种遍历方式:
public class ArrayListTest {
public static int NUM = 10000000;
public static ArrayList<Integer> list = new ArrayList<Integer>() {{
for (int i = 0; i < NUM; i++) {
add(i);
}
// list.add(1); 如此写会造成空指针异常
}};
@Test
void testGetByIndex() {
Integer value;
int size = list.size();
long start = System.nanoTime();
for (int j = 0; j < size; j++) {
value = list.get(j);
}
long end = System.nanoTime();
System.out.println("Get by index :" + (end - start));
}
@Test
void testGetByFor() {
Integer value;
long start = System.nanoTime();
for (Integer integer : list) {
value = integer;
}
long end = System.nanoTime();
System.out.println("Get by for :" + (end - start));
}
@Test
void testGetByIterator() {
Iterator<Integer> iterator = list.iterator();
Integer value;
long start = System.nanoTime();
while (iterator.hasNext()) {
value = iterator.next();
}
long end = System.nanoTime();
System.out.println("Get by iterator :" + (end - start));
}
@Test
void testGetByForEach() {
final Integer[] i = new Integer[1];
long start = System.nanoTime();
list.forEach(p -> i[0] = p);
long end = System.nanoTime();
System.out.println("Get by forEach :" + (end - start));
}
}
遍历方式 | NUM=10 | NUM = 10 000 | NUM = 10 000 000 |
---|---|---|---|
直接通过下标获取 | 11465 | 1923252 | 48856746 |
通过增强for循环遍历 | 5129 | 2009547 | 64140772 |
通过迭代器遍历 | 29872 | 3893875 | 90126997 |
通过Java8的forEach遍历 | 626399 | 3215275 | 64065640 |
说明:
1. 用匿名内部类初始化时注意别写成list.add()方法,否则会造成NPE。
2. 利用下标获取时记得size()方法别放在for里,否则会非常影响性能。
3. 可以看出到后面通过下标获取是最有效率的。
4. 删除元素只能在iterator中或用for循环逆序删除。
迭代器
说到遍历,还得说下ArrayList内部有2种迭代器实现:Itr和ListItr。
具体代码就不贴了,二者都是ArrayList的内部类,都实现了Iterator接口,而ListItr与Itr主要区别如下:
-
ListItr只有一个带参构造,需传入迭代位置的下标,可以从任意位置开始迭代;而Itr只有无参构造,即从数组下标0开始迭代
-
ListItr有hasPrevious()和previous()方法,可以向前迭代;而Itr只能向后迭代。
-
ListItr有nextIndex()和previousIndex()方法,可以知道当前迭代的下标位置;而Itr则不能。
ArrayListSpliterator
ArrayListSpliterator为jdk1.8新增的可分割迭代器,可以用来并行遍历元素,虽然也是迭代器的一种,但是实现的是Spliterator接口。
Spliterator接口为jdk1.8新增接口,在Iterator接口中有默认方法实现,(目前还在学习中,故代码就不全贴了,附上测试代码帮助练习,最好还是能debug跟着一步一步看,这样更加清晰)。
public Spliterator<E> spliterator() {
return new ArrayListSpliterator<>(this, 0, -1, 0);
}
static final class ArrayListSpliterator<E> implements Spliterator<E> {
private final ArrayList<E> list; //用于存放ArrayList对象
private int index; // 起始位置
private int fence; // 结束位置,-1 为到了最后一个元素
private int expectedModCount; // 存放结构改变次数
ArrayListSpliterator(ArrayList<E> list, int origin, int fence,int expectedModCount) {
this.list = list;
this.index = origin;
this.fence = fence;
this.expectedModCount = expectedModCount;
}
//第一次使用时,初始化结束位置。
private int getFence() { // initialize fence to size on first use
int hi; // (a specialized variant appears in method forEach)
ArrayList<E> lst;
// 刚传进来为-1
if ((hi = fence) < 0) {
//list 为空则为0
if ((lst = list) == null)
hi = fence = 0;
else {
//否则,大小为list.size
expectedModCount = lst.modCount;
hi = fence = lst.size;
}
}
return hi;
}
//对单个元素执行给定的动作,执行成功则返回true
public boolean tryAdvance(Consumer<? super E> action) {
if (action == null)
throw new NullPointerException();
//第一次调用会初始化fence
int hi = getFence(), i = index;
// i < hi 说明还没到迭代器尾部,即还有元素
if (i < hi) {
index = i + 1; //索引+1并取出元素
@SuppressWarnings("unchecked") E e = (E)list.elementData[i];
action.accept(e);
if (list.modCount != expectedModCount)
throw new ConcurrentModificationException();
return true;
}
return false;
}
// 对每个剩余元素执行给定动作,依次处理
public void forEachRemaining(Consumer<? super E> action) {
int i, hi, mc; // hoist accesses and checks from loop
ArrayList<E> lst; Object[] a;
if (action == null)
throw new NullPointerException();
// 给lst赋值,并判断是否有元素
if ((lst = list) != null && (a = lst.elementData) != null) {
if ((hi = fence) < 0) {
mc = lst.modCount;
hi = lst.size;
}
else
mc = expectedModCount;
// 下标>0且结束位置小于数组长度,说明还没到末尾
if ((i = index) >= 0 && (index = hi) <= a.length) {
//循环执行给定的Consumer方法
for (; i < hi; ++i) {
@SuppressWarnings("unchecked") E e = (E) a[i];
action.accept(e);
}
if (lst.modCount == mc)
return;
}
}
throw new ConcurrentModificationException();
}
//返回还未处理的元素的个数
public long estimateSize() {
return (long) (getFence() - index);
}
}
public class ArrayListTest {
public static int NUM = 10;
public static ArrayList<Integer> list = new ArrayList<Integer>() {{
for (int i = 0; i < NUM; i++) {
add(i);
}
}};
@Test
void testSpliterator(){
// 获取可分割迭代器
Spliterator<Integer> spliterator = list.spliterator();
// 计算还剩下多少个元素需要遍历
long size = spliterator.estimateSize();
Assertions.assertEquals(10, size); // size大小为10
// 输出0 到 9
spliterator.forEachRemaining(System.out::println);
boolean b = spliterator.tryAdvance(System.out::println);
Assertions.assertFalse(b); //b 为false 因上个已经迭代完成,输出方法执行失败,返回false
long size2 = spliterator.estimateSize();
Assertions.assertEquals(0, size2); // size2为0,因迭代器中已没有元素
}
}
内部类SubList
SubList为ArrayList的内部类,继承了AbstractList,实现了RandomAccess接口,通过调用subList方法返回,并且其注释说明,返回的只是一个视图,且属于左闭右开型,(即包含左小标,不包含右下标)
在其构造方法中可以看到,只是将ArrayList各属性赋值给自己的属性,并没有重新创建一个List。其包含许多特性,使用时要特别注意一下:
- 因是ArrayList内部类,且没有继承ArrayList,因此,无法强转成ArrayList。
- 对原List或subLis的非结构性改变都会影响彼此,如set()等。
- 对List做结构性改变,会影响到subList,此时subList做任何操作都会抛异常。
- 对SubList做结构性改变,会影响到原List,不会抛异常。
结构性改变:即改变了数组的长度,如add()或remove()等,使modCount发生改变的方法。
非结构性改变:类似于get()等,没有改变数组长度,即没改变modCount的方法。
/**
* Returns a view of the portion of this list between the specified
* {@code fromIndex}, inclusive, and {@code toIndex}, exclusive.
*/
public List<E> subList(int fromIndex, int toIndex) {
//入参校验,无非就是判断有没超出下标啥的
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
private final int parentOffset;
private final int offset;
int size;
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}
private void checkForComodification() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
}
public int size() {
checkForComodification();
return this.size;
}
// 其他方法就不贴了,基本操作类似于ArrayList
}
//此测试方法需用到之前初始化的list,因篇幅就不贴了,可直接从上面CV
@Test
void testSubList() {
List<Integer> subList = list.subList(0, 5);
subList.add(10);
List<Integer> array = Arrays.asList(0, 1, 2, 3, 4, 10, 5, 6, 7, 8, 9);
Assertions.assertEquals(new ArrayList<>(array), list);
ArrayListTest.list.add(1);
Assertions.assertThrows(ConcurrentModificationException.class, subList::size);
Assertions.assertThrows(ConcurrentModificationException.class,()->subList.add(1));
}
说明:subList的操作和ArrayList基本类似,从源码中可以看出,subList的每个方法都会调用checkForComodification()检查原List和subList的modCount是否相等,以上第三点即可由此说明。
总结
虽然ArrayList比较简单,相当于集合的入门级,但其中可扩展的知识点还是有的,如序列化、Arrays的相关方法、迭代等。本文通过源码从各个主要方法入手,(有些简单方法就没贴)逐步分析,有助于更加全面地了解集合底层的相关知识,在实际场景中能更加合理地选择,而不是一个ArrayList从头用到尾,能在面试中吹得比别人更久。
如此,即可。