前言
上一篇文章探究了Collection接口的详细设计和相关的方法。文章的链接如下:
http://blog.csdn.net/kiritor/article/details/8869937,接下来将会探究其子接口List的源码,通过
源码探究其接口设计和特点。
List接口设计
通过查阅源码,我们知道的是List接口继承至Collection接口,并且实现了集合中元素的有序存
储。也就是说用户可以对列表中的每个元素的插入位置进行精确的控制,可以根据元素在集合中
的索引对元素进行访问和遍历(类似数组)。
List接口源代码解析
对于List接口的设计,我们直接看源码和API来的更加实际一些。
package com.kiritor; import java.util.Iterator; import java.util.ListIterator; /** * List源码探究 * @author Kiritor*/ public interface List<E> extends Collection<E> { /**List作为Collection的子接口 * 提供了Collection接口定义的方法 * 这些方法在Collection源码学习中 * 已经分析过了,就不在说明了 * */ int size(); boolean isEmpty(); boolean contains(Object o); Iterator<E> iterator(); Object[] toArray(); <T> T[] toArray(T[] a); boolean add(E e); boolean remove(Object o); boolean containsAll(Collection<?> c); boolean addAll(Collection<? extends E> c); boolean addAll(int index, Collection<? extends E> c); boolean removeAll(Collection<?> c); boolean retainAll(Collection<?> c); void clear(); boolean equals(Object o); int hashCode(); /**同时List接口定义了一些自己的方法 * 来实现“有序”这一功能特点*/ /** *返回列表中指定索引的元素 *return E *throws IndexOutofBoundException(index<0||index>=size()) * */ E get(int index); /** *设定某个列表索引的值 *throws: *UnsupportedOperationException ClassCastException NullPointerException IllegalArgumentException IndexOutOfBoundsException * */ E set(int index, E element); /** *在指定位置插入元素,当前元素和后续元素后移 *这是可选操作,类似于顺序表的插入操作 */ void add(int index, E element); /** * 删除指定位置的元素(可选操作)*/ E remove(int index); /** * 获得指定对象的最小索引位置, * 没有则返回-1*/ int indexOf(Object o); /**获得指定对象的最大索引位置 * 可以知道的是若集合中无给定元素的重复元素下 * 其和indexOf返回值是一样的*/ int lastIndexOf(Object o); /**一种更加强大的迭代器,支持向前遍历,向后遍历 * 插入删除操作,此处不解释*/ ListIterator<E> listIterator(); ListIterator<E> listIterator(int index); /** * 返回索引fromIndex(包括)和toIndex(不包括) * 之间的视图。*/ List<E> subList(int fromIndex, int toIndex); }
ArrayList类实现
对于ArrayList类的实现笔者就不将代码一一贴出来了,这里只是简要的对一些个方法的
实现做一些分析。首先我们看看ArrayList类头吧
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable可以看出的是他实现了一些额外的接口RandomAccess、Cloneable、Serializable
RandomAccess:List实现使用的标记接口,主要用于提高连续或随机访问列表时的性能。
Cloneable:实现该接口的类可以对该类的实例进行克隆(按字段进行复制)
Serializable:用于标识可序列化(何为序列化?为什么要序列化,有待总结)
1、底层实现
通过对其源码的观察,可以发现的是ArrayList的底层是通过数组来实现的。
private transient Object[] elementData; private int size;这里解释一下transient关键字 :
Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的
对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭
serialization,可以在这个域前加上关键字transient。
2、构造方法
通过查看源码,我们可以知道的是ArrayList提供三种构造方法
public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; } public ArrayList() { this(10); } 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); }前两种构造方法很容易理解,根据提供的参数来进行数组空间分配,默认大小为10,
第三种构造则显得稍微复杂些,它是将给定集合转换成数组(按照集合本身的顺序),如果
转化成的emlementData不是Object[]类型则调用Arrays.copyOf方法将其转换为Object[]。
Tips:需要特别注意前两个构造方法和第三个构造方法的区别,前两者生成的集合是
无元素的,代码中也没有对size进行修改的地方,第三个构造器对size是有修改的。
3、存储元素
ArrayList提供了set(int index, E element)、add(E e)、add(int index, E element)、
addAll(Collection<? extends E> c)、addAll(int index, Collection<? extends E> c)这些添加
元素的方法。下面我们一一讲解:
/* * 用指定的元素代替集合中指定位置的元素 * 并返回以前位于该位置的 */ public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; }
set方法会通过rangeCheck()函数检查index的返回,若index>=size则会主动抛出
IndexOutOfBoundsException异常,不过需要注意的是对于index<0的情况并不会主动抛出
该异常,且两种情况抛出的异常详细信息不同(如何不同读者可自己尝试),这对于我们以后
ArrayList下表越界错误的查找也算有一定的帮助吧,助于说为什么JDK对于该方法为什么
不将index<0情况考虑进去,可能是因为对于数组越界的问题,我们更加关心的是“向前越界”
// 将指定的元素添加到此列表的尾部。 public boolean add(E e) { ensureCapacity(size + 1); //该方法用于调整数组的容量 elementData[size++] = e; return true; }ensureCapacity(size+1)这个方法主要用于数组容量的扩容的,至于如何实现,具体后面
会详细分析。
// 将指定的元素插入此列表中的指定位置。 // 如果当前位置有元素,则向右移动当前位于该位置的元素以及所有后续元素(将其索引加1)。 public void add(int index, E element) { if (index > size || index < 0) throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size); // 如果数组长度不足,将进行扩容。 ensureCapacity(size+1); // Increments modCount!! // 将 elementData中从Index位置开始、长度为size-index的元素, // 拷贝到从下标为index+1位置开始的新的elementData数组中。 // 即将当前位于该位置的元素以及所有后续元素右移一个位置。 System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; }
// 按照指定collection的迭代器所返回的元素顺序,将该collection中的所有元素添加到此列表的尾部。 public boolean addAll(Collection<? extends E> c) { Object[] a = c.toArray(); int numNew = a.length; ensureCapacity(size + numNew); // Increments modCount System.arraycopy(a, 0, elementData, size, numNew); size += numNew; return numNew != 0; }
// 从指定的位置开始,将指定collection中的所有元素插入到此列表中。 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 int numMoved = size - index; if (numMoved > 0) System.arraycopy(elementData, index, elementData, index + numNew, numMoved); System.arraycopy(a, 0, elementData, index, numNew); size += numNew; return numNew != 0; }
4、读取元素
//返回列表中指定位置的元素,需边界检查 public E get(int index) { rangeCheck(index); return elementData(index); }
5、删除元素
ArrayList提供了根据下标或者指定对象两种方式来删除元素代码如下:// 移除此列表中指定位置上的元素,并返回,需边界转换 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; // Let gc do its work return oldValue; }
// 移除此列表中首次出现的指定元素(如果存在)。这是应为ArrayList中允许存放重复的元素。 //该方法通过对象删除,不许进行边界检查 public boolean remove(Object o) { // 由于ArrayList中允许存放null,因此下面通过两种情况来分别处理。 if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { // 类似remove(int index),移除列表中指定位置上的元素。 fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; }第二个方法牵扯到了ArrayList中的一个私有的方法,该方法和remove(int index)方法
类似,但是并不需要进行边界检查(通过remove(Object o)已经可以index的合理性了),容易
明白的是该方法不应该直接调用,因此设定为private的,对象引用无法直接调用它,安全性得以
保障。
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 }Tips:从数组中移除一个元素也会导致后面的元素左移。
6、调整容量
很久以前我们就知道ArrayList等集合都是动态的,那么这种动态是如何来实现的?通过 查阅源码,这种动态性的实现主要有下面两个方法实现的。
public void ensureCapacity(int minCapacity) { if (minCapacity > 0) ensureCapacityInternal(minCapacity); } private void ensureCapacityInternal(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); }
其工作过程以add方法为例
// 将指定的元素添加到此列表的尾部。 public boolean add(E e) { ensureCapacity(size + 1); //该方法用于调整数组的容量 elementData[size++] = e; return true; }
在进行元素尾部插入的时候,首先确保插入元素后集合的size也就是minCapacity必须<=
集合的容量,也就是elementData.lentgh,如果大于集合的容量之后则需通过grow方法
进行扩容。阅读下面方法很容易知道ArrayList是按照自己的50%扩容的。
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }思考:ArrayList进行扩容的时候,会将就数据中的元素重新拷贝一份到新的数组中去,这种
代价是挺高的,因此我们应该尽量避免数组的扩容。而且需注意一点的是ensureCapacity()方法
是public的,我们可以通过它手动的增加容量。
而且ArrayList提供了一个有趣的方法,它可以将底层数组的容量调整为集合中实际元素的数量
public void trimToSize() { modCount++; int oldCapacity = elementData.length; if (size < oldCapacity) { elementData = Arrays.copyOf(elementData, size); } }
7、Fail-Fast(快速失败)机制
仔细观察上述的各个方法,我们在源码中就会发现一个特别的属性modCount,API解释如下:
The number of times this list has been structurally modified. Structural modifications are those
that change the size of the list, or otherwise perturb it in such a fashion that iterations in progress
may yield incorrect results.
记录修改此列表的次数:包括改变列表的结构,改变列表的大小,打乱列表的顺序等使正在进行
迭代产生错误的结果。Tips:仅仅设置元素的值并不是结构的修改
我们知道的是ArrayList是线程不安全的,如果在使用迭代器的过程中有其他的线程修改了List就会
抛出ConcurrentModificationException这就是Fail-Fast机制。
那么快速失败究竟是个什么意思呢?
在ArrayList类创建迭代器之后,除非通过迭代器自身remove或add对列表结构进行修改,否则在其他
线程中以任何形式对列表进行修改,迭代器马上会抛出异常,快速失败。
8、迭代器遍历
我们知道Collection接口都有产生迭代器的接口,作为List接口的具体实现类ArrayList来说肯定是
少不了的,而且ArrayList同时还提供一个ListIterator实现更为灵活的遍历,这里我们就Iterator做一些
探究吧。看看产生Iterator的方法
/*返回一个迭代器接口的具体实现类*/ public Iterator<E> iterator() { return new Itr(); } /** * An optimized version of AbstractList.Itr */ 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; public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }通过阅读源码,很容易就知道了,通过迭代器我们并不需要知道集合的底层实现,我们只需
生成一个迭代器对象,通过迭代器对象提供的方法,实现集合的遍历等操作。
留意到的是checkForComodification可以知道,当多个线程同时操作一个ArrayList造成
modeCount不一致时,会抛出ConcurrentModificationException异常,仔细观察又可以发现的是
迭代器每一个方法执行都需要调用checkForComodification()方法,这样就实现了上面所说的
Fail-Fast(快速失败),相较于迭代完成之后抛出异常更合理些。
9、一个小"陷阱"
通过阅读源码笔者还发现了一个有意思的问题,那就是subList(index1,index2)方法,返回一个
从index1(包括)开始到index2(不包括)的子列表。通过观察可发现的是这里子列表的幕后还是原
列表,对其的修改也会造成原列表的修改。
如果不想出现这种情况,很简单 new ArrayList(list.subList(index1,index2));就ok了。
好了,对于List、ArrayList的整理就到这个地方了,对于一些细节的东西笔者难免有些遗漏的
地方,希望给予指出与交流!