1.List是什么?
List就是有序队列。
List是一个接口,继承自Collection。
其中主要进行实现基本的List接口功能的有AbstractList抽象类,AbstractSequentialList。
其中AbstractList抽象类继承了AbstractionCollection并进行实现了List接口,AbstractList实现了List接口中除了size(),get(int location)之外的函数。
AbstractSequentialList抽象类进行继承了AbstractList并且进行实现了链表中根据index操作链表的全部函数。
2.ArrayList
2.1概览
1.ArrayList支持所有的List操作,继承了AbstractList并进行实现了List。
2.ArrayList支持快速随机访问,实现了标志性接口RandomAccess,实现这个接口的都标志着可以进行随机快速访问(根据索引访问)。
3.ArrayList支持浅拷贝,实现了Cloneable接口。
4.ArrayList支持序列化,实现了Serializable接口。
5.ArrayList并不是线程安全的。
2.2ArrayList的数据结构
ArrayList底层通过一个动态的Object数组进行存储数据,默认ArrayList的动态数组长度是10,ArrayList存储长度的size。
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // non-private to simplify nested class access
private int size;
2.3ArrayList的序列化
2.3.1不采用默认JDK序列化方式
ArrayList采用了一种十分巧妙的序列化方式,在JAVA中只有实现Serializable接口的类才可以被序列化,其中实现之后默认的序列化方式是JDK底层默认的二进制序列化方式,其中这种序列化方式进行使用的时候会将类中没有transient关键字标注的字段名全都序列化。
可以看到ArrayList中的动态数组被transient进行标注了,代表不能被JDK底层默认的序列化方式进行序列化,必须进行自己编写序列化这个字段的逻辑,或者不进行去序列化这个字段。
transient Object[] elementData;
2.3.2ArrayList的序列化方式
由于ArrayList底层是一个动态扩容的数组进行实现的,数组中所有位置不一定都被存储上数据了,所以如果贸然将整个数组进行序列化,就会特别浪费空间,所以ArrayList底层重写了序列化方式,仅仅对数组中已经有数据的部分进行序列化。
ArrayList重写了Serializable接口中的readObject和writeObject方法。
这段代码十分好读,writeObject是序列化的过程,先进行调用默认的序列化的方法,将对象中没有被transient进行标注的字段进行序列化,再进行将size进行读取到里面去,然后循环写入elementData中的数据,只遍历索引0-(size - 1)中的数据(也就是动态数组中有数据的方式)。
readObject是反序列化的过程,先给elementData赋值控数组,先进行默认的反序列化过程,将对象中没有被transient进行标注1的字段进行反序列化,再进行反序列化,也是仅仅是反序列化给数组中有的数据。
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
int capacity = calculateCapacity(elementData, size);
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
2.4构造方法
ArrayList中有三个构造方法,一个无参构造器,一个接收一个initialCapactity的构造函数(初始化ArrayList中动态数组的长度),一个接收一个集合,进行使用集合初始化。
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() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
2.4.1无参构造器
DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个用于ArrayList底层初始化的空数组,长度为0,是一个类常量字段。当进行使用无参构造器创建ArrayList对象的时候,就会执行此方法,更改引用指向。
2.4.2疑问:为什么所有通过无参构造器新建ArrayList对象都是指向一个初始化数组,不会共享引用出错吗?
不会的,因为在添加数据的时候,会进行判断是不是第一个添加数据并且elementData是共享的默认空数组,如果是就会进行新建一个新的初始化默认长度数组,不会进行使用这个初始化空数组,导致出现引用错误。
2.4.3指定initialCapacity初始化ArrayList
如若initialCapactiy > 0则使用intialCapacity长度进行初始化一个Object数组。
如若等于0进行使用EMPTY_ELEMENTDATA这个数组进行初始化Object数组。
如果 < 0就直接抛出错误。
2.4.4使用集合作为参数初始化ArrayList
会先将这个集合转换为一个数组,先进行判断集合是不是属于ArrayList类型,如果是就直接将转换为数组的集合赋值给elementData,如果不是ArrayList类型,就调用Array.copyOf将a中的数据赋值给elementData。如果a中的长度=0,就直接将EMPTY_ELEMENTDATA赋值给elementData。
2.5ArrayList访问数据
ArrayList实现了RandomAccess接口,所以支持使用索引进行快速访问。
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
这是rangeCheck的源码,主要是判断index是否大于size了,如果大于了,直接进行抛出IndexOutOfBoundsException错误。为什么需要这样呢?因为主要是害怕动态数组的长度确实>index,数组访问支持,但是对于ArrayList集合就出错了,为了防止开发者进行访问越出集合有效数据边界的数据,进行以下校验。
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
2.6ArrayList添加数据
添加数据分为两种,一种是进行在ArrayList尾部进行添加数据。
一种是在ArrayList的任意位置进行添加数据。
2.6.1add函数的基本分析
add(E e)默认是将数据插入到动态数组的末尾,这段代码非常简单,就是先调用ensureCapacity看一下动态数组是否需要进行扩容,然后将element[size++] = e进行赋值即可。
add(int index, E element)就是先调用rangeCheckForAdd进行检查这个index正常吗,然后再进行调用ensureCapacityInternal看一下动态数组是否需要进行扩容,然后使用System.arraycopy配合elementData[index] = element实现数据的插入。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
这个校验也很有意思,进行插入的时候索引为负或者索引>size也会报错,这个主要是为了确保ensureCapacity代码正常执行,做校验的。
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
2.6.2两种添加元素的不同点
1.添加元素到任意位置的时候,会导致在该位置后的所有元素都需要进行重新排列。
2.添加元素到数组末尾,在没有发生扩容的情况下,是不会出现元素的赋值排序过程的。所以向ArrayList后面进行添加元素的时候只要不出现扩容的情况,性能是十分优秀的,但是如果发生扩容现象,性能就不咋地了,会发生数组的复制扩容现象。
2.6.3两种添加元素的相同点
添加元素时,都会调用ensureCapacity进行检查容量的大小,如果发现容量不足,会自动扩容为原来数组容量的1.5倍。
2.6.4ensureCapacityInternal方法探究
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return 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);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
我现在用较为通俗的语言进行介绍一下。
首先ensureCapacity进行接收一个minCapacity最小容量值(size+1)=> 其实就是当前动态数组中已经有的数据的数目。
先调用calculateCapacity(elementData, minCapacity),该函数进行接收原始elementData数据,minCapacity最小接收容量,在这个函数中先进性判断一下elementData是不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,其实就是判定这个elementData是不是初始化时的空数组,返回一个Math.max(DEFAULT_CAPACITY, minCapacity),判定是返回默认长度还是minCapacity,如果elementData已经不是初始化的默认空数组了,就直接进行返回minCapacity。
然后调用ensureExplicitCapacity,接收的参数是minCapacity,也就是计算出的最小所需容量,由于结构发生了修改,先进行将modCount进行+1(这个变量主要是进行记录ArrayList对象的结构变化次数),然后最重要的语句来了,先进行判断minCapaciity是否大于动态数组的长度,如果大于,就直接调用grow,动态数组扩容函数,接收的参数是minCapacity。
grow函数接收minCapacity,进行扩容。先使用oldCapacity进行接收elementData的长度,然后使用newCapacity进行接收扩容后数组的长度 => oldCapacity + (oldCapacity >> 1),就是扩容为oldCapacity的1.5倍。
然后我们看到newCapacity - minCapacity < 0,这句话其实就是为了处理如果扩容后,长度还是装不下这些数据,那就直接将newCapacity赋值为minCapacity即可,需要多少我给你多少。
为什么会出现扩容后长度还是不够的情况呢,其实这段代码的主要作用就是防止溢出。
看下面图,如果发生了移除,就会这样进行循环,很有可能newCapacity算出来是一个负数。
newCapacity - MAX_ARRAY_SIZE > 0主要是进行判断计算出来的值是否大于MAX_ARRAY_SIZE,如果大于了,就将minCapacity传入到hugeCapacity函数中去,进行重新计算newCapacity。
这是MAX_ARRAY_SIZE的定义。
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
hugeCapacity接收到minCapacity后,如果发现minCapacity小于0,直接抛出OutOfMemoryError错误。
返回值主要是用双目运算符,进行判断minCapacity和MAX_ARRAY_SIZE的大小,如果minCapacity大,就赋值为Integer.MAX_VALUE,如果小就赋值为MAX_ARRAY_SIZE。
最后使用Arrays.copyOf(elementData, newCapacity)进行赋值给elementData,自此,扩容结束。
画一下扩容的整体流程。
2.7ArrayList删除数据
删除数据的代码也很有意思,新增了结构改变的变量值,然后获取到了老elementData的数据,又进行获取到了需要移动的数据的值,我们看一下为什么要移动这么多变量。
如果移动的变量>0,就进行使用System.arraycopy进行移动数组。
然后将elementData[--size] = null => 因为移动数组的本质是一个一个向前复制占位,最后一个数据还是存在,需要置空。
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;
}
2.8ArrayList的Fail-Fast机制
前面我们讲了很多源码相关的知识,你会发现在新增/删除的时候都会出现modCount这个结构变化次数变量,那这个变量到底是干什么的呢?
2.8.1modCount到底是什么?
结构变化次数变量,字面意思其实就是修改结构的次数的变量,结构变化是指什么呢?
新增/删除元素,动态数组扩容,这些都是进行结构变化。
但是更新元素的数据,这种修改元素内部的行为都不是结构变化。
结构变化就是动态数组中元素个数/动态数组的长度的变化,这些才是结构变化,元素内部的变化不属于结构变化。
2.8.2从序列化的角度理解Fail-Fast机制
这是ArrayList中序列化的代码,我们将最后几段关于modCount的代码提取出来。
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
你会发现,他对比modCount是否和被期待的modCount相等,如果不相等就抛出ConcurrentModificationException错误。
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
就是你在序列化/反序列化过程中就进行发现错误了,你结构和被期待的结构不一致,我就拦截你,以最快的速度发现错误,不会给你一错千里的机会,这就是Fail-Fast机制,可以防止黑客进行偷袭程序等,有效防止黑客控制ObjectOutputStream后进行错误的操作,提前发现错误。