话不多说,先上图:
![](https://i-blog.csdnimg.cn/blog_migrate/79829160380659569e9cbf804208df7f.png)
我们在源码中可以看到整个ArrayList的继承与实现。可以看到它是实现了List接口,是基于数组实现的。那我们都知道数组要在创建的时候就确定长度,那这样势必会影响程序的性能和资源分配,那么作为近乎最常用的Java集合类–ArrayList又是怎么解决这些问题的呢?
下面让我们走进它的源码一探究竟,按照源码的顺序做一番思考~
壹·相关变量
/**
* 默认初始化容量,即不定义数组的长度时默认长度为10
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 用于空实例的共享数组实例,容量为0的时候给数组变量赋值
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 用于默认大小的空实例的共享空数组实例. 我们将其与EMPTY_ELEMENTDATA区分开来,以便知道添加
* 第一个元素时要膨胀多少
* 容量为默认的时候给数组变量赋值
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* transient关键字表示变量不能自动序列化,用来关闭变量的serialization(持久化)机制
* ArrayList中的元素都储存在elementData数组中
*/
transient Object[] elementData;
/**
* 数组列表的大小
*
* @serial
*/
private int size;
正因为elementData是Object类型的数组,所以添加元素的时候都会向上转型为object,这才成就了ArrayList动态数组的特性。
在JDK1.7的时候,当elementData为空时会赋给一个空数组,但如果程序中含有很多个空的ArrayList便会造成资源的浪费,因此在JDK1.8中引入了两个空数组,static表示被类中所有对象共享,都会指向这两个数组,final锁定数组地址不变,并且被所有的对象共享,因此就解决了1.7的问题。
那么这两个空数组是如何操作的呢?我们接着往后面看
贰·相关构造方法
/**
* 使用指定的初始容量构造一个空列表,如果使用带容量大小的构造函数,当参数大于0的时候,则会创建
* 一个和参数大小相同的Object数组,若等于0,则赋值为空数组,若小于0,抛出异常
*/
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的空列表,即当定义为无参的时候默认数组长度为10
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 按照集合迭代器返回的顺序构造一个包含指定集合元素的列表。
*toArray将集合转化为数组然后赋值给存储元素的数组
* @param c 要将其元素放入此列表中的集合
* @throws NullPointerException 如果指定的集合为null,则抛出NullPointerException
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray 可能错误的不返回 Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 替换为空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
叁·CURD实现原理
增加元素:
/**
* 将指定的元素添加到列表的末尾。
*
* @param e 元素添加到此列表
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
//调用ensureCapacityInternal方法对数组长度进行检查,不足的话则进行扩容
ensureCapacityInternal(size + 1); // 增量modCount!!
//对数组中第N+1个元素赋值
elementData[size++] = e;
return true;
}
增加元素的过程中涉及到了ArrayList的扩容机制,那这个机制是如何进行的呢?
//3.判断最小容量和当前数组实际长度的大小,若最小容量大于实际长度,则调用grow方法进行扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**2.判断elementData是否是ArrayList属性中定义的其中一个空数组
*DEFAULTCAPACITY_EMPTY_ELEMENTDATA
*若是则以默认为10的容量和赋值的容量的最大值作为数组的最小容量,否则以赋值的容量为最小容量
*/
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
//1.先是调用了此方法对数组长度进行判断
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
/**
* 要分配的数组的最大大小。有些虚拟机在数组中保留一些头词。尝试分配更大的数组可能会导致
* OutOfMemoryError:请求的数组大小超过VM限制
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
*4.增加容量,以确保至少可以保存由最小容量参数指定的元素数量
* @param minCapacity要求的最小容量
*/
private void grow(int minCapacity) {
// overflow-conscious code
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);
}
//5.如果最小容量大于定义的最大大小,则以int型数组的最大值为新的数组长度,否者为定义的最大大小
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
通过以上五个步骤,我们就拿到了扩容以后的新数组,之后便是将原本数组中的元素拷贝进新数组了,从grow方法中我们可以看到是调用的Arrays.copyOf方法
//1.
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
//2.
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength] //以新长度创建一个新的object数组,名为copy
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
/**3.实现数组复制,其中各个参数含义为:src:原数组;srcPos:源数组要复制的起始位置;
*dest:目的数组; destPos:目的数组放置的起始位置; length:复制的长度
*/
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
至此,ArrayList最主要的特性,扩容机制就结束了,但笔者之前提出来的问题你有没有答案了呢?就是这两个空数组是到底如何操作呢?
这个问题在源码中已经有了答案,当ArrayList使用无参构造器的时候,空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA会赋值给elementData,当使用有参构造器时,便会赋值给其EMPTY_ELEMENTDATA。这两个空数组的区别便在于第一次赋值的时候,如果小于10作为最小容量的会是谁,并以此决定了扩容方式。
获取元素:
/**
* 返回列表中指定位置的元素。
*
* @param index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
/**
* 检查给定的索引是否在范围内。如果不是,则抛出适当的运行时异常。这个方法不会检查索引是否为
* 负数:它总是在数组访问之前立即使用,如果index为负数,将抛出ArrayIndexOutOfBoundsException
* 异常。
*/
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* 构造一个IndexOutOfBoundsException详细消息。在错误处理代码的许多可能重构中,这种“概述”在服务器和客户端vm上都表现得最好。
*/
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size;
}
这部分源码不是很难,看着自带的注释基本就可看懂,这里就不做解释了。
删除元素:
/**
* 删除列表中指定位置的元素。
* 将所有后续元素向左移动(从它们的下标减去1)。
*
* @param index 要删除的元素的索引
* @return 从列表中删除的元素
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
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; // 可以让GC完成它的工作
return oldValue;
}
/**
* 检查给定的索引是否在范围内。如果不是,则抛出适当的运行时异常。这个方法不会检查索引是否为
* 负数:它总是在数组访问之前立即使用,如果index为负数,将抛出ArrayIndexOutOfBoundsException
* 异常。
*/
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
根据对象找到指定元素并删除:
/**
* 从列表中删除第一个出现的指定元素(如果它存在的话)。如果列表中不包含该元素,则保持不变。更正式的做法是删除索引最低的元素
* <tt>i</tt> such that
* <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>
* (如果存在这样的元素)。如果列表中包含了指定的元素,则返回true(或者等效地,如果列表因调用而改变)。
*
* @param o element to be removed from this list, if present
* @return <tt>true</tt> if this list contained the specified element
*/
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 remove方法,跳过边界检查,不返回已删除的值
*/
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
}
肆·fail-fast(快速失败机制)
我们在ArrayList的crud操作中都会看到一个modCount变量,每次执行crud的时候这个变量都会发生改变,那么这个变量到底有什么用呢?
ArrayList的快速失败机制,是一种失败检测机制,当迭代集合的过程中,该集合却被修改了一次之后,就有可能会发生fail-fast,即抛出异常。
异常又是如何被抛出的呢?
我们知道,对于集合如list,map类,我们都可以通过迭代器来遍历,而Iterator只是一个接口,具体的实现还是要看具体的集合类中的内部类去实现Iterator并实现相关方法。
public Iterator<E> iterator() {
return new Itr();
}
我们看到这会返回一个Itr类,而Itr类是ArrayList的内部类
/**
* AbstractList.Itr的优化版本
*/
private class Itr implements Iterator<E> {
int cursor; // 要返回的下一个元素的索引
int lastRet = -1; // 返回的最后一个元素的索引;如果没有则返回-1
/**modCount是抽象类AbstractList中的变量,默认为0,而ArrayList 继承了AbstractList ,
*所以也有这个变量,modCount用于记录集合操作过程中作的修改次数,与size还是有区别的,
*并不一定等于size
*/
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
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();
}
}
@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();
}
}
可以看到next方法中会先调用checkForComodification方法
final void checkForComodification() {
/**在一开始,expectedModCount初始值默认等于modCount,在前面ArrayList扩容机制的分析中,
*我们知道在ArrayList进行add,remove,clear等涉及到修改集合中的元素个数的操作时,
*modCount就会发生改变(modCount ++),所以当另一个线程(并发修改)或者同一个线程遍历过程中,
*调用相关方法使集合的个数发生改变,就会使modCount发生变化,所以如果不等的时候,表示着出现
*了另一个线程并发执行了,因此抛出异常
*/
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
了解了快速失败机制之后,我们如何去避免它呢?
?在单线程中,我们调用remove方法的时候可以换成迭代器的remove方法而不是集合类的remove方法。
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;//在这里会更新modCount,保证二者的一致性
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
这种方式有一定的局限性,那还有没有更好的解决方式了呢?
?采用JUC中的类替代ArrayList。
比如使用 CopyOnWriterArrayList代替 ArrayList, CopyOnWriterArrayList在是使用上跟 ArrayList几乎一样, CopyOnWriter是写时复制的容器(COW),在读写时是线程安全的。该容器在对add和remove等操作时,并不是在原数组上进行修改,而是将原数组拷贝一份,在新数组上进行修改,待完成后,才将指向旧数组的引用指向新数组,所以对于 CopyOnWriterArrayList在迭代过程并不会发生fail-fast现象。但 CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。
伍·线程的不安全性
?什么是线程不安全
在说线程不安全之前,我们先看一下什么叫线程安全:
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。也就是我们常说的锁机制。
而线程不安全就是就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
举个栗子:
在上面的源码中我们可以看到ArrayList添加数据的时候可能分为这两个步骤:
- 在elementData数组中存放元素
- 扩容
在单线程情况下,如果原本的size为0,添加一个元素之后size变为1;
而在多线程情况下就有可能发生这样一个情况:
比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (因为线程A仅仅完成了第一个步骤),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。
那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。
既然线程是不安全的,但是ArrayList的高效率又是我们所需要的,那我们该如何解决呢?
一种寻求线程安全的解决方式就是ArrayList 的封装类
//也就是加上synchronizedList锁
Collections.synchronizedList(new ArrayList<>());
另外一种就是使用Vector类。
参考:
https://blog.csdn.net/weixin_43274704/article/details/113032508
https://blog.csdn.net/jiaochunyu1992/article/details/51177373