写在前面
ArrayList相信大家做开发的同学都不陌生,在开发过程中这应该是最常用的数据结构了吧。但是现在是“源码时代”,会用还不够,要知道他的实现原理,本文主要基于jdk1.8对ArrayList源码进行分析。
一、从主要字段开始
值得注意的是,ArrayList内部会有一个modCount字段,但是这个字段是在父类AbstractList中的,代表着修改次数,后面会讲
/**
* 默认容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 空元素集,构造函数传入空集合的时候使用
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 空元素集,默认无参构造函数中使用
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
ArrayList真正保存元素的数组,在第一次插入元素的时候扩容
*
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* 当前数组元素个数
*
* @serial
*/
private int size;
接下来看一下主要的构造方法:
/**
* 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];
} else if (initialCapacity == 0) {
//如果等于0,则将上面的空数组赋值
this.elementData = EMPTY_ELEMENTDATA;
} else {
//如果是小于0的负数,抛非法参数异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
//默认无参构造给的是一个空数组,不在这个时候扩容
this.elementData = DEFAULTCAPACITY_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
*/
//传入集合的构造函数
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
//如果长度不为0,进此逻辑
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
//如果该元素数组类型不是等于Object[],则拷贝一份Object[]类型的数组 并赋值给
//elementData
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
//长度是0,给空数组
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
二、add方法
1、list默认的add方法:
public boolean add(E e)
/**
* 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!!
//这里添加元素
elementData[size++] = e;
return true;
}
来看一下那个函数
private void ensureCapacityInternal(int minCapacity) {
//调用了这个函数,记住这个minCapacity代表啥:size+1
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
再来看一下参数里面这个calculateCapacity函数
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//如果元素数组为空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//返回默认容量和size+1中较大的那个
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
//如果不是空数组,返回size+1
return minCapacity;
}
继续跟踪:
private void ensureExplicitCapacity(int minCapacity) {
//首先modCount加1,这个是干嘛用的后面会说
modCount++;
// overflow-conscious code
//关键点来了,这句话啥意思?size+1比当前数组容量大的时候,很明显刚开始是0,1比0大很正常
if (minCapacity - elementData.length > 0)
//grow方法明显是扩容的真正方法
grow(minCapacity);
}
继续来看grow函数:
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
//获取数组原始容量
int oldCapacity = elementData.length;
//注意这行,扩容后的新容量等于老容量右移一位加上自身,也就是原来的1.5倍,而不是hashmap的
//两倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果新容量还是比size+1小,这是什么情况?想想
//当然是老容量为0的时候,0*1.5=0,所以这个时候新容量等于1
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//当扩容之后的新容量比这个整形最大值-8还要大的时候
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);
}
来看一下上面的hugeCapacity方法
private static int hugeCapacity(int minCapacity) {
//如果size+1之后比0小,也就是整形溢出的时候,直接抛异常
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//否则如果比整形最大值-8大 就取整形最大值Integer.MAX_VALUE,否则取整形最大值-8
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
至此整个扩容操作完成。
2、指定位置添加元素add方法
public void add(int index, E element)
public void add(int index, E element) {
//这个函数很简单主要是检查index下标是否越界,否则抛异常
rangeCheckForAdd(index);
//这个函数上面分析过了,不再解释
ensureCapacityInternal(size + 1); // Increments modCount!!
//这个函数可能大家没见过,他的作用是将elementData数组的index位置开始size -index长度的
//元素全部拷贝到elementData 数组index+1的位置,也就是将index位置开始的数组全部后移一
//位
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//再将插入的元素赋值给index位置
elementData[index] = element;
//数组元素加1
size++;
}
三、remove方法
1.根据下标删除的remove方法
public E remove(int index) {
//检查下标是否越界
rangeCheck(index);
//修改次数加1
modCount++;
E oldValue = elementData(index);
//需要移动多少个元素 如数组有5个元素 0,1,2,3,4,index等于2就是5-2-1=2,需要移动两
//个元素
int numMoved = size - index - 1;
//如果移动的元素个数大于0
if (numMoved > 0)
//这个函数之前介绍过,之前是移动index之后的元素到index+1,现在恰好相反,把index+1
//后面的元素移动到index
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//把最后一个元素置为null,方便jvm回收,也是上面如果移动的元素个数等于0 ,也就是要删除的
//是最后一个元素时,直接置为空
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
2.根据元素删除的remove方法
public boolean remove(Object o) {
//如果元素等于null,则进入此逻辑
if (o == null) {
//遍历数组
for (int index = 0; index < size; index++)
//如果遇到元素为null的
if (elementData[index] == null) {
//快速删除,这个函数下面会讲
fastRemove(index);
return true;
}
} else {
//如果要删除的元素不等于null
for (int index = 0; index < size; index++)
//遍历过程遇到相等的,则删除
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
来看一下fastRemove方法:
/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
//你发现了什么?哎好像就是走之前根据索引删除的逻辑 一摸一样啊
//但是仔细看 你会发现少了一个下标越界的检查操作,看头顶这行注释,为什么?该方法是私有方法
//调用的地方已经保证index在0到size-1之间,所以这个方法名fastRemove挺有意思
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
}
三、迭代操作
我不知道你面试的时候,有没有被问到过,ArrayList在循环的时候,能删除元素吗?如果你看过面试题,肯定知道答案是不行,那我们来看看为啥不行。这就和上面频繁出现的modCount有关了。
首先我们要知道ArrayList有iterator()方法,这个方法的来源是Collection接口继承了Iterable接口,实现这个接口说明该类是可迭代的,而iterator方法的返回值 类型Iterator成为迭代器,是需要自己实现的,这个类似于Comparable和Comparator的关系。
知道了这个,那我们对增强for循环一定不陌生,那凭什么ArrayList可以用增强 for循环,我们自己的类却不行?原因是增强for底层用的是迭代器操作,我们只要自己实现Iterable接口,也能使用增强for。
public interface Collection<E> extends Iterable<E>
public Iterator<E> iterator() {
return new Itr();
}
接下来看一下问题关键:ArrayList的迭代器实现
private class Itr implements Iterator<E> {
//游标,代表的是当前元素的索引下标
int cursor; // index of next element to return
//最近返回的元素
int lastRet = -1; // index of last element returned; -1 if no such
//期待的修改次数,我们发现他在迭代器被创建的时候就已经被指定了
int expectedModCount = modCount;
Itr() {}
public boolean hasNext() {
//游标是否已到最大值
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
//这是关键所在,检查修改次数,不一样会抛异常,下面方法写了,而上面我们看到无论是添
//加元素还是删除元素modCount都会加1因此过不了这里的校验
checkForComodification();
int i = cursor;
//如果游标大于数组长度,抛出异常
if (i >= size)
throw new NoSuchElementException();
//这里可能大家会有疑问,上面不是已经判断游标是否越界了吗怎么还要判断
//其实这边应该是为了防止多线程并发修改的情况。比如{1,2,3,4,5}有五个元素,这时候线程
//1在遍历,游标 刚好在索引4这个位置,线程2此时删除了一个元素,这个时候数组长度只有4
//了,此时显然取不到那个元素,因此此处直接抛出异常
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
//游标+1
cursor = i + 1;
//获取对应位置的元素,并赋值给lastRet
return (E) elementData[lastRet = i];
}
//这是迭代器自己的删除方法,在迭代中是可用的,我们来看下是为什么
public void remove() {
// 如果一次都没迭代过,则抛异常,只要调用了next,lastRet都会有值
if (lastRet < 0)
throw new IllegalStateException();
//检查状态
checkForComodification();
try {
//调用arraylist自己的remove方法,哎?那按理说modCount++会抛异常啊,为啥没有
//来看下面代码
ArrayList.this.remove(lastRet);
//next之后游标会加1,但是我们删除了元素,所以把游标重置到当前位置
cursor = lastRet;
//这里是重置lastRet为-1,删除之后必须迭代到下一个
lastRet = -1;
//这行代码你发现了什么?modCount又赋值给了expectedModCount,所以不会报错
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
//如果当前的修改次数不等于期待的修改次数,抛出异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
四、写在最后
关于ArrayList源码的解析就先写到这里,原创不易,注释都是一行一行手敲的,转载请注明出处。欢迎大家进行评论和点赞,有什么问题也可以加我私人微信:zyj-jy66。
来源:CSDN
作者:coderyjz
链接:https://blog.csdn.net/coderyjz/article/details/103599050