ArrayList源码分析
ArrayList源码分析
ArrayList简介
- ArrayList 是 Collection 和 List 接口的实现类。底层的数据结构是数组,数组结构特点:增删慢、查询快。线程不安全集合!
- ArrayList 的特点:
- 单列集合:对应于 Map 集合来说【双列集合】
- 有序性:存入元素和取出元素时顺序是一样的
- 元素可以重复:可以存放两个相同的元素
- 含带索引的方法:数组与生俱来就含有索引【下标】
ArrayList原理分析
ArrayList 的数据结构源码分析
// 空的对象数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认容量空对象数组,通过空的构造参数生成 ArrayList 对象实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// ArrayList 实际存储数据的对象数组
transient Object[] elementData;
// 1. 为什么是 Object 类型?利用面向对象多态特性,可以存储任意引用数据类型
// 2. ArrayList 不能存储基本数据类型
ArrayList 默认容量&最大容量
// 默认初始化容量是 10
private static final int DEFAULT_CAPACITY = 10;
// 最大容量:2^31 - 1 - 8 = 21 4748 3639 【21亿】
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
- 为什么最大容量要减8?
- 目的是为了存储 ArrayList 集合的基本信息,比如 List 集合的最大容量
- 参考资料:https://stackoverflow.com/questions/35756277/why-the-maximum-array-size-of-arraylist-is-integer-max-value-8
为什么ArrayList查询快、增删慢?
ArrayList 的底层数据结构就是一个 Object 的数组,是一个可变的数组,对于其所有操作都是通过数组来实现的。
- 数组的特点是查询快、增删慢
- 查询数据是通过索引定位,查询任意数据耗时均相同——查询效率高
- 删除数据时,需要将原始数据删除,同时迁移后面的所有数据——删除效率比较低
- 在指定索引位置插入数据时,需要添加位置后的每个元素后移,再在该索引位置添加元素——插入效率极低
- 当新增数据发生扩容时,要先将原数组复制到另一个内存空间更大的数组中,然后将新元素添加到扩容后的数组中——添加效率低
ArrayList 初始化容量
- ArrayList 底层是数组、动态数组:底层是 、Object 对象数组,数组存储的数据类型是 Object,数组名字为 elementData
transient Object[] elementData;
无参创建
创建 ArrayList 之后,ArrayList 容量是多少?
答案:JDK 1.7及以前,初始容量 10;JDK 1.8 及之后,初始化容量是 0
// 初始化的 ArrayList 容量,是0
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 空数组,容量是 0
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
如何初始化动态数组的容量?10个
- 在执行 add() 方法时进行初始化【懒加载】
- 判断当前数组的容量是否有存储空间,若没有则初始化容量大小为 10
// 向数组中添加一个元素
public boolean add(E e) {
// 确保有容量,如果第一次添加,会初始化一个容量为 10 的list
// size 是当前集合元素的个数,随着添加的元素递增
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) {
// 将当前 ArrayList 对象与默认数组进行比较
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 从默认容量 10 与 传入的容量1 中取一个最大值,返回初始化容量 10
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
// 确保不会超过数组的真实容量
private void ensureExplicitCapacity(int minCapacity) {
// minCapacity是当前计算后的容量10
modCount++; // 对当前数组操作的计数器
// overflow-conscious code
// 最小容量(10) - 当前数组({})的长度 0
if (minCapacity - elementData.length > 0)
grow(minCapacity); // 扩容
}
带初始化容量创建
// 创建 ArrayList 集合,并且设置固定容量,initialCapacity:手动设置的初始化容量
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 如果大于0,则创建一个容量为 initialCapacity 的对象数据,并交给 elementData
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) { // 如果设置容量为 0,则设置为默认数组
this.elementData = EMPTY_ELEMENTDATA;
} else { // 以上都不是,则抛出非法参数异常
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}
- 注意:使用 ArrayList 集合,建议如果知道集合大小,最好提前设置,提升集合的使用效率。
ArrayList扩容原理
add 方法先要确保数组的容量足够,防止数组已经填满还往里面添加数据而造成数组越界:
- 如果数组空间足够,直接将数据添加到数组中;
- 如果数组空间不够了,则进行扩容,扩容 1.5 倍;
- 扩容:原始数组 copy 到新数组中,同时向新数组后面加入数据。
使用 new 创建的 ArrayList 的对象是没有容量的,在第一次 add 时,会进行第一次扩容,扩容到初始化值10。0 => 10。
// grow 扩容数组
// minCapacity 当前数组的最小容量,是指存储了多少元素
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length; // 获取当前存储数据数组的长度
int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量 = 旧容量 + 扩容容量【旧容量/2】
// 极端情况过滤:新容量 - 旧容量 < 0【int 值溢出了】,则不扩容
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 新容量比 ArrayList 的最大值还要大,则设置新的容量为 ArrayList 的最大值,以ArrayList最大值为当前容量
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);
}
总结
- 扩容的规则并不是翻倍,而是原来容量的 1.5 倍。
- ArrayList 的数组最大值 Integer.MAX_VALUE - 8,不允许超过这个最大值。
- 新增元素时,没有严格的数据值的检查,所以可用设置为null。
ArrayList线程安全问题及解决方案
错误复现
- 我们知道 ArrayList 底层是以数据方式实现,数组大小可变,允许所有元素,包括null。下面举个例子:开启多线程操作 List 集合,向 ArrayList 中添加和删除元素。
/**
* ArrayList线程安全问题复现
* @author yangwei
*/
public class ArrayListTest {
/**
* 全局线程共享的 ArrayList
*/
protected static ArrayList<String> arrayList = new ArrayList<>();
/**
* 定义线程,线程执行:向集合中添加自己的线程名称
*/
private static class MyThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(1000);
ArrayListTest.arrayList.add(Thread.currentThread().getName());
} catch (Exception e) {}
}
}
public static void main(String[] args) throws Exception {
// 创建500个线程数组
MyThread[] threads = new MyThread[500];
for (int i = 0; i < threads.length; i++) {
threads[i] = new MyThread();
threads[i].start();
}
// 遍历线程,等待线程执行完毕
for (MyThread t : threads) t.join();
// 遍历arrayList集合,打印所有线程名称
for (String threadName : arrayList) System.out.println("threadName = " + threadName);
}
}
- 运行代码,可能出现如下几种情况:① 打印 null,② 某些线程并未打印,③ 数组下标越界。
导致ArrayList线程不安全的源码分析
- ArrayList 成员变量:ArrayList的Object的数组存所有元素,size变量保存当前数组中元素个数。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
transient Object[] elementData; // non-private to simplify nested class access
private int size;
// ...
}
- 出现线程不安全的源码之一:add() 方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
- add 添加元素,实际做了两大步骤:
- 判断 elementData 数组容量是否满足需求;
- 在 elementData 数组对应的位置设置值。
- 线程不安全的隐患【1】,导致 ③ 数组下标越界
- 线程不安全的隐患【1】,导致 ① 打印 null,② 某些线程并未打印
- 由此我们可以得出,在多线程情况下操作ArrayList 并不是线性安全的。
解决方案
- 第一种方案:使用 Vector 集合,Vector 集合是线程安全的【不推荐】
protected static Vector<String> vector = new Vector<>();
- 第二种方案:使用 Collections.synchronizedList,它会自动将我们的 list 方法进行改变,最后返回给我一个加锁的 List
protected static List<String> list = Collections.synchronizedList(arrayList);
- 第三种方案:使用 JUC 中的 copyOnWriteArrayList 类进行替换【最佳选择】
protected static CopyOnWriteArrayList<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
ArrayList的Fail-Fast机制深入理解
什么是 Fail-Fast 机制?
“快速失败”即 Fail-Fast 机制,是Java中的一种错误检测机制。
- 当多个线程对集合进行结构上的改变,或者在迭代元素时直接调用自身方法,改变集合结构而没有通知迭代器时,有可能会发生 Fail-Fast 机制并抛出异常【ConcurrentModificationException】。注意是有可能,并不是一定会发生。
- 触发时机:在迭代的过程中,集合的结构发生改变,而此时迭代器并不知情,或者还没来得及反应,便会产生 Fail-Fast 事件。
- 再次强调,迭代器的快速失败行为无法得到保证!一般来说,不可能对是否出现不同步并发修改,或者自身修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。
- Java.util包中的所有集合类都是快速失败的,而java.util.concurrent包中的集合类都是安全失败的;快速失败的迭代器抛出ConcurrentModificationException,而安全失败的迭代器从不抛出这个异常。
ArrayList的Fail-Fast问题复现
- ArrayList的Fast-Fail事件复现及解决方案:
/**
* 复现Fail-Fast机制
* 1.产生条件:
* 多个线程操作同一个集合
* 同时遍历这个集合,该集合被修改
* 2. 解决办法:使用JUC并发包中的集合 CopyOnWriteArrayList
*/
public class ArrayListTest {
// protected static List<String> list = new ArrayList<>();
protected static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
public static void main(String[] args) throws Exception {
// 创建线程1,并且向集合添加元素,打印集合中的内容
Thread t1 = new Thread(() -> {
for (int i = 0; i < 6; i++) {
list.add(Thread.currentThread().getName() + " " + i);
printAll();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 6; i++) {
list.add(Thread.currentThread().getName() + " " + i);
printAll();
}
});
t1.start();
t2.start();
}
private static void printAll() {
// 获取当前集合的迭代器
Iterator<String> it = list.iterator();
// 通过迭代器遍历集合
while (it.hasNext()) {
System.out.println(it.next() + ",");
}
}
}