Java源码阅读之ArrayList
1.ArrayList类介绍
用可调整数组( Resizable-array )实现接口,实现了所有对列表的操作,而且允许存放所有元素包括null。除了是实现接口之外,此类提供了一些方法来操作列表(List)的内部数组(array)的大小(size)来操作列表。这个类大致和Vector类是一样的,但它是不同步的(unsynchronized)意味着线程不安全。
size(),isEmpty(),get(),set(),iterator(),listIterator()这些操作是运行在固定时间 (constant time)。add()操作运行在分摊常量时间(amortized constant time),也就是说添加n个元素需要O(n)时间,其他所有的操作运行在线性时间(线性时间),常数因子低于LinkedList。
每个ArrayList实例都有一个(容量),容量是List中存放元素的数组的大小,容量至少和list的大小一样大(array.length>=list.size).当元素被添加到ArrayList中时,容量会自动增长。扩容策略的细节
源码解析
ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
//默认的初始化容量
private static final int DEFAULT_CAPACITY = 10;
//存储ArrayList元素的数组缓冲区,ArrayList的容量是这个数组缓冲区的长度。所有的空Arrylist的
//elementData ==DEFAULTCAPACITY_EMPTY_ELEMENTDATA(一个空数组),当第一个元素添加时,将
//将扩展到DEFAULT_CAPACITY(默认容量为10)
//被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化
transient Object[] elementData;
//ArrayList的大小(它包含的元素个数)
//注意容量和大小是两个概念
private int size;
3个构造方法:
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);
}
}
//构造一个初始容量为10的空列表。(源码注释这么说的,感绝不太对,也可能他的意思DEFAULT_CAPACITY是10,并不是数组缓冲区大小就是10)
//看上面elementData的注释,空的list因该是0,添加第一个元素时,变成10.
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//传入一个集合
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
//将这个ArrayList<实例的容量调整为列表的当前大小
//modCount是继承父类AbstractList的,结构上发生变化时++
//protected transient int modCount = 0;
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
//增加这个ArrayList实例的容量,以确保指定最小容量参数,可以容纳所有元素。
//手动扩容
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// 除了不是默认容量的空数组外任意大小的数组
? 0
//默认容量空数组(空参构造出来的)这里感觉和上面提到了默认容量是10有点关系,如果对默认容量的空数组扩容,还是默认容量10.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
//确定扩容后容量大小的数组扩容方法
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 扩容后大小(要扩大到多大)要大于现在数组长度
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//ArrayList中真正实现数组的方法
private void grow(int minCapacity) {
// 数组原长度
int oldCapacity = elementData.length;
//默认新长度扩容,原长度二进制右移一位,相当于oldCapacity/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);
}
//要分配的数组的最大大小。一些vm在数组中保留一些头字。尝试分配更大的数组可能会导致OutOfMemoryError:请求的数组大小超过VM限制
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//int的最大常量值2^31-1
@Native public static final int MAX_VALUE = 0x7fffffff;
//超大容量,返回一个能扩容的最大容量
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
//返回list中的元素个数
public int size() {
return size;
}
//list判断是否为空
public boolean isEmpty() {
return size == 0;
}
//判断是否包含元素
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
//通过循环遍历数组查询是否包含此元素并返回数组下标,没有则返回-1,注意这里是可以对null进行查找的
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;
}
//判断元素最后一次出现的位置,原理是数组的反遍历
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;
}
//返回Arraylist实例的浅拷贝(元素本身没有拷贝),(源码注释是这样写的,但是咱们说是深拷贝)
//Arrays.copyOf()是创建一个新的数组(也就是分配了一个新的内存空间),然后调用System.arraycopy()复制内容,赋值给新数组,然后返回新数组。
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
关于数组拷贝的一些细节:
Arrays.copyOf()是创建一个新的数组(也就是分配了一个新的内存空间),然后调用System.arraycopy()复制内容,赋值给新数组,然后返回新数组。
如果要赋值的数组存的基本数据类型(int、float、double等等),那么数组的每个元素存的就直接是数值,所以赋值后,得到的是个独立全新的副本,操作副本,不会影响原来的数组。
如果数组存的是对象的话,那么数组每一个元素存的就是对象的引用,所以复制后,得到了一个新的引用副本,操作副本就等于改变了原来的数组。
因为Java 虚拟机内存上就是这样分配的,栈帧中,只会直接为基本数据类型分配空间(比如 int ,float),然后直接将数据的值存在栈帧中,而对于对象,那么只会在栈帧中分配一个引用的大小,存储对象的引用就行了。
//返回一个新的对象数组,返回的数组将是“安全的”,因为没有list的引用。(换句话说,此方法必须分配新数组)。因此,调用者可以自由修改返回的数组。
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
//传一个数组,如果a数组长度大于list的size则将数组中数据copy进去,多余的填充null。注意会覆盖原数组。
//如果传入数组长度小于list大小,则创建新的数组。
//用法可以在调用时传入 new一个空数组,然后定义一个数组去接返回值。People[] bb =list2.toArray(new People[0])长度任意
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;
}
下面是一些常用操作list的方法:
get( )方法:
E elementData(int index) {
return (E) elementData[index];
}
//返回此列表中指定位置的元素
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
set( )方法:返回被替代的元素
//将此列表中指定位置的元素替换为指定元素
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
add( )方法:一个核心方法,为什么说是核心呢,add方法需要保证底层数组正确性。可以看看下面关于的源码。
//将指定的元素追加到此列表的末尾。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确保容量足够,不够会扩容
elementData[size++] = e;//放入元素
return true;
}
//首先需要保证内部容量,调用分析(从下往上)
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 计算后的容量大于本数组的长的长度(说明元素个数大于底层数组的长度,需要扩容)
if (minCapacity - elementData.length > 0)
grow(minCapacity);//详情看上面数组扩容
}
//计算出容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//elementData :该列表的数组 minCapacity:size+1(确保加一个是可以加进去的)
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {//是否为初始化空list
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
//在数组中的指定位置插入指定的元素列表。如果该位置有元素,将该元素和右面的所有元素右移一位,将指定元素插入
// 实现add的步骤 范围检查(添加的指定位置) -->确保容量 -->数组拷贝(右移) -->插入目标元素 --> 列表大小(元素个数)+1
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // 确保容量足够,不够会扩容,详情参照上方添加
System.arraycopy(elementData, index, elementData, index + 1,
size - index);//向右移动其实是进行数组拷贝,此时数字大小经过上面已经可以保证足够
elementData[index] = element;//将目标元素放入指定位置
size++;//list的大小+1
}
//本地方法,参数可以看出方法作用,想象一下复制粘贴。参数:(源数组或者说被拷贝数组,拷贝开始的位置,目标数组,粘贴开始的位置,需要拷贝的长度或者说个数)
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
//检查添加位置没有越界
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
remove( )方法:根据索引移除,和根据元素移除
//实现步骤:检查范围--> 确定需要移动元素范围--> 数组拷贝(左移)-->移除数组中多余的元素+list的大小-1-->返回移除的元素
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);
//移动完后,会多一位,即最后面的一位元素。这行代码两个作用,1.移除数组中多余的元素2.list的大小-1
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;
}
//私有移除方法,它跳过边界检查,不返回移除的值。原理和index移除基本一样
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
}
//清空数组中内容,将list的元素个数置为0
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
//实现步骤:集合转数组--> 确定容量--> 数组copy -->增加list的size
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;
}
//类似与add
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;
}
迭代器:
迭代器是一种模式、详细可见其设计模式,可以使得序列类型的数据结构的遍历行为与被遍历的对象分离,即我们无需关心该序列的底层结构是什么样子的。只要拿到这个对象,使用迭代器就可以遍历这个对象的内部
实现了Collection接口的集合类,都有一个Iterator方法,用于返回一个实现了Iterator接口的对象,用于遍历集合;(Iterator接口定义了3个方法分别是hasNext(),next(),remove();)
对List来说,你也可以通过listIterator()取得其迭代器,两种迭代器在有些时候是不能通用的,Iterator和ListIterator主要区别在以下方面:
1. iterator()方法在set和list接口中都有定义,但是ListIterator()仅存在于list接口中(或实现类中);
2. ListIterator有add()方法,可以向List中添加对象,而Iterator不能
3. ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。
4. ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。
5. 都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改。
因为ListIterator的这些功能,可以实现对LinkedList等List数据结构的操作。其实,数组对象也可以用迭代器来实现。
ArrayList中有两个内部类分别实现了ListIterator和Iterator接口
//这是它们的构造方法:
public ListIterator<E> listIterator() {
return new ListItr(0);
}
public Iterator<E> iterator() {
return new Itr();
}
下面我们看看具体的实现类:
private class Itr implements Iterator:
//An optimized version of AbstractList.Itr(源码注释说优化了父类AbstractList的迭代器)
private class Itr implements Iterator<E> {
int cursor; // index of next element to return(要返回的下一个元素的索引)
//返回最后一个元素的索引;如果没有返回,则返回-1
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;//modCount是list中数组改变次数
Itr() {}//构造方法
public boolean hasNext() {
return cursor != size;//游标不等于元素个数,说明还有元素。
}
@SuppressWarnings("unchecked")
//下面有图解
public E next() {
//调用checkForComodification()会去校验expectedModCount 和modCount是否相等,不等抛异常,
//modCount 字段作为外部类结构修改次数的记录,为子类提供快速迭代,
//使用迭代器对集合进行遍历时,不能对集合进行修改
checkForComodification();//修改次数是否一致
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
//实现的核心,游标+1,返回当前元素,设置最后一个索引为返回元素索引
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
//调用list的remove()--游标左移--lastRet置为-1--同步修改次数
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
next()方法图解:
private class ListItr extends Itr implements ListIterator :
//public interface ListIterator<E> extends Iterator<E>
//可以看出ListIterator继承了Iterator,所以可以用父类的的成员变量
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;
}
//与前面的next()相似
@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();
}
}
// 第一步,依旧是利用checkForComodification()方法对修改值modCount进行检查,判断当前数组是否发生修改。
// 第二步,获取当前的游标cursor并赋予新声明的变量i。
// 第三步,这里是极为重要的一步,就是插入动作,你也可以叫添加。它调用了ArrayList本类的add(int index, E element)方法来进行插入操作。第二步中的i就是插入的位置索引。通俗的说就是,在游标cursor处进行插入操作。
// 第四步,将当前的游标cursor进行加1操作。这步操作的目的是为了不影响游标cursor所代表的含义,即,指向某一个特定元素。你在这个位置新添加了一个元素,那么我当初游标cursor如果还在这个位置的话,是不是就指向了这个新元素?但是其本身是要指向原先你本身cursor指向的那个旧元素,怎么办呢?你就只能进行移位操作,也就是加1了。
// 第五步,我们把这个上一项元素指向lastRet初始化为-1。这种操作无非就是不让你进行set(E e)操作,抑或是不让你进行remove()操作。为什么呢?因为其实现的方法注释是规定了如此:
//remove()规定:
//只有在最后一次调用next或previous之后没有调用add时,才可以执行此操作。
//set(E e)规定
//只有在最后一次调用next或previous之后既不调用remove也不调用add,才可以执行此调用。
public void add(E e) {
checkForComodification();
try {
int i = cursor;//保存当前元素的索引 ListItr(int index) { super();cursor = index }
ArrayList.this.add(i, e);//调用add方法
cursor = i + 1;//游标右移
lastRet = -1;//lastRet重置为-1
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
是不让你进行set(E e)操作,抑或是不让你进行remove()操作。为什么呢?因为其实现的方法注释是规定了如此:
//remove()规定:
//只有在最后一次调用next或previous之后没有调用add时,才可以执行此操作。
//set(E e)规定
//只有在最后一次调用next或previous之后既不调用remove也不调用add,才可以执行此调用。
public void add(E e) {
checkForComodification();
try {
int i = cursor;//保存当前元素的索引 ListItr(int index) { super();cursor = index }
ArrayList.this.add(i, e);//调用add方法
cursor = i + 1;//游标右移
lastRet = -1;//lastRet重置为-1
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}