您的位置:首页 > 编程语言 > Java开发

ArrayList源码解析(jdk1.6)

2017-07-03 21:02 591 查看
一、定义
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable


ArrayList 是一个数组队列,相当于动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList类,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口。

要点:

1、继承于AbstractList类,实现了List接口

2、实现RandomAccess接口,提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能。

3、ArrayList中的操作不是线程安全的!所以,建议在单线程中才使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList(系列源码后续讲到)。

二、属性

// 保存ArrayList中数据的数组
private transient Object[] elementData;
// ArrayList中实际数据的数量
private int size;
要点:

1、elementData是个动态数组,我们能通过构造函数 ArrayList(int initialCapacity)来执行它的初始容量为initialCapacity;如果通过不含参数的构造函数ArrayList()来创建ArrayList,则elementData的容量默认是10。可进行动态扩容

2、有个关键字需要解释:transient。Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。简单理解为就是:序列化该对象时,某个属性不想被序列化,可加transient关键字避免。

3、注意size为动态数组的实际大小。

三、构造函数

// ArrayList带容量大小的构造函数。
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
// 新建一个数组
this.elementData = new Object[initialCapacity];
}

// ArrayList构造函数。默认容量是10。
public ArrayList() {
this(10);
}

// 构造一个包含指定元素的list,这些元素的是按照Collection的迭代器返回的顺序排列的
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}

第一个构造方法使用提供的initialCapacity来初始化elementData数组的大小。
第二个构造方法调用第一个构造方法并传入参数10,即默认elementData数组的大小为10。
第三个构造方法则将提供的集合转成数组返回给elementData(返回若不是Object[]将调用Arrays.copyOf方法将其转为Object[]),也很常用。
四、常见API
1、添加

在讲添加元素之前,我们先来看一个函数:

/**
* 数组容量检查,不够时则进行扩容
*/
public void ensureCapacity( int minCapacity) {
//对于modCount变量,我们后面会给出解释
       modCount++;
// 当前数组的长度
int oldCapacity = elementData .length;
// 最小需要的容量大于当前数组的长度则进行扩容
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
// 新扩容的数组长度为旧容量的1.5倍+1
int newCapacity = (oldCapacity * 3)/2 + 1;
// 如果新扩容的数组长度还是比最小需要的容量小,则以最小需要的容量为长度进行扩容
if (newCapacity < minCapacity)
newCapacity = minCapacity;
// minCapacity is usually close to size, so this is a win:
// 进行数据拷贝,Arrays.copyOf底层实现是System.arrayCopy()
elementData = Arrays.copyOf( elementData, newCapacity);
}
}

当进行添加元素时,需要进行是否扩容判断。当增加modCount之后,判断minCapacity(即size+1)是否大于oldCapacity(即elementData.length),若大于,则调整容量为max((oldCapacity*3)/2+1,minCapacity)(将一个集合插入到list会出现后者,比如addAll),调整elementData容量为新的容量,即将返回一个内容为原数组元素,大小为新容量的数组赋给elementData;否则不做操作。

注意:当需要扩容时,jdk1.6容量为max((oldCapacity*3)/2+1,minCapacity);

jdk1.7容量为max(oldCapacity + (oldCapacity >> 1),minCapacity)


关于添加的四个函数:

/**
* 添加一个元素
*/
public boolean add(E e) {
// 进行扩容检查
ensureCapacity( size + 1);  // Increments modCount
// 将e增加至list的数据尾部,容量+1
elementData[size ++] = e;
return true;
}

/**
* 在指定位置添加一个元素
*/
public void add(int index, E element) {
// 判断索引是否越界,这里会抛出多么熟悉的异常。。。
if (index > size || index < 0)
throw new IndexOutOfBoundsException(
"Index: "+index+", Size: " +size);

// 进行扩容检查
ensureCapacity( size+1);  // Increments modCount
// 对数组进行复制处理,目的就是空出index的位置插入element,并将index后的元素位移一个位置,共有
//size-index个元素需要进行移动
System. arraycopy(elementData, index, elementData, index + 1,
size - index);
// 将指定的index位置赋值为element
elementData[index] = element;
// list容量+1
size++;
}
/**
* 增加一个集合元素
*/
public boolean addAll(Collection<? extends E> c) {
//将c转换为数组
Object[] a = c.toArray();
int numNew = a.length ;
//扩容检查
ensureCapacity( size + numNew);  // Increments modCount
//将c添加至list的数据尾部
System. arraycopy(a, 0, elementData, size, numNew);
//更新当前容器大小
size += numNew;
return numNew != 0;
}
/**
* 在指定位置,增加一个集合元素
*/
public boolean addAll(int index, Collection<? extends E> c) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(
"Index: " + index + ", Size: " + size);

