ArrayList源码解析(jdk1.6)

一、定义
 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, 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快速失败机制
此类的 iteratorlistIterator 方法返回的迭代器是快速失败的:在创建迭代器之后,除了通过迭代器自身的removeadd 方法从结构上对列表进行修改,否则在任何时间以任何方式对列表进行修改,迭代器都会抛出ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
注意,迭代器的快速失败行为无法得到保证,快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。迭代器的快速失败行为应该仅用于检测 bug。

前面说到 ArrayList中定义了一个 modCount来记录对容器进行结构修改的次数,在addaddAllremoveclearclone方法中都会引起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(),直接在原数组上操作。


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值