ArrayList源码解读
一、前言
一直都只是会用ArrayList,对于ArrayList的理解都比较简单。正好借此机会也把自己观看源码的一些理解写出来。方便自己以后回顾。使用的版本是JDK1.8。
二、ArrayList特点
随机访问速度快,插入和移除性能较差(数组的特点),支持null元素,有顺序,元素可以重复,线程不安全。
三、数组扩容所使用到的API
1、直接将数组容量扩容至一个固定值。生成一个新的数组
Object[] objs = {1, 2};
System.out.println(objs.length);
Object[] objects = Arrays.copyOf(objs, 10);
System.out.println(objects.length);
2、将原数组从起始位置开始到复制的长度,替换目标数组从目标起始位置到替换的长度。
//原始数组
int [] arr={1,2,3,4,5,6,7,8};
for (int i : arr) {
System.out.print(i+",");
}
System.out.println();
//目标数组
int[] arr1=new int[11];
arr1[0]=1;
arr1[1]=2;
arr1[2]=3;
//参数 原数组 ,起始位置 ,目标数组 ,目标的起始位置 , 复制的长度 这是一个本地方法
System.arraycopy(arr,0,arr1,3,8);
//从起始位置复制,替换目标起始位置,需要复制的长度
for (int i : arr1) {
System.out.print(i+",");
}
结论:从arr数组0索引开始,copy到arr1数组从索引为3的开始。长度为8。
四、ArrayList的继承实现关系
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
- ArrayList实现了List接口是一个数组队列拥有了List基本的增删改查功能
- ArrayList实现了RandomAccess接口拥有随机读写的功能
- ArrayList实现了Cloneable接口可以被克隆
- ArrayList实现了Serializable接口并重写了序列化和反序列化方法,使得ArrayList可以拥有更好的序列化的性能
五、ArrayList类属性
//序列版本号
private static final long serialVersionUID = 8683452581122892189L;
//默认数组容量
private static final int DEFAULT_CAPACITY = 10;
//当指定数组的容量为0时使用这个常量赋值
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认空参构造函数时使用这个常量赋值
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//真正存放数据的对象数组,transient标识不被序列化
transient Object[] elementData;
//数组中的真实元素个数,该值小于或等于elementData.length
private int size;
// 最大数组容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//记录修改次数 继承于AbstractList
protected transient int modCount = 0;
六、ArrayList构造方法
//初始化设置数组集合长度
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];//初始长度大于0,就直接初始化数组容量
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;//默认复制一个空数组
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);//报错
}
}
public ArrayList() {
//默认空数组 JDK1.7之后初始化的时候不指定大小,那么集合初始大小为0
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();//转化为数组
if ((size = elementData.length) != 0) {//非空集合
if (elementData.getClass() != Object[].class)//是否转化为Object类型数组
elementData = Arrays.copyOf(elementData, size, Object[].class);//不为Object类型数组就进行复制
} else {
this.elementData = EMPTY_ELEMENTDATA;//空集合,初始为空数组
}
}
七、add方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); //真实数组元素加1 调用ensureCapacityInternal方法
elementData[size++] = e; //进行处理之后,将值复制到索引为size的位置上
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//判断
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//空数组,返回两个值最大的一个
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
//不是空数组直接返回minCapacity
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++; //修改统计数+1
// 判断数组真实元素个数加1后的长度与当前数组长度大小关系,如果小于0,返回,如果大于0就调用grow()方法
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
//原始数组的长度
int oldCapacity = elementData.length;
// 新增数组容量大小 (oldCapacity >> 1)=oldCapacity/2
int newCapacity = oldCapacity + (oldCapacity >> 1);
//这个地方有一个面试题,如果初始数组的长度1,增加的元素超过1个,那么上面计算的新数组大小也是为1的。这个判断就是如果新数组小于minCapacity,最少保证newCapacity=minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
//最多不能超过最大容量
newCapacity = hugeCapacity(minCapacity);
//复制数组从新赋值
elementData = Arrays.copyOf(elementData, newCapacity);
}
public void add(int index, E element) {
rangeCheckForAdd(index);//索引校验
//这里和上面add()处理的差不多 扩容数组
ensureCapacityInternal(size + 1);
//这个地方和上面最开始说的那种copy例子差不多,从elementData数组的index索引位置开始,copy到elementData数组索引位置为index+1。长度为size-index
//说白了就是将存在index索引位置的值一直到size-index中间所有的值。替换index-1索引位置一直到size-index的值
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;//根据索引复制
size++;
}
private void rangeCheckForAdd(int index) {
//超过数组实际长度报错
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
结论:从源码上看,两个方法大致相同,最主要的差别是System.arraycopy(elementData, index, elementData, index + 1, size - index),从index位置开始以及之后的数据,整体拷贝到index+1开始的位置,然后再把新加入的数据放在index这个位置,而之前的数据不需要移动。(这些动作比较消耗性能)
八、get方法
public E get(int index) {
rangeCheck(index);//校验索引是否越界
//根据索引取值
return elementData(index);
}
九、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);
//因为数组已经替换了,把size-1的索引位置的元素赋值为null,方便GC回收
elementData[--size] = null;
return oldValue;
}
public boolean remove(Object o) {
if (o == null) {
// 为null的时候遍历删除第一个位null的值
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
//不为null的时候通过equals判断删除
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null;
}
结论:删除其实还是使用的是java.lang.System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length),也是比较消耗性能的
十、set方法
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
结论:修改没什么好说的,索引校验,然后获取之前索引的值,替换、返回。
总结:基于数组实现的List在随机访问和遍历的效率比较高,但是往指定位置加入元素或者删除指定位置的元素效率比较低。jdk1.7之前默认初始长度为10,jdk1.8之后初始长度为0,第一次add的时候如果没有指定长度默认为10。如果数组的实际容量 + 1 大于 数组的存储容量的时候,就开始扩容,每次扩1.5 倍。