Object[] a = c.toArray();
int numNew = a.length ;
ensureCapacity( size + numNew);  // Increments modCount

// 计算需要移动的长度(index之后的元素个数)
int numMoved = size - index;
// 数组复制,空出第index到index+numNum的位置,即将数组index后的元素向右移动numNum个位置
if (numMoved > 0)
System. arraycopy(elementData
e82f
, index, elementData, index + numNew,
numMoved);

// 将要插入的集合元素复制到数组空出的位置中
System. arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
2、删除
删除操作常见API如下:

/**
* 根据索引位置删除元素
*/
public E remove( int index) {
// 数组越界检查
RangeCheck(index);

modCount++;
// 取出要删除位置的元素,供返回使用
E oldValue = (E) elementData[index];
// 计算数组要复制的数量
int numMoved = size - index - 1;
// 数组复制,就是将index之后的元素往前移动一个位置
if (numMoved > 0)
System. arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将数组最后一个元素置空(因为删除了一个元素,然后index后面的元素都向前移动了,所以最后一个就没用了),好让gc尽快回收
// 不要忘了size减一
elementData[--size ] = null; // Let gc do its work

return oldValue;
}

/**
* 根据元素内容删除,只删除匹配的第一个
*/
public boolean remove(Object o) {
// 对要删除的元素进行null判断
// 对数据元素进行遍历查找,知道找到第一个要删除的元素,删除后进行返回,如果要删除的元素正好是最后一个那就惨了,时间复杂度可达O(n) 。。。
if (o == null) {
for (int index = 0; index < size; index++)
// null值要用==比较
if (elementData [index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
// 非null当然是用equals比较了
if (o.equals(elementData [index])) {
fastRemove(index);
return true;
}
}
return false;
}

/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(int index) {
modCount++;
// 原理和之前的add一样,还是进行数组复制,将index后的元素向前移动一个位置,不细解释了,
int numMoved = size - index - 1;
if (numMoved > 0)
System. arraycopy(elementData, index+1, elementData, index,
numMoved);
//后面的元素交给垃圾回收器
       elementData[--size ] = null; // Let gc do its work
}

/**
* 数组越界检查
*/
private void RangeCheck(int index) {
if (index >= size )
throw new IndexOutOfBoundsException(
"Index: "+index+", Size: " +size);
}
上面讲增加元素可能会进行扩容,而删除元素却不会进行缩容,如果进行一次大扩容后,我们后续只使用了几个空间或者继续进行删除操作?
/**
* 将底层数组的容量调整为当前实际元素的大小,来释放空间。
*/
public void trimToSize() {
modCount++;
// 当前数组的容量
int oldCapacity = elementData .length;
// 如果当前实际元素大小 小于 当前数组的容量,则进行缩容
if (size < oldCapacity) {
elementData = Arrays.copyOf( elementData, size );
}


3、查询/更新太简单,就不贴上了。

4、是否包含:

public boolean contains(Object o) {
return indexOf(o) >= 0;
}
//找到指定对象的第一个索引位置,从前往后,判断对象是否为null
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;
}

//与前面相反,从后往前
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData [i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData [i]))
return i;
}
return -1;
}
五、Fast-Fail快速失败机制
此类的
iterator
listIterator
方法返回的迭代器是快速失败的:在创建迭代器之后,除了通过迭代器自身的
remove
add
方法从结构上对列表进行修改,否则在任何时间以任何方式对列表进行修改,迭代器都会抛出
ConcurrentModificationException
。因此,面对并发的修改,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。

