ArrayList源码分析
ArrayList集合是Collection和List接口的实现类。底层的数据结构是数组。
数据结构特点 : 增删慢,查询快。线程不安全的集合!
开发的时候,不建议无脑选用ArrayList
ArrayList的特点:
单列集合 : 对应与Map集合来说【双列集合】
有序性 : 存入的元素和取出的元素是顺序是一样的
元素可以重复 : 可以存入两个相同的元素
含带索引的方法 : 数组与生俱来含有索引【下角标】
ArrayList的数据结构源码分析
//空的对象数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认容量空对象数组,通过空的构造参数生成ArrayList对象实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//ArrayList对象的实际对象数组!
transient Object[] elementData; // non-private to simplify nested class access
//1、为什么是Object类型呢?利用面向对象的多态特性,当前ArrayList的可以存储任意引用数据类 型。
//2、ArrayList有一个问题,不能存储基本数据类型!就是数组的类型是Object类型
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集合的最大容量!
为什么ArrayList查询快,增删慢?
ArrayList的底层数据结构就是一个Object数组,一个可变的数组,对于其的所有操作都是通过数组来实
现的。
数组是一种,查询快、增删慢!
查询数据是通过索引定位,查询任意数据耗时均相同。查询效率贼高!
删除数据时,要将原始数据删除,同时后面的每个数据迁移。删除效率就比较低!
新增数据,在添加数组的位置加入数组,同时在数组后面位置后移以为!添加效率极低!
ArrayList初始化容量
ArrayList底层是数组,动态数组!
底层是Object对象数组,数组存储的数据类型是Object,数组名字为elementData。
transient Object[] elementData;
创建ArrayList对象分析:无参数
创建ArrayList的之后,ArrayList容量是多少呢?回答10是错误的!回答0是正确【限定条件,在
JDK1.8中】
如何 初始化 动态数组的容量?10个
构造方法
/**
* Constructs an empty list with an initial capacity of ten.
*/
//初始化的ArrayList的容量,是10个!
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//空数组!
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
在执行add()方法的时候初始化!【懒加载】
判断当前数组的容量是否有存储空间,如果没有初始化一个10的容量。
创建ArrayList对象分析:带有初始化容量构造方法
//创建ArrayList集合,并且设置固定的集合容量
public ArrayList(int initialCapacity) {
//initialCapacity 手动设置的初始化容量
if (initialCapacity > 0) {//判断容量是否大于0,如果大于0
//创建一个对象数组位指定容量大小,并且交给ArrayList对象
this.elementData = new Object[initialCapacity];
//如果设置的容量为0,设置默认数组
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;//默认的元素数据数组{}
} else {
//如果不是0,也不是大于0的数,会抛出非法参数异常!
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
}
}
注意 : 使用ArrayList的集合,建议如果知道集合的大小,最好提前设置。提示集合的使用效率!
ArrayList扩容原理
add方法先要确保数组的容量足够,防止数组已经填满还往里面添加数据造成数组越界:
1. 如果数组空间足够,直接将数据添加到数组中
2. 如果数组空间不够了,则进行扩容。扩容1.5倍扩容。
3. 扩容 : 原始数组copy新数组中,同时向新数组后面加入数据
注意 : new的ArrayList的对象没有容量的,在第一次添加的add,会进行第一次扩容。0 -> 10!
总结:
1. 扩容的规则并不是翻倍,是原来容量的1.5倍
2. ArrayList的数组最大值Integer.MAX_VALUE。不允许超过这个最大值
3. 新增元素时,没有严格的数据值的检查。所有可用设置null
ArrayList线程安全问题及解决方案
错误复现
ArrayList 我们都知道底层是以数组方式实现的,实现了可变大小的数组,它允许所有元素,包括null。
看下面一个例子:开启多个线程操作List集合,向ArrayList中增加元素,同时去除元素
运行代码结果可知,会出现以下几种情况:
①打印null
②某些线程并未打印
③数组角标越界异常
导致ArrayList线程不安全的源码分析
ArrayList成员变量
ArrayList的Object的数组存所有元素。
size变量保存当前数组中元素个数。
出现线程不安全源码之一 : add()方法
add添加元素,实际做了两个大的步骤:
1. 判断elementData数组容量是否满足需求
2. 在elementData对应位置上设置值
线程不安全的隐患【1】,导致③数组下标越界异常
线程不安全的隐患【2】,导致①Null、②某些线程并未打印
由此我们可以得出,在多线程情况下操作ArrayList 并不是线性安全的。
那如何解决呢?
3.3 解决方案
第一种方案:使用Vector集合,Vector集合是线程安全的
//线程安全问题解决方案1
protected static Vector<Object> vector = new Vector<>();
第二种方案:使用Collections.synchronizedList。它会自动将我们的list方法进行改变,最后返回给我们
一个加锁了List
//线程安全问题解决方案2
//将集合改为同步集合
protected static List<Object> synList = Collections.synchronizedList(arrayList);
第三种方案:使用JUC中的CopyOnWriteArrayList类进行替换。【】
//线程安全问题解决方案3 JUC 【最佳选择】
protected static CopyOnWriteArrayList<Object> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
ArrayList的Fail-Fast机制深入理解
什么是Fail-Fast机制?
"快速失败"即Fail-Fast机制,它是Java中一种错误检测机制!
当多钱程对集合进行结构上的改变,或者在迭代元素时直接调用自身方法改变集合结构而没有通知迭代
器时,有可能会触发Fail-Fast机制并抛出异常【ConcurrentModificationException】。注意,是有可能
触发Fail-Fast,而不是肯定!
触发时机 : 在迭代过程中,集合的结构发生改变,而此时迭代器并不知情,或者还没来得及反应,便会
产生Fail-Fast事件。
再次强调,迭代器的快速失败行为无法得到保证!一般来说,不可能对是否出现不同步并发修改,或者
自身修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。
Java.util包中的所有集合类都是快速失败的,而java.util.concurrent包中的集合类都是安全失败的;快
速失败的迭代器抛出ConcurrentModificationException,而安全失败的迭代器从不抛出这个异常。
ArrayList的Fast-Fail事件复现及解决方案