ArrayList是Java中最基础的数据结构之一,即顺序表。本篇文章将从源码角度简单介绍ArrayList的基本实现原理。
(本文内容中涉及的源码使用JDK1.6版本,在高版JDK中可能源码做了简单调整,但数据结构的实现机制依然是一样的)
顺序表,顾名思义,是一个有序的数组。数据按顺序在内存中存储,这样的数据结构利于快速的查找,但在数组中间插入或删除数据会导致整个数组发生变动。
ArrayList类中,核心变量有两个:
private transient Object[] elementData;
存储数据的数组,ArrayList存储能力取决于此数组总长度(注:transient修饰词指此变量不会被序列化)
private transient Object[] elementData;
ArrayList的真实长度,即其中存放的数据个数
ArrayList共有3个构造方法:
public ArrayList() {
this(10);
}
默认构造方法,10为默认的初始长度
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
}
使用此构造方法,可以为
ArrayList
指定一个初始长度。
当你已知数据长度超过10时候,使用此方法可以减少ArrayList扩容次数。
当初始值小于0时候,会抛出异常。
public ArrayList(Collection extends E> c) {
elementData = c.toArray();
size = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
当你已经有一个ArrayList时候,使用此方法形成一个新的ArrayList。注释的意思是,此处的toArray方法可能会错误地没有返回一个Object类型数组,因此在下面进行判断,若不是,则转为Object类型数组。
ArrayList中最常用的方法包括:add、set、get、remove,这些方法可能有不同的参数而形成了多个重载函数。
add方法:
public boolean add(E e)
public void add(int index, E element)
第一个方法:
public boolean add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
return true;
}
首先,此方法调用了ensureCapacity方法,它的意思是,如果当数组容量不足的时候,需要扩充容量(后面会介绍)。之后只要简单的将新的数据添加到数组中即可。
注意,此方法一定返回true。
第二个方法:
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(
"Index: "+index+", Size: "+size);
ensureCapacity(size+1);
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
此方法中,指定了插入数据的位置。首先对index进行判断,当其值为负或大于当前数组长度时候,会抛出异常。(数组中最后一个数据的位置对应的是size-1)。
当index合法时候,判断是否需要扩容,之后将执行arraycopy这个方法,此方法是个native方法(由C语言封装),它的意思是将elementData这个数组中从index开始的数据复制到从index+1开始(相当于从index开始后移一位)。
当我执行add(2, 10)的时候,数组会发生如下变动:如图:
原数组 |
23 |
24 |
25 |
26 |
1 |
2 |
13 |
|
|
位移后 |
23 |
24 |
25 |
25 |
26 |
1 |
2 |
13 |
|
完成 |
23 |
24 |
10 |
25 |
26 |
1 |
2 |
13 |
|
由此可见,数组发生了较大的变动,index后面的数据全部平移了1位。因此如果数据大量插入,会耗费一定时间。
set方法:
public E set(int index, E element)
set只有一个方法,即修改index的值为element。
它的实现:
public E set(int index, E element) {
RangeCheck(index);
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}
RangeCheck方法即监测index是否越界(后面会介绍)。之后只要将对应位置数据修改了即可。此方法会返回oldValue,即原来index位置的值
get方法:
public E get(int index)
get只有一个方法,即获取index位置的值。
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
很简单,只是返回这个值即可。当然,也是要先经过RangeCheck,看index是否越界。
remove方法:
public E remove(int index)
public boolean remove(Object o)
第一个方法:
public E remove(int index) {
RangeCheck(index);
modCount++;
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null;
return oldValue;
}
凡是方法中参数涉及index,都要经过RangeCheck判断是否越界。
此方法中有一个值为modCount,这是父类中的一个值,具体作用下面会介绍。
remove方法核心就是,根据index取得这个原先的值,然后将其删除。删除的本质是通过移动表来实现的,同样使用到了arraycopy这个方法。
举例:
当我执行remove(2)的时候,数组会发生如下变动:如图:
原数组 |
23 |
24 |
25 |
26 |
1 |
2 |
13 |
|
|
位移后 |
23 |
24 |
26 |
1 |
2 |
13 |
13 |
|
|
完成 |
23 |
24 |
26 |
1 |
2 |
13 |
置空 |
|
|
numMoved表示如果我删除的是数组中的最后一个值,直接置空即可,不需要移动数据。
删除同样可能导致数据的移动,因此大量删除操作会耗费一定时间。
第二个方法:
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;
}
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;
}
另一个remove方法是根据值去删除数据,先遍历查到要删除的值的index,然后调用fastRemove方法删除,可以看到,fastRemove就是remove第一个方法中的一部分,通过移动数组来删除数据,和上面是完全一样的。
除了上述的基本方法外,ArrayList还有一些其他常用方法,如:
public boolean addAll(Collection extends E> c)
public boolean addAll(int index, Collection extends E> c)
public List subList(int fromIndex, int toIndex)
addAll方法主要使用arraycopy实现,arraycopy具体内容上面已有介绍。
subList方法是ArrayList的父类AbstractList中的内部类方法,具体实现并不复杂,因此不再多说,有兴趣可以去看一看。
下面补充一下上面涉及到的一些方法和变量的介绍。
1、ensureCapacity
2、RangeCheck
3、modCount
public void ensureCapacity(int minCapacity) {
modCount++;
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
int newCapacity = (oldCapacity * 3)/2 + 1;
if (newCapacity < minCapacity)
newCapacity = minCapacity;
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
ensureCapacity,从名字上就可以看出,此方法是确认空间是否足够,即是否越界。
首先获取数组长度,之后判断传入的参数minCapacity是否大于数组长度,如果大于的话,将计算新的长度,是old * 3 / 2 + 1;即原来的1.5倍+1的,如果变化后的值依然较小,即将新长度设置为minCapacity。
随后执行copyOf方法生成一个新的组数,此数组长度为newCapacity,并将之前elementData中的值复制到其中,这样来完成扩容。
private void RangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(
"Index: "+index+", Size: "+size);
}
RangeCheck方法较为简单,只是比较index和size的值的关系,如果index大于了当前值,那么抛出越界异常。
modCount
这个是一个较为重要的值。它字面意思为modify count,即修改次数。
当数组进行了诸如添加、删除之类的操作,此值会变化。
那么问题是,它到底有什么用?
我们知道ArrayList有一种foreach的循环方式:
for (Integer i : list) {
}
如果在迭代过程中,进行对ArrayList的修改操作,如add remove等,那么将报出ConcurrentModificationExceptions
这个异常的意思是同时进行迭代和修改而抛出的异常。它的大致原因是,在迭代时候,修改数据导致ArrayList长度发生了变化,因此在check时候会抛出这个异常。此处暂时不对具体源码进行分析。
如果需要在循环中删除某个元素,应当如下写法:
int i = 0;
while (i < list.size()) {
if (list.get(i) == 5) {
list.remove(i);
} else {
i++;
}
}