一、前言
站在巨人的肩膀上,本系列的Java集合框架文章参考了skywang12345——Java 集合系列,真心讲的很好,可以参考。但是不足的是,时间过于长久了,大佬的文章是基于JDK1.6.0_45,对于现在来说至少都用JDK 8.0以上了,而且JDK 6.0与JDK 8.0中集合框架的改动挺大的,所以本系列的文章是基于JDK_1.8.0_161进行说明的。
二、介绍
我们先来看看ArrayList的类定义,以及继承关系
java.util.Collection<E>
-> 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 是一个数组队列,相当于动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口。
- ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
- ArrayList 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。稍后,我们会比较List的“快速随机访问”和“通过Iterator迭代器访问”的效率。
- ArrayList 实现了Cloneable接口,即覆盖了函数clone(),能被克隆。
- ArrayList 实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。
再来从整体看一下ArrayList与Collection关系,如下图:
ArrayList包含了两个重要的对象:elementData 和 size。
(1) elementData 是"Object[]类型的数组",它保存了添加到ArrayList中的元素。实际上,elementData是个动态数组,我们能通过构造函数ArrayList(int initialCapacity)来执行它的初始容量为initialCapacity;如果通过不含参数的构造函数ArrayList()来创建ArrayList,则elementData默认是一个空数组。elementData数组的大小会根据ArrayList容量的增长而动态的增长,具体的增长方式,请参考源码分析中的ensureCapacity()函数。
(2) size 则是动态数组的实际大小。
三、解析
对源码的解析,我个人喜欢从方法的使用上去看,而不是把源码所有的方法从头到尾的都去看一遍,除非是那种比较短的源码还行,如果是上千行代码的话,我觉得会很崩溃的。这样不仅学习效率底下,而且看源码本身就是一件非常枯燥的事情,容易看着看着就不想看下去了(大佬忽略),非常打击学习源码的兴趣。
1、要了解一个类,首先我们要去了解的就是它的构造方法。那么我们就先从ArrayList的构造方法开刀。ArrayList它有3个构造方法。
//默认的ArrayList构造函数。把elementData设置为一个空数组
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// ArrayList带容量大小的构造函数。
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//如果initialCapacity初始容量大于0,则新建一个数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//初始容量为0,把elementData是·初始化成一个空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}
//创建一个包含collection的ArrayList
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
//如果包含collection的ArrayList大小不为0,拷贝数据到elementData中
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
//否则,初始化一个空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
在构造方法中出现了几个字段:elementData、EMPTY_ELEMENTDATA、DEFAULTCAPACITY_EMPTY_ELEMENTDATA等等,那我们就去看看这些字段的定义。
//共享的空数组实例。
private static final Object[] EMPTY_ELEMENTDATA = {};
//一个用于共享的空数组实例,初始化一个默认的空数组,这里要和EMPTY_ELEMENTDATA区分,以便在添加第一个元素时知道要填充多少
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//对于上面两个空数组而言,感觉作用差不多,为什么会这样设计?从3个构造方法中可以看到,无参数的构造方法创建实例用到了
//DEFAULTCAPACITY_EMPTY_ELEMENTDATA,而有参数的构造方法用到的是EMPTY_ELEMENTDATA。在JDK 1.7时只有EMPTY_ELEMENTDATA,
//JDK 1.8就用DEFAULTCAPACITY_EMPTY_ELEMENTDATA代替了EMPTY_ELEMENTDATA,EMPTY_ELEMENTDATA是为了优化创建ArrayList空实例时,
//产生不必要的空数组,使得所有ArrayList空实例都指向同一个空数组。
//真正存放ArrayList数据的数组
transient Object[] elementData;
2、构造方法看完了,那我们就只有根据ArrayList的使用方法,来继续了解它的源码,要使用ArrayList首先需要添加数据,所以我们就从添加数据的add方法下手。ArrayList添加数据有两种添加方式。
//添加元素到ArrayList末尾位置
public boolean add(E e) {
//第一次添加元素,此时size=0,所以传递过去的参数是1
ensureCapacityInternal(size + 1); // Increments modCount!!
//把添加的元素放到最后
elementData[size++] = e;
return true;
}
//添加元素到ArrayList的指定位置
public void add(int index, E element) {
//检查index是否越界或者合法
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
//拷贝数据
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
//大小+1
size++;
}
private void ensureCapacityInternal(int minCapacity) {
//如果是第一次添加数据,calculateCapacity方法返回的就是10,接着看ensureExplicitCapacity方法
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//计算容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//如果elementData是空数组的话,就对比传递进来的minCapacity和DEFAULT_CAPACITY=10的最大值
//由于是第一次新增,所以Math.max(1,10) = 10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
//修改次数+1
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
//先获取当前elementData数组容量,即当前数组的长度
int oldCapacity = elementData.length;
//新的容量是:当前容量+当前容量/2(即扩容至原始容量的1.5倍)
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//拷贝数组到新的容量
elementData = Arrays.copyOf(elementData, newCapacity);
}
在add(E e)方法中遇到一个字段size,去看看定义此变量的地方,说明size表示ArrayList中元素的个数;DEFAULT_CAPACITY表示调用无参数的构造方法时,ArrayList的默认容量;
下面具体说说添加元素的add方法调用过程:
①. 当我们调用add方法首次添加元素时,不管是调用哪种方法添加,都会调用ensureCapacityInternal方法,调用add(int index, E element),只是在最开始判断了以下index是否越界合法,并且多了一个数据拷贝的操作。
②. ensureCapacityInternal这个方法很简单,只是调用了ensureExplicitCapacity方法。
③. ensureExplicitCapacity方法参数中调用了calculateCapacity计算容量的方法。
④. calculateCapacity方法先判断了elementData数组是否等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA,这里就和调用无参数的构造方法有关联了,如果等于的话就会返回一个默认的初始容量10。
⑤. 进入ensureExplicitCapacity方法,首先是对修改次数的modCount变量+1,注意这个变量很重要,在添加、删除等很多地方都会有它的身影,后面再具体说明这个modCount。接着会进入一个if判断,这里肯定是true,调用grow方法,把初始容量10传递进去。
⑥. 进入grow方法,首先会获取当前容量,然后声明了一个新的容量是当前容量,再加上当前容量一半的值,这里也就说明了当ArrayList容量不足时,会自动扩容到原始容量的1.5倍,最后调用Arrays.copyOf进行数组元素的拷贝,这里就是说明了当调用无参数的构造方法时,会初始化一个默认容量为10的数组,并且扩容是原始容量的1.5倍。
当第二次调用add方法,此时数组的真实元素长度只有1个,即size=1,而数组的长度为10,传递到ensureCapacityInternal里的参数就是2,可以自己去看接下来是什么逻辑。也可以看看当调用其他构造方法后,add操作的逻辑。而对于addAll方法,由于它是添加的一个Collection集合,所以需要先把这个集合转化成数组,然后再根据这个数组的大小来调整是否需要扩容,也是调用的ensureCapacityInternal方法,这里就不再说明。
3、接下来我们看看删除元素的操作。
//根据下标删除,并返回此元素
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;
}
//删除指定的元素
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;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
//如果没有被移除
if (numMoved > 0)
//数组拷贝
System.arraycopy(elementData, index+1, elementData, index,numMoved);
//把数组最后一项置为null,同时长度-1,使得GC可以进行垃圾回收
elementData[--size] = null; // clear to let GC do its work
}
//根据范围进行批量删除
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;
}
//根据一个Collection集合进行删除
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
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)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
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;
}
删除操作有4种,removeAll(Collection<?> c)这个方法用的很少,就不再做说明了,有兴趣的可以自己去看看。下面具体说明方法的调用过程:
使用remove(Object o):
①. 当o为null时,这也就说明ArrayList是可以添加null的。然后循环遍历数组并调用fastRemove方法,fastRemove方法中判断此元素是否已经被删除,如果没有就先对数组进行拷贝,把数组最后一个元素置空,这样就可以让GC进行回收。
②. 当o不为null时,先遍历判断o是否和数组中的某项元素相等,如果有相等则操作和为null时是一样的,如果没有相等的元素,则会删除失败返回false。
使用remove(int index):
首先会进行下标检查,然后获取此下标的元素,并判断此下标的元素是否已经被删除了,如果没有的话,进行数组拷贝,然后把数组最后一个元素置空,并size-1,最后返回这个下标的元素。
使用removeRange(int fromIndex, int toIndex): 首先会对这个范围的数据进行拷贝,然后把对数组元素进行遍历置空,最后把数组的长度重新赋值。总的来说,ArrayList删除操作相对简单。
4、继续了解ArrayList获取元素的get操作和修改元素的set操作。
//通过下标获取元素
public E get(int index) {
//检查下标是否越界
rangeCheck(index);
return elementData(index);
}
@SuppressWarnings("unchecked")
E elementData(int index) {
//返回元素
return (E) elementData[index];
}
//更新某个下标的元素
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
从上面的代码可以看出,获取元素以及更新元素并不复杂。首先获取元素时检测下标,再直接返回数组index位置的元素;更新数据也是先检测下标,取出当前位置的元素,再直接替换原来的元素后返回旧的元素。
5、ArrayList的迭代器
对于ArrayList的迭代,我们可以使用:for、foreach、iterator。如果对列表数据修改操作时,是会出问题的,甚至造成崩溃,我们在平时也会遇到这种需求。
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
System.out.println("-------------------------");
for (String str : list) {
System.out.println(str);
}
}
}
我们使用命令行javap -c -l Main.class编译class文件后,会得到以下Main.class文件中的内容。可以看到在使用for循环进行遍历时,没有调用Iterator相关的方法,所以在for循环中进行添加、删除等操作时不会造成ConcurrentModificationException异常,但会引发其他的问题,这里就不展开了;但是在使用foreach进行遍历时,我们看到下面的第78行,会调用到java/util/List.iterator:()Ljava/util/Iterator;方法进行遍历,当我们在foreach遍历中进行添加/删除元素,就会产生ConcurrentModificationException异常,可以自己尝试一下。当然直接用iterator进行迭代的同时修改数据,也会导致ConcurrentModificationException异常,这个异常就和我们之前说的modCount有关了。
C:\Users\Administrator\Desktop\新建文件夹>javap -c -l Main.class
Compiled from "Main.java"
public class Main {
public Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: ldc #4 // String a
11: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
16: pop
17: aload_1
18: ldc #6 // String b
20: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
25: pop
26: aload_1
27: ldc #7 // String c
29: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
34: pop
35: iconst_0
36: istore_2
37: iload_2
38: aload_1
39: invokeinterface #8, 1 // InterfaceMethod java/util/List.size:()I
44: if_icmpge 69
47: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
50: aload_1
51: iload_2
52: invokeinterface #10, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
57: checkcast #11 // class java/lang/String
60: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
63: iinc 2, 1
66: goto 37
69: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
72: ldc #13 // String -------------------------
74: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
77: aload_1
78: invokeinterface #14, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
83: astore_2
84: aload_2
85: invokeinterface #15, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
90: ifeq 113
93: aload_2
94: invokeinterface #16, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
99: checkcast #11 // class java/lang/String
102: astore_3
103: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
106: aload_3
107: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
110: goto 84
113: return
}
接下来我们就来看看iterator()迭代方法。
//直接返回了Itr对象
public Iterator<E> iterator() {
return new Itr();
}
//Itr实现了Iterator接口
private class Itr implements Iterator<E> {
//下一个要返回的元素的索引
int cursor;
int lastRet = -1;
//这里对expectedModCount进行赋值
int expectedModCount = modCount;
Itr() {}
//如果下一个元素的索引不等于ArrayList长度,说明还没有指向最后一个元素
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
//ConcurrentModificationException异常检测
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
//删除元素
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
//当modCount修改次数不等于所期望的修改次数时,就抛出异常。
//这里能引起modCount变化的,就是我们的add、remove、clear等等相关操作
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
//省略其他方法
......
}
ArrayList有专门用于迭代的迭代器——listIterator()。它有2个重载的方法。都直接返回了一个ListItr对象。
public ListIterator<E> listIterator(int index) {
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: "+index);
return new ListItr(index);
}
public ListIterator<E> listIterator() {
return new ListItr(0);
}
private class ListItr extends Itr implements ListIterator<E> {
ListItr(int index) {
super();
cursor = index;
}
//是否有前一个元素
public boolean hasPrevious() {
return cursor != 0;
}
//返回下一个元素的索引
public int nextIndex() {
return cursor;
}
//返回前一个元素的索引
public int previousIndex() {
return cursor - 1;
}
//返回前一个元素
@SuppressWarnings("unchecked")
public E previous() {
checkForComodification();
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;
return (E) elementData[lastRet = i];
}
//更新数据
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.set(lastRet, e);
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
//添加数据
public void add(E e) {
checkForComodification();
try {
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
可以看到ListItr继承了Itr类,实现了ListIterator接口,而ListIterator接口也是继承了Iterator接口。并且我们也看到了在ListItr类中有add、set等操作,这就说明我们在迭代的同时可以进行数据的新增和修改操作,还有获取上一个元素、上一个元素的索引等新方法,同样也有删除等其他操作,所以在ArrayList中进行遍历的话,可以使用listIterator()方法。
6、ArrayList其他常用的方法。
//从数组头部,查询某个元素的下标索引
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;
}
//没有找到此元素,直接返回-1
return -1;
}
//返回数组中是否包含某个元素,此时调用的是indexOf方法
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
//从数组的尾部,查询某个元素的下标索引
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;
}
//没有找到此元素,直接返回-1
return -1;
}
//清空数组列表
public void clear() {
modCount++;
//遍历数组,把全部元素至为null,使得GC可以进行垃圾回收
for (int i = 0; i < size; i++)
elementData[i] = null;
//把长度至为0
size = 0;
}
//判断列表是否是空数组
public boolean isEmpty() {
return size == 0;
}
//转化成Object数组,注意调用此方法把ArrayList列表转化成数组时,此方法可能会抛出ClassCastException类型转化异常
//如果需要转化成特定的类型请使用toArray(T[] a)方法
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
//把ArrayList列表转化成数组,代码都很简单,就是对数据的拷贝
@SuppressWarnings("unchecked")
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;
}
以上就是ArrayList中常用的几个方法了,至于其余方法就不再说明,有兴趣的可以自己去看看。
四、总结
1、ArrayList实际上是通过一个动态数组去保存数据的。当我们构造ArrayList时;若使用默认构造函数,在首次添加数据时,ArrayList会初始化默认容量为10。
2、当ArrayList容量不足以容纳全部元素时,ArrayList会重新设置容量:新的容量=原始容量 + 原始容量 / 2。即原始容量的1.5倍。
3、ArrayList的克隆函数,即是将全部元素克隆到一个数组中。
4、ArrayList实现java.io.Serializable的方式。当写入到输出流时,先写入“容量”,再依次写入“每一个元素”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。
5、ArrayList不是线程安全的,从ArrayList方法中我们没有看到任何的锁以及同步块,所以它不是线程安全的。