ArrayList源码分析:
一、前言
跟很多人一样,第一个使用的集合就是ArrayList,但是用了很久的集合类,从来没有系统的理解内部到底是怎么实现的。
二、ArrayList数据结构
每次面试的时候,都会有人问,集合的底层数据结构是什么,底层是怎么存储实现的。 ArrayList的数据结构如下: ArrayList的底层数据结构就是一个Object类型的数组,可以存放所有类型数据(可由下面的源码分析得出)。当你对ArrayList进行操作,相当于你对数组进行操作。
三、源码分析
1.类的继承关系
说明:
- 实现了List接口是一个数组队列拥有了List基本的增删改查功能
- 实现了RandomAccess接口拥有随机读写的功能
- 实现了Cloneable接口可以被克隆
- 实现了Serializable接口并重写了序列化和反序列化方法,使得ArrayList可以拥有更好的序列化的性能
2.ArrayList的成员变量
/**
* 定义序列化ID
*/
private static final long serialVersionUID = 8683452581122892189L;
/**
* 默认的数组存储容量,当你没定义ArrayList数组容量大小时,默认为10
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 当指定数组的容量为0,使用这个常量赋值(与下面空参定义一样)
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 默认空参构造函数时使用这个常量赋值
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 真正存放数据的对象数组
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* 数组中的真实元素个数,该值小于或等于elementData.length
*/
private int size;
/**
* 修改次数
*/
protected transient int modCount = 0;
3.构造方法
/**
* 构造函数一:指定了容量的大小
* @param initialCapacity
*/
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;
}
/**
* 构造函数三:传入集合参数的构造函数
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
public ArrayList(Collection<? extends E> c) {
elementData = ((ArrayList<?>) 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对象中的elementData中的长度是0,size是0,当进行第一次add的时候,elementData将会变成默认的长度:10.
- 如果传入int类型参数,则代表指定ArrayList的初始数组长度,传入参数如果是大于等于0,则使用用户的参数初始化,如果用户传入的参数小于0,则抛出异常。
- 如果传入带Collection对象,将collection对象转换成数组,然后将数组的地址的赋给elementData。更新size的值,同时判断size的大小,如果是size等于0,直接将空对象EMPTY_ELEMENTDATA的地址赋给elementData。如果size的值大于0,则执行Arrays.copy方法,把collection对象的内容(可以理解为深拷贝)copy到elementData中。
- 注意:this.elementData = arg0.toArray(); 这里执行的简单赋值时浅拷贝,所以要执行Arrays,copy做深拷贝
4.常用的方法
- 添加元素
(面试可常问问题,ArrayList的添加元素实现原理,他的扩容机制)
/**
* add(E e):添加元素方法
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
/**
* 如果原来的空数组,则比较加入的个数与默认个数(10)比较,取较大值
*/
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 初始化数组的大小
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 取minCapacity和DEFAULT_CAPACITY中较大的那个
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 检查有没有扩容的必要
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
/**
* 判断数组真实元素个数加1后的长度与当前数组长度大小关系
* 如果小于0,返回,如果大于0
* 则调用grow(minCapacity)方法
*/
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* 扩容方法,当数组容量不足以存储数据时,通过grow方法,将当前数组进行扩容,并通过Arrays的
* CopyOf方法将旧数组拷贝到新的大小数组
*/
private void grow(int minCapacity) {
// 记录旧的length
int oldCapacity = elementData.length;
// 扩容1.5倍, 位运算符效率更高
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 判断有没有溢出
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 判断有没有超过最大的数组大小
if (newCapacity - MAX_ARRAY_SIZE > 0)
//计算最大的容量
newCapacity = hugeCapacity(minCapacity);
// 旧数组拷贝到新的大小数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
// 最大的容量
private static int hugeCapacity(int minCapacity) {
// 大小溢出
if (minCapacity < 0)
throw new OutOfMemoryError();
// 需要的最小容量 > 数组最大的长度,则取Integer的最大值,否则取数组最大长度
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
添加元素的流程:
- ensureCapacityInternal(size+1)方法,在该方法中首先判断了当前数组是否是空数组,如果是则比较加入的个数与默认个数(10)比较,取较大值,调用2方法。
- ensureExplicitCapacity(int minCapacity)方法,在该方法中首先是对modCount+1,判断数组真实元素个数加1后的长度与当前数组长度大小关系,如果小于0,返回,如果大于0,则调用3方法。
- grow(minCapacity)方法,使用 oldCapacity + (oldCapacity >> 1)是当前数组的长度变为原来的1.5倍,再与扩容后的长度以及扩容的上限值进行对比,然后调用4方法。
- Arrays.copyOf(elementData, newCapacity)方法,该方法的底层就是调System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength))方法,把旧数据的数据拷贝到扩容后的新数组里面,返回新数组
- 然后再把新添加的元素赋值给扩容后的size+1的位置里面。
一句话总结:
ArrayList添加元素的流程为通过ensureCapacityInternal方法先判断是否为空数组添加元素,是则先添加元素,在对比加入个数与默认个数(10)比较,取最大值。不是则通过ensureExplicitCapacity方法判断当前数组大小在元素个数加1后的长度与当前数组长度大小关系,当数组元素添加个数小于数组容量时,则直接放回。当数组元素添加个数大于数组容量时,则通过grow方法将数组进行1.5倍进行扩容的方式扩容,在通过Arrays.copyOf方法,将旧数据的数据拷贝到扩容后的新数组里面,返回新数组。
- 移除
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) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
- 查询
public E get(int index) {
/**
* 检查是否越界
*/
rangeCheck(index);
/**
* 返回指定位置上的元素
*/
return elementData(index);
}
// 位置访问操作
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
- 修改
public E set(int index, E element) {
/**
* 检查是否越界
*/
rangeCheck(index);
/**
* 获取旧的元素值
*/
E oldValue = elementData(index);
/**
* 新元素赋值
*/
elementData[index] = element;
/**
* 返回旧的元素值
*/
return oldValue;
}
- 是否包含
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
/**
* 该方法分两种情况:null值和非null的遍历
* 如果查询到就返回下标位置,否则就返回-1
* 然后与0比较,大于0就存在,小于0就不存在。
*/
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
四、总结
ArrayList常常被问到有底层数据结构,添加元素流程以及扩容机制等,ArrayList在随机访问的时候,数组的结构导致访问效率比较高,但是在指定位置插入,以及删除的时候,需要移动大量的元素,导致效率低下,在使用的时候要根据场景特点来选择,另外注意循环访问的方式选择。