基于jdk1.8源码
UML类关系图
我们先来看一下ArrayList的类关系图。

从图中可以看出,ArrayList继承了AbstractList抽象类,实现了4个接口。
1.实现了List接口:
2.实现了RandomAccess接口:
3.实现了Cloneable接口:
4.实现了Serializable接口:
构造函数
<u>ArrayList的构造方法就做一件事情,就是初始化一下储存数据的容器,其实本质上就是一个数组,在其中就叫elementData。</u>
1.ArrayList()
构造一个初始容量为 10 的空列表。如果我们创建ArrayList对象的时候不传入参数,则使用此无参构造方法创建ArrayList对象。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
初始化一个空的list,在add第一个元素的时候,设置初始容量为10
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//data一开始是空,则返回10的初始容量
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
2.ArrayLIst(int)
构造一个指定容量为capacity的空ArrayList。这是一个带初始容量大小的有参构造函数。
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);
}
}
- 初始容量大于0,实例化数组,将自定义的容量大小当成初始化elementData的大小
- 初始容量等于0,将空数组赋给elementData
- 初始容量小于0,抛异常
3.ArrayList(Collection<? extends E>)
构造一个包含指定 collection 的元素的列表,这些元素是按照该 collection 的迭代器返回它们的顺序排列的。第二个有参构造方法构造时赋的值是它的父类Collection对象。
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)
//每个集合的toarray()的实现方法不一样,所以需要判断一下,如果不是Object[].class类型,那么就需要使用ArrayList中的方法去改造一下。
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
- 将集合对象转成数据,地址赋值给elementData
- 如果数组的实际大小等于0(c中没有元素),将空数组EMPTY_ELEMENTDATA赋值给elementData
- 如果size的值大于0,则执行Arrays.copy方法,把collection对象的内容(可以理解为深拷贝)copy到elementData中()
重要方法
1.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];
}
可以看到由于Arraylist底层就是个obj的数组,所以获取元素是比较简单的。直接先对index进行范围判断,然后返回数组下标对应的值。
2.set方法
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
set方法是替换数组某个位置的值。先检查下标是否越界,然后取出旧值,把新值替换进去,最后返回旧值给用户。
3.add方法
(1)一个参的方法
在list末尾加上一个元素
public boolean add(E e) {
//确保内部容量,传的是size+1的最小容量值,避免浪费
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//实际数组大小如果为空的话,初始化max(10,minCapacity)
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, 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;
//新容量=老容量+老容量/2
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);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
(2)两个参的方法
在list指定的位置插入一个元素
public void add(int index, E element) {
//越界检查
rangeCheckForAdd(index);
//确保内部容量,不够会调用grow进行扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//复制数组,arraycopy(原数组,源数组中的起始位置,目标数组,目标数据中的起始位置,要复制的数组元素的数量)
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
4.remove方法
(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);
//将索引指向null,让GC回收多余末尾的旧值
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
(2)删除第一个匹配到的元素
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);
elementData[--size] = null; // clear to let GC do its work
}
(3)删除一段区间的元素
fromIndex<=删除区间<toIndex
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;
}
对比jdk11的写法
protected void removeRange(int fromIndex, int toIndex) {
if (fromIndex > toIndex) {
throw new IndexOutOfBoundsException(
outOfBoundsMsg(fromIndex, toIndex));
}
modCount++;
shiftTailOverGap(elementData, fromIndex, toIndex);
}
private void shiftTailOverGap(Object[] es, int lo, int hi) {
System.arraycopy(es, hi, es, lo, size - hi);
for (int to = size, i = (size -= hi - lo); i < to; i++)
es[i] = null;
}
(4)removeAll和retainAll
//删除源数组中包含目标数组的元素
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);//false表示不保留相同的元素,即删除
}
//交集,保留源数组中包含目标数组的元素
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);//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)//判断每一个源元素是否存在于对比的list中。complement为true则把元素保存下来(retainAll);为false则不保留(removeAll)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {//如果contains过程出错,则把剩下的源数据的元素复制到目标数组里面去
System.arraycopy(elementData, r,//源数组,源起始位置
elementData, w,//目标数组,目标起始位置
size - r);//复制的长度
w += size - r;//更新目标数组的最新长度
}
if (w != size) {//目标数组剩下的元素为源数组的元素,可以==null,让gc回收掉
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;//数组的大小有变化则返回true;
}
}
return modified;
}
(5)indexOf和lastIndexOf
//返回源数组中第一个命中的下标,如没有则返回-1。
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;
}
(6)clear()
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
问题
1.默认初始化大小
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
2.怎么扩容
当判断需要的最小容量大于list中数组的长度时,则通过grow扩容,首先扩大为原数组的1.5倍,还不够就扩充为需求的容量大小。
3.最大容量
最大容量为MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
4.怎么找到下一个的
因为lsit的底层存储还是数组,所以直接通过下标定位就行了。
5.线程安全吗
不安全的,主要问题出现在获取size大小操作完后,size++这里不是原子性的。导致会出出现脏读的情况。举例:A和B两个线程同时调用add方法
- 初始列表size大小为0。此时两个线程同时拿到size为0。
- 线程A执行插入操作,将elementData[0]=A
- 此时线程B也执行插入操作,将elementData[0]=B
- 线程A将size++
- 线程B将size++
此时我们会发现ArrayList的size变成了2,[0]=B;[1]=null。这就出现线程不安全的问题了
6.和hashmap的区别
额外知识点
1.transient关键字
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class acces Object[] elementData; // non-private to simplify nested class access
transient的字面意思是短暂的,在java中被此关键字修饰的属性值将不会被序列化。
具体可查看:https://baijiahao.baidu.com/s?id=1636557218432721275&wfr=spider&for=pc
2.MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
The maximum size of array to allocate.
Some VMs reserve some header words in an array.
Attempts to allocate larger arrays may result in
OutOfMemoryError: Requested array size exceeds VM limit
有一些虚拟机在数组中保留了一些头信息,为了避免内存溢出而已。
3.modCount的作用
在 AbstractList 中,有一个全局变量 madCount,记录了结构性改变的次数。结构性改变指的是那些修改了列表大小的操作,在迭代过程中可能会造成错误的结果。
madCount 交由迭代器(Iterator)和列表迭代器(ListIterator)使用,当进行 next()、remove()、previous()、set()、add() 等操作时,如果 modCount 的值意外改变,那么迭代器或者列表迭代器就会抛出 ConcurrentModificationException 异常。
如果希望提供快速失败(fail-fast)机制,那么其子类就应该在 add(int, E)、remove(int) 等结构性改变的方法中将 madCount 变量自增,否则可以忽略该变量。