注意,迭代器的快速失败行为无法得到保证,快速失败迭代器会尽最大努力抛出
ConcurrentModificationException
。迭代器的快速失败行为应该仅用于检测 bug。

前面说到
ArrayList
中定义了一个
modCount
来记录对容器进行结构修改的次数,在
add
addAll
remove

clear
clone
方法中都会引起
modCount
变化,而在创建迭代器时,会使用局部变量保存当前的
modCount
值:

private class Itr implements Iterator<E> {
int cursor;       // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
...
在进行迭代的过程中,会先检查
modCount
有没有发生变化,以此来判定是否有外部操作改变了容器:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
最后,因为
ArrayList
是非同步的,因此,在多线程环境下,如果有对容器进行结构修改的操作,则必须使用外部同步。

举个例子:移除ArrayList某一个指定元素:

import java.util.ArrayList;
public class ArrayListRemove
{
public static void main(String[] args)
{
ArrayList<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("b");
list.add("c");
list.add("c");
list.add("c");
remove(list);

for (String s : list)
{
System.out.println("element : " + s);
}
}
public static void remove(ArrayList<String> list)
{
// TODO:
}
}


如果要想删除list的b字符,有下面两种常见的错误例子:

错误写法实例一:

public static void remove(ArrayList<String> list)
{
for (int i = 0; i < list.size(); i++)
{
String s = list.get(i);
if (s.equals("b"))
{
list.remove(s);
}
}
}


错误的原因:这种最普通的循环写法执行后会发现第二个“b”的字符串没有删掉。

错误写法实例二:

public static void remove(ArrayList<String> list)
{
for (String s : list)
{
if (s.equals("b"))
{
list.remove(s);
}
}
}


错误的原因:这种for-each写法会报出著名的并发修改异常:java.util.ConcurrentModificationException。

先解释一下实例一的错误原因。翻开JDK的ArrayList源码,先看下ArrayList中的remove方法(注意ArrayList中的remove有两个同名方法,只是入参不同,这里看的是入参为Object的remove方法)是怎么实现的:

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;
}
一般情况下程序的执行路径会走到else路径下最终调用faseRemove方法:

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; // Let gc do its work
}
可以看到会执行System.arraycopy方法,导致删除元素时涉及到数组元素的移动。针对错误写法一,在遍历第一个字符串b时因为符合删除条件,所以将该元素从数组中删除,并且将后一个元素移动(也就是第二个字符串b)至当前位置,导致下一次循环遍历时后一个字符串b并没有遍历到,所以无法删除。针对这种情况可以倒序删除的方式来避免:

public static void remove(ArrayList<String> list)
{
for (int i = list.size() - 1; i >= 0; i--)
{
String s = list.get(i);
if (s.equals("b"))
{
list.remove(s);
}
}
}


因为数组倒序遍历时即使发生元素删除也不影响后序元素遍历。

接着解释一下实例二的错误原因。错误二产生的原因却是foreach写法是对实际的Iterable、hasNext、next方法的简写,问题同样处在上文的fastRemove方法中,可以看到第一行把modCount变量的值加一,但在ArrayList返回的迭代器(该代码在其父类AbstractList中):

public Iterator<E> iterator() {
return new Itr();
}
这里返回的是AbstractList类内部的迭代器实现private class Itr implements Iterator,看这个类的next方法:
public E next() {
checkForComodification();
try {
E next = get(cursor);
lastRet = cursor++;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
第一行checkForComodification方法:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
这里会做迭代器内部修改次数检查,因为上面的remove(Object)方法修改了modCount的值,所以才会报出并发修改异常。要避免这种情况的出现则在使用迭代器迭代时(显示或for-each的隐式)不要使用ArrayList的remove,改为用Iterator的remove即可。

public static void remove(ArrayList<String> list)
{
Iterator<String> it = list.iterator();
while (it.hasNext())
{
String s = it.next();
if (s.equals("b"))
{
it.remove();
}
}
}


总结:

在需要扩容的时候,调用Arrays.copyOf(),这个是创建新数组,底层也是System.arraycopy()

其他操作时用的是System.arraycopy(),直接在原数组上操作。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  arraylist 源码 jdk