ArrayList是List的接口中一个非常重要的实现类,也是项目中用的最频繁的集合,要了解为什么是最频繁的,就需要我们走进ArrayList内部,进行剖析它。
一、ArrayList内部的数据结构
从ArrayList源码中我们可以很清楚的看到,ArrayList底层的数据结构是数组,所有ArrayList集合的增删改查无非就是对数组的增删改查,但是我们又知道数组的长度是不可变的,那么当数组满时,我们怎样在向ArrayList中添加呢?听我慢慢道来
二、ArrayList几个重要的属性
第一个属性:数组的默认大小
/**
* 底层默认的数组大小
*/
private static final int DEFAULT_CAPACITY = 10;
第二个属性:当构造函数为ArrayList(0)
/**
* 当构造函数是ArrayList(0)时,默认的数组
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
第三个属性:当构造函数为ArrayList()
/**
* 当构造函数是ArrayList()时,默认的数组 .
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
第四个属性:ArrayList底层的数据结构
/**
* ArrayList存储数据的数组
*/
transient Object[] elementData;
有的同学不了解transient关键字的作用,这个关键字最主要的作用就是当序列化时,被transient修饰的内容将不会被序列化,如果想深入了解transient,请自行搜索。这里不做深入研究,你就记着ArrayList的数据在序列化时底层数据将不会被序列化。
第五个属性:集合数组修改次数的标识
protected transient int modCount = 0;
第六个属性:ArrayList实际的长度
/**
* 集合的长度
*/
private int size;
这里有必要解释一个知识点,size是否等于elementData.length呢?刚接触ArrayList集合的也许认为就是一样的,其实这种说法是不正确的,size和elementData的长度并没有必然的联系,例如,如果我们ArrayList()时,ArrayList只是给我们默认的elementData=DEFAULTCAPACITY_EMPTY_ELEMENTDATA,此时只是空数组,只有第一次add时才默认数组的大小为DEFAULT_CAPACITY=10,此时elementData.length=10,而size=0,所有说两者并没有必然的联系,要说有联系,也是elementData.length>=size.
总结:上面六个属性是ArrayList重要属性,前五个是ArrayList内部使用的,我们在用ArrayList的时候不会用到它们,但是size是我们使用非常频繁的。
三、ArrayList构造方法
第一个构造方法:无参构造方法
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
第二个构造方法:有参构造方法
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);
}
}
第三个构造方法:参数是一个集合
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)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
第一个和第二个构造方法我们有必要说一下,当ArrayList()和ArrayList(0)时,为什么默认的是不同呢?我们可以仔细看出,无参构造函数只是将底层的数据默认成DEFAULTCAPACITY_EMPTY_ELEMENTDATA空数组,数组并没有长度,当我们第一次调用add时要进行初始化数组的长度为:DEFAULT_CAPACITY =10,而有参构造函数的是要初始化数组的长度的,即使是0,在接下来无参构造函数要初始化数组长度的,而ArrayList(0)则已经初始化了,ArrayList()和ArrayList(0)用不同的默认,则就是为了下面第一次扩容时进行区别。
四、ArrayList重要的方法解读
第一个重要的方法:增加
public boolean add(E e) { ensureCapacityInternal(size + 1); -------------------------❶ elementData[size++] = e;------------------------------------❷ return true; }
add方法非常的简单,总共两行代码,
❶
判断是否扩容,
❷
将加入的放到数组的末尾,
至于怎样扩容的
,
并且怎样把默认的无参构造初始化为10的,当数组满后,怎样扩容的,这些我们等会在讲,我们先了解一个非常重要的内容,就是当多线程并发时,ArrayList为什么是不安全的。
第一个多线程情况:底层数据还有一个空位置,数组越界
线程1执行❶,因为有一个空位置,则不扩容.此时线程1让出CPU
线程2执行❶时因为有一个空位置,则不进行扩容,当执行完❷后,数据已经满了。
然后线程1获取CPU,开始执行❷,此时数据已经满了,在进行添加,就会数组下标越界
第二个多线程情况:扩容,出现脏数据
线程1执行❶后,进行了扩容,然后执行❷,我们知道size++是复合操作读-改-写,当size=10.
线程2执行❶后,因为线程1已经扩容了,则无需扩容了,执行❷,因为线程1还没有执行++,此时size=10.
所以两个线程同时向size=10下标写数据,导致脏数据的出现
从上面的两个出现的情况可以看出,ArrayList在多线程下是不安全的,如果在多线程下不进行安全处理,则不能把ArrayList创建成共享的数据,就是全局变量。
好了,我们回到add方法的❶看看是怎样扩容的,先上代码
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {--------------------❸
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);-----------------❹
}
ensureExplicitCapacity(minCapacity);---------------------------------------❺
}
当看到❸时,是不是想到上面我们讲到的,当进行无参数构造函数时,在第一次调用add时会进行对数组初始化大小,❹获取两者的最大值,minCapacity=0,DEFAULT_CAPACITY =10,所以是10,接下来看❺
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 此时:minCapacity=10,elementData.length=0 所以会执行grow
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
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);
}
最终扩容的真正逻辑在grow中出现了,newCapacity是新数组的长度,newCapacity=oldCapacity+(oldCapacity>>1),这句话的意思就是老数组的长度+老数组长度右移一位,newCapacity=1.5oldCapacity.
就是如果ArrayList集合扩容,则扩容为以前的1.5倍。下面的代码就很容易理解了,大家自行观看,然后把老数组的数据复制到新数组中。其他的add(index,E e),set方法类似,这里就不在阐述了
第二个重要的方法:删除remove
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; // clear to let GC do its work ------------------❿
return oldValue;
}
❻是判断用户给出的index是否合法,就是判断index>=size,就是不合法,将会抛出异常,❼是获取将要删除的值,❽获取将要移动的下标,❾把删除位置后面的数据进行向前移动
从add(index,e)和remove两个方法可以看出,ArrayList集合的插入和删除需要对数组进行复制和移动,所以效率相对较低,所以ArrayList不适合那些插入和删除较多的逻辑。
第三个重要的方法:查
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
查是非常的简单的,首先判断index的合法性,然后通过给定的下标获取值。这个方法虽然简单,但是我们可以看出ArrayList对于查效率是非常的高的,时间复杂度(o1)
这篇文章就先写到这里,我们接下来总结一下:
1:如果调用的是无参构造函数,则默认的是空数组,并没有对数组的长度赋值 2:调用add(e)时,首先判断是否进行扩容,然后把e添加到最后,这个方法只是在扩容时进行数组的复制,不扩容时,没有数组的复制。 3:调用add(index,e)和remove时都有数组的复制和移动,所以效率相对较低,而查询get效率高,所以我们可以认定ArrayList适合于读取多,插入和移除少的情况 4:ArrayList并不适合多线程下作为共享变量,ArrayList是线程不安全的。 |
下一篇文章我将逐渐深入ArrayList其他方法的源码分析和用途,请持续关注