在前面几篇,咱们已经学习了常见了Map,下面开始阅读实现Collection接口的常见的实现类。在有了以前源码的铺垫以后,咱们后面的阅读之路将会变得简单不少,由于不少Collection的结构与Map的相似,甚至有很多是直接用了Map里的方法。接下来让咱们一块儿来看一下ArrayList
的源码。java
顾名思义,ArrayList的结构实际就是一个Object[]
。因此它的特性很明显,插入一个元素的时候,是耗时是一个常量时间O(1),在插入n个元素的时候,须要的时间就是O(n)。其余的操做中,运行的时间也是一个线性的增加(与数组中的元素个数有关)。算法
其中值得一提的是RandomAccess
接口,该接口的目的是这么说的:数组
List
实现所使用的标记接口,用来代表其支持快速(一般是固定时间)随机访问。此接口的主要目的是容许通常的算法更改其行为,从而在将其应用到随机或连续访问列表时能提供良好的性能。dom
对于顺序访问的list,好比LinkedList,使用Iterator访问会比使用for-i来遍历list更快。这一点其实很好理解,当对于LinkedList使用get(i)的时候,因为是链表结构,因此每次都会从表头开始向下搜索,耗时确定会多。
对于实现RandomAccess这个接口的类,如ArrayList,咱们在遍历的时候,使用for(int i = 0; i < size; i++)
来遍历,其速度比使用Iterator快(接口上是这么写的)。可是笔者看源码的时候,Iterator里使用的也是i++
,这种遍历,无非是增长了fail-fast判断,估计就是这个致使了性能的差距,可是没有LinkedList这么大。笔者循环了 1000 * 1000 次,贴出比较结果,仅供参考,有兴趣的朋友们能够试一试,循环次数越多越明显:函数
----------now is arraylist---------- 使用Iterator迭代一共花了19ms时间 使用for-i迭代一共花了9ms时间 ----------now is linkedList---------- 使用Iterator迭代一共花了17ms时间 使用for-i迭代一共花了434ms时间
而其继承的AbstractList主要给ArrayList提供了诸如add
,get
,set
,remove
的集合方法。oop
//初始化默认容量 private static final int DEFAULT_CAPACITY = 10; // 空对象数组 private static final Object[] EMPTY_ELEMENTDATA = {}; // 默认容量的空对象数组 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 实际存储对象的数组 transient Object[] elementData; // 存储的数量 private int size; // 数组能申请的最大数量 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
特别说明一下: EMPTY_ELEMENTDATA 和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA是为了在调用构造方法的时候,给elementData数组初始化,当elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA
的时候,当ArrayList第一次插入元素的时候,它的数组大小将会被初始化为DEFAULT_CAPACITY。而EMPTY_ELEMENTDATA能够理解为初始化的时候size=0,下面让咱们看下构造方法,来更加清楚的理解。性能
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
当调用默认构造函数的时候,给elementData指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA。学习
public ArrayList(int initialCapacity) { // 当 initialCapacity > 0 时,初始化对应大小的数组 if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; // 为 0 时,用指向EMPTY_ELEMENTDATA } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } }
这里当initialCapacity=0的时候,就是上述提到的状况。this
public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // c.toArray不返回Object[]的时候,则进行数组拷贝 if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // 若是为空,则指向EMPTY_ELEMENTDATA this.elementData = EMPTY_ELEMENTDATA; } }
public E get(int index) { rangeCheck(index); return elementData(index); } E elementData(int index) { return (E) elementData[index]; }
get方法很简单,就是先检查index范围是否正确,正确的话从数组里取出元素。3d
private void rangeCheck(int index) { // 若是index 大于 存储的个数,则抛出异常 if (index >= size) // outOfBoundsMsg里面就是简单的字符串拼接。 throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
这里值得一提的是:这里只判断了index >= size
的状况,对于index < 0
的状况没有判断,是由于在获取数组值的时候,若是为负数会抛出ArrayIndexOutOfBoundsException异常。
在看源码以前,咱们先思考一个问题,往数组里添加元素的时候要注意什么:
知道这些须要考虑以后,咱们再来看看它的代码:
public boolean add(E e) { //对上述的3个前提进行判断 ensureCapacityInternal(size + 1); //赋值,而后指针走到下一个空位 elementData[size++] = e; return true; }
咱们接着来看ensureCapacityInternal()的方法代码:
private void ensureCapacityInternal(int minCapacity) { // 上述状况一:初始化数组的大小 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 取minCapacity和DEFAULT_CAPACITY中较大的那个 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } // 检查有没有扩容的必要 ensureExplicitCapacity(minCapacity); }
ensureCapacityInternal()方法的做用就是对构造方法初始化的数组进行处理。
再来看一下ensureExplicitCapacity():
private void ensureExplicitCapacity(int minCapacity) { // 修改次数的计数器(在AbstractList中定义的) modCount++; // 若是须要的空间大小 > 当前数组的长度,则进行扩容 if (minCapacity - elementData.length > 0) grow(minCapacity); }
ensureExplicitCapacity()检查是否须要扩容。
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; }
最后的grow()扩容就是判断有没有超过数组的最大长度,以及对应的处理。
public E remove(int index) { rangeCheck(index); // 修改计数器 modCount++; // 记录旧值,返回 E oldValue = elementData(index); // 计算要往前移动的元素个数 int numMoved = size - index - 1; //个数大于0,进行拷贝,从index+1开始,拷贝numMoved个,拷贝起始位置是index if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); // 设置为null,以便GC elementData[--size] = null; return oldValue; }
对于被删除的元素,其后面的元素须要往前移。
public boolean remove(Object o) { // 判断o为null,loop遍历找到为null的元素 if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } // 不为null } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } // 与上面的remove(int index) 相似 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; }
public E set(int index, E element) { rangeCheck(index); // 记录旧的值 E oldValue = elementData(index); //在原位置设置新的值 elementData[index] = element; return oldValue; }
设置index位置的元素值为element,返回该位置的原来的值
public boolean addAll(Collection<? extends E> c) { Object[] a = c.toArray(); int numNew = a.length; // 对于新的最小长度进行判断处理 ensureCapacityInternal(size + numNew); //将a数组,从index-0开始,拷贝numNew个,到elementData的size位置 System.arraycopy(a, 0, elementData, size, numNew); //将size增长numNew个 size += numNew; return numNew != 0; }
ArrayList在随机访问的时候,数组的结构致使访问效率比较高,可是在指定位置插入,以及删除的时候,须要移动大量的元素,致使效率低下,在使用的时候要根据场景特色来选择,另外注意循环访问的方式选择。最后谢谢各位园友观看,若是有描述不对的地方欢迎指正,与你们共同进步!