一、初始化过程ArrayList集合类,相信大家都不陌生,并且可以说是非常熟悉,因为在日常开发中,需要用到集合的时候,大多数情况都是使用的这个类,那么为什么大家都使用这个类呢?它到底好在哪里?那我们通过源码分析来一探究竟。
首先来看一下 ArrayList 内部声明的变量都有哪些:
// 默认的容量大小为 10
private static final int DEFAULT_CAPACITY = 10;
// 声明了一个空的数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 又声明了一个默认容量的空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//存储 ArrayList 元素的数组缓冲区
transient Object[] elementData;
// 集合的元素个数,默认是 0
private int size;
// 扩容时用来比较容量大小的,这个值已经非常的大了
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
1. 无参构造 初始化
当我们使用无参构造函数List list = new ArrayList()时,我们分析一下,它做了什么:
// 将 变量 elementData 赋值了一个默认容量大小为 10 的空数组
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
通过这一步,我们可以看到,ArrayList 存储数据,其实也是用的数组来完成的;
2. 有参构造 初始化看一下有参构造做了什么:
// 根据传入的大小,创建一个同等大小容量的数组
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);
}
}
二、add method
初始化过程了解了,我们看一下添加元素方法:
public boolean add(E e) {
// 确保容量足够,先计算一下,当我们新增第一个元素的时候,size = 0
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
可以看到,为了确保容量足够,先计算一下【ensureCapacityInternal(size + 1);】当我们新增第一个元素的时候,此时的size = 0,下面我们来看一下ensureCapacityInternal 方法干了啥:
// 第一次添加元素,此时 minCapacity = 1
private void ensureCapacityInternal(int minCapacity) {
// 上一步,我们第一次添加元素的时候,size = 0, 此时这个方法的 minCapacity = 1
// elementData 是在我们初始化的时候,赋值了一个容量大小为 10 的空数组
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
再来看一下 calculateCapacity 方法:
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 当我们使用无参构造时,进入判断,即 此时返回默认容量 10,因为传入的 minCapacity = 1
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
容量计算完了,我们回过头来,再看一下ensureExplicitCapacity:
// minCapacity = 10
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// elementData.length = 10,minCapacity = 10 ,当容量足够时,直接返回进行数组元素添加
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
分析到上面,我们发现,当使用无参构造初始化时,默认的数组容量是 10,在添加前10 个元素的时候即默认容量足够时,直接通过数组的下标进行添加元素:
public boolean add(E e) {
// ...
elementData[size++] = e;
return true;
}
那么,当默认容量 10被我们使用完之后,当我们添加第 11 个元素时,会怎么样呢?小伙伴们可以继续通过上面的流程进行分析。我直接跳过前面几步,从计算容量这里开始:
// minCapacity = 11
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// elementData.length = 10,minCapacity = 11 ,容量不足,进入扩容方法
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
这里,我们继续分析 grow 方法【自动扩容】:
// 假设第 11 次添加元素,此时 minCapacity = 10
private void grow(int minCapacity) {
// oldCapaccity = 10
int oldCapacity = elementData.length;
// 这里将 oldCapacity 向右偏移 1 位,即 1010 >> 1 = 0110 = 6
// newCapacity = 10 + 6 = 16,自动扩容 1.6 倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 将原来的数组大小的容量扩大 1.6 倍后,通过复制元素,生成一个新的容量的数组,这里就完成了新增时的自动扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
看到这里,相信大家肯定直呼 666,扩容原来这么简单,就是通过一个【位移运算+复制】就完成了自动扩容。在我们平时的开发工作中,如果遇到这种场景,也可以使用这种小技巧来完成。
三、get method集合中获取元素的方法,经常使用。
public E get(int index) {
// 传入元素下标,首先要检查下标的位置是否超出集合的总容量大小
rangeCheck(index);
return elementData(index);
}
private void rangeCheck(int index) {
// 当下标的大小 >= 集合的容量时,会报出我们经常看到的下标越界的异常,因为集合的下标是从 0 开始的
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
E elementData(int index) {
// 下标不越界,正常从数组中,通过下标获取元素
return (E) elementData[index];
}
四、remove method
集合中移除元素的方法,经常使用。
public E remove(int index) {
// 同样,首先要检查下标是否越界
rangeCheck(index);
modCount++;
// 取出要移除的元素
E oldValue = elementData(index);
// 算出要移动的下标位置
int numMoved = size - index - 1;
// 如果移除的是最后一个,则直接将最后一个元素置为 null
if (numMoved > 0)
// 通过计算出的下标的位置将后面的元素往前移动
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
代码比较简单,很容易理解,就不做一一描述。
ArrayList 中,还有其他的方法,不再逐一讲解,小伙伴们可以通过自行阅读源码进行学习。这里最主要的就是 add 添加元素的时候,自动扩容机制。
五、为什么不直接使用数组?我们通过分析 ArrayList 源码,得出结论,其实就是使用数组来进行存储数据的,那为什么不直接使用数组呢?为什么又要在数据的结构上进行封装呢?这个就要知道数组的特点,总结有以下几点:
- 内存地址连续;可以通过下标访问成员,性能较高;增删操作带来更大的性能消耗(需要保证数据越界问题和动态扩容问题)
正是因为数组结构的优点和缺点,促使了 ArrayList 使其进行封装,使用 ArrayList 我们无须关心容量不够的问题,它自动帮我们完成了扩容,提供了更加方便的 API,使其操作元素更加便捷。
六、FailFast 机制【modCount++】快速失败的机制,Java 集合类为了应对并发访问在集合迭代过程中,内部结构发生变化的一种防护措施,这种错误检查的机制是为这种可能发生错误,通过抛出ConcurrentModificationException异常。
我们在上面分析源码的过程中,会经常看到 modCount++;这一行代码,它的作用就是为了这个 FailFast 机制 而提供的;它是在父类 java.util.AbstractList.java 中声明的。
protected transient int modCount = 0;
下面,我们来写一个DEMO,演示一下。
- 新建一个ThreadListTest1.java
public class ThreadListTest1 extends Thread {
private List list;
public ThreadListTest1(List list) {
this.list = list;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
list.add(i);
System.out.println("list add :" + i);
try {
sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} 再建一个ThreadListTest2.java public class ThreadListTest2 extends Thread {
private List list;
public ThreadListTest2(List list) {
this.list = list;
}
@Override
public void run() {
while (true) {
for (Iterator iterator = list.iterator();iterator.hasNext();) {
iterator.next();
try {
sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
} 测试一下 public class ConcurrentListTest {
private static List list = new ArrayList();
public static void main(String[] args) {
ThreadListTest1 thread1 = new ThreadListTest1(list);
ThreadListTest2 thread2 = new ThreadListTest2(list);
thread1.start();
thread2.start();
}
} 可以看到,在并发场景下,对同一个集合进行操作时,抛出了异常,这是为什么呢?我们通过源码可以看一下:
private class Itr implements Iterator{ int cursor; int lastRet = -1; // 迭代的时候,首先将 当前的 modCount 赋值给了expectedModCount int expectedModCount = modCount; Itr() {} public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { // 执行 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]; } // 省略其他源代码... }
final void checkForComodification() {
// 如果不相等的话,抛出异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
这就是 Java 集合在并发操作下的 控制方式,通过对比两个值是否相等来处理,有点类似于数据库的乐观锁。。
以上,是我个人的学习总结,仅供参考。



