本文基于JDK-8u261源码分析java
ArrayList做为最基础的集合类,其底层是使用一个动态数组来实现的,这里“动态”的意思是能够动态扩容(虽然ArrayList能够动态扩容,但却不会动态缩容)。可是与HashMap不一样的是,ArrayList使用的是1.5的扩容策略,而HashMap使用的是2的方式。还有一点与HashMap不一样:ArrayList的默认初始容量为10,而HashMap为16。面试
有意思的一点是:在Java 7以前的版本中,ArrayList的无参构造器是在构造器阶段完成的初始化;而从Java 7开始,改成了在add方法中完成初始化,也就是所谓的延迟初始化。在HashMap中也有一样的设计思路。算法
另外,同HashMap同样,若是要存入一个很大的数据量而且事先知道要存入的这个数据量的固定值时,就能够往构造器里传入这个初始容量,以此来避免之后的频繁扩容。数组
/** * ArrayList: * 无参构造器 */ public ArrayList() { //DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空实现“{}”,这里也就是在作初始化 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } /** * 有参构造器 */ public ArrayList(int initialCapacity) { if (initialCapacity > 0) { //initialCapacity>0就按照这个容量来初始化数组 this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { //EMPTY_ELEMENTDATA也是一个空实现“{}”,这里也是在作初始化 this.elementData = EMPTY_ELEMENTDATA; } else { //若是initialCapacity为负数,则抛出异常 throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); } }
添加指定的元素:数据结构
/** * ArrayList: */ public boolean add(E e) { //查看是否须要扩容 ensureCapacityInternal(size + 1); //size记录的是当前元素的个数,这里就直接往数组最后添加新的元素就好了,以后size再+1 elementData[size++] = e; return true; } /** * 第6行代码处: */ private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private static int calculateCapacity(Object[] elementData, int minCapacity) { /* minCapacity = size + 1 以前说过,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空实现“{}”,这里也就是在判断是否是调用的无参构造器 并第一次调用到此处 */ if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { /* 若是是的话就返回DEFAULT_CAPACITY(10)和size+1之间的较大者。也就是说,数组的最小容量是10 这里有意思的一点是:调用new ArrayList<>()和new ArrayList<>(0)两个构造器会有不一样的默认容量(在HashMap中 也是如此)。也就是说无参构造器的初始容量为10,而传进容量为0的初始容量为1。同时这也就是为何会有 EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA这两个常量的存在,虽然它们的值都是“{}” 缘由就在于无参构造器和有参构造器彻底就是两种不一样的实现策略:若是你想要具体的初始容量,那么就调用有参构造器吧, 即便传入的是0也是符合这种状况的;而若是你不在意初始的容量是多少,那么就调用无参构造器就好了,这会给你默 认为10的初始容量 */ return Math.max(DEFAULT_CAPACITY, minCapacity); } //若是调用的是有参构造器,或者调用无参构造器但不是第一次进来,就直接返回size+1 return minCapacity; } /** * 第16行代码处: */ private void ensureExplicitCapacity(int minCapacity) { //修改次数+1(快速失败机制) modCount++; /* 若是+1后指望的容量比实际数组的容量还大,就须要扩容了(若是minCapacity也就是size+1后发生了数据溢出, 那么minCapacity就变为了一个负数,而且是一个接近int最小值的数。而此时的elementData.length也会是一个接近 int最大值的数,那么该if条件也有可能知足,此时会进入到grow方法中的hugeCapacity方法中抛出溢出错误) */ if (minCapacity - elementData.length > 0) grow(minCapacity); } private void grow(int minCapacity) { //获取扩容前的旧数组容量 int oldCapacity = elementData.length; //这里扩容后新数组的容量是采用旧数组容量*1.5的方式来实现的 int newCapacity = oldCapacity + (oldCapacity >> 1); /* 若是新数组容量比+1后指望的容量还要小,此时把新数组容量修正为+1后指望的容量(对应于newCapacity为0或1的状况) 这里以及后面的判断使用的都是“if (a - b < 0)”形式,而不是常规的“if (a < b)”形式是有缘由的, 缘由就在于须要考虑数据溢出的状况:若是执行了*1.5的扩容策略后newCapacity发生了数据溢出,那么它就同样 变为了一个负数,而且是一个接近int最小值的数。而minCapacity此时也一定会是一个接近int最大值的数, 那么此时的“newCapacity - minCapacity”计算出来的结果就可能会是一个大于0的数。因而这个if条件 就不会执行,而是会在下个条件中的hugeCapacity方法中处理这种溢出的问题。这同上面的分析是相似的 而若是这里用的是“if (newCapacity < minCapacity)”,数据溢出的时候该if条件会返回true,因而 newCapacity会错误地赋值为minCapacity,而没有使用*1.5的扩容策略 */ if (newCapacity - minCapacity < 0) newCapacity = minCapacity; /* 若是扩容后的新数组容量比设定好的容量最大值(Integer.MAX_VALUE - 8)还要大,就从新设置一下新数组容量的上限 同上面的分析,若是发生数据溢出的话,这里的if条件也多是知足的,那么也会走进hugeCapacity方法中去处理 */ if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); /* 能够看到这里是经过Arrays.copyOf(System.arraycopy)的方式来进行数组的拷贝, 容量是扩容后的新容量newCapacity,将拷贝后的新数组赋值给elementData便可 */ elementData = Arrays.copyOf(elementData, newCapacity); } /** * 第83行代码处: */ private static int hugeCapacity(int minCapacity) { //minCapacity对应于size+1,因此若是minCapacity<0就说明发生了数据溢出,就抛出错误 if (minCapacity < 0) throw new OutOfMemoryError(); /* 若是minCapacity大于MAX_ARRAY_SIZE,就返回int的最大值,不然返回MAX_ARRAY_SIZE 无论返回哪一个,这都会将newCapacity从新修正为一个大于0的数,也就是处理了数据溢出的状况 其实从这里能够看出:本方法中并无使用*1.5的扩容策略,而只是设置了一个上限而已。可是在Java中 真能申请获得Integer.MAX_VALUE这么大的数组空间吗?其实不见得,这只是一个理论值。实际上须要考虑 -Xms和-Xmx等一系列JVM参数所设置的值。因此这也可能就是MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8) 其中-8的含义吧。但无论如何,当数组容量达到这么大的量级时,乘不乘1.5其实已经不过重要了) */ return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
在指定的位置处添加指定的元素:并发
/** * ArrayList: */ public void add(int index, E element) { //index参数校验 rangeCheckForAdd(index); //查看是否须要扩容 ensureCapacityInternal(size + 1); /* 这里数组拷贝的意义,就是将index位置处以及后面的数组元素日后移动一位,以此来挪出一个位置 System.arraycopy是直接对内存进行复制,在大数据量下,比for循环更快 */ System.arraycopy(elementData, index, elementData, index + 1, size - index); //而后将须要插入的元素插入到上面挪出的index位置处就能够了 elementData[index] = element; //最后size+1,表明添加了一个元素 size++; } /** * 第6行代码处: * 检查传入的index索引位是否越界,若是越界就抛异常 */ private void rangeCheckForAdd(int index) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } private String outOfBoundsMsg(int index) { return "Index: " + index + ", Size: " + size; }
/** * ArrayList: */ public E get(int index) { //index参数校验 rangeCheck(index); //获取数据 return elementData(index); } /** * 第6行代码处: * 这里只检查了index大于等于size的状况,而index为负数的状况 * 在elementData方法中会直接抛出ArrayIndexOutOfBoundsException */ private void rangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } /** * 第8行代码处: * 能够看到,这里是直接从elementData数组中获取指定index位置的数据 */ @SuppressWarnings("unchecked") E elementData(int index) { return (E) elementData[index]; }
删除指定的元素:工具
/** * ArrayList: */ public boolean remove(Object o) { if (o == null) { //若是要删除的元素为null for (int index = 0; index < size; index++) //遍历数组中的每个元素,找到第一个为null的元素 if (elementData[index] == null) { /* 删除这个元素,并返回true。这里也就是在作清理的工做:遇到一个为null的元素就清除掉 注意这里只会清除一次,并不会所有清除 */ fastRemove(index); return true; } } else { //若是要删除的元素不为null for (int index = 0; index < size; index++) //找到和要删除的元素是一致的数组元素 if (o.equals(elementData[index])) { /* 找到了一个就进行删除,并返回true。注意这里只会找到并删除一个元素, 若是要删除全部的元素就调用removeAll方法便可 */ fastRemove(index); return true; } } /* 若是要删除的元素为null而且找不到为null的元素,或者要删除的元素不为null而且找不到和要删除元素相等的数组元素, 就说明此时不须要删除元素,直接返回false就好了 */ return false; } /** * 第14行和第26行代码处: */ private void fastRemove(int index) { //修改次数+1 modCount++; //numMoved记录的是移动元素的个数 int numMoved = size - index - 1; if (numMoved > 0) /* 这里数组拷贝的意义,就是将index+1位置处以及后面的数组元素往前移动一位, 这会将index位置处的元素被覆盖,也就是作了删除 */ System.arraycopy(elementData, index + 1, elementData, index, numMoved); /* 由于上面是左移了一位,因此最后一个位置至关于腾空了,这里也就是将最后一个位置(--size)置为null 固然若是上面计算出来的numMoved自己就小于等于0,也就是index大于等于size-1的时候(大于不太可能, 是属于异常的状况),意味着不须要进行左移。此时也将最后一个位置置为null就好了。置为null以后, 原有数据的引用就会被断开,GC就能够工做了 */ elementData[--size] = null; }
删除指定位置处的元素:源码分析
/** * ArrayList: */ public E remove(int index) { //index参数校验 rangeCheck(index); //修改次数+1 modCount++; //获取指定index位置处的元素 E oldValue = elementData(index); //numMoved记录的是移动元素的个数 int numMoved = size - index - 1; if (numMoved > 0) /* 同上面fastRemove方法中的解释,这里一样是将index+1位置处以及后面的数组元素往前移动一位, 这会将index位置处的元素被覆盖,也就是作了删除(这里是否能够考虑封装?) */ System.arraycopy(elementData, index + 1, elementData, index, numMoved); //同上,将最后一个位置(--size)置为null elementData[--size] = null; //删除以后,将旧值返回就好了 return oldValue; }
这是《阿里巴巴编码规范》中的一条。正例:大数据
List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); if ("2".equals(item)) { iterator.remove(); } }
反例:this
List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); for (String item : list) { if ("2".equals(item)) { list.remove(item); } }
运行上面的代码能够看到,使用迭代器的删除操做是不会有问题、能成功删除的;而使用foreach循环进行删除则会抛出ConcurrentModificationException异常,但若是使用foreach循环删除第一个元素“1”的时候又会发现不会抛出异常。那么这究竟是为何呢?
众所周知,foreach循环是一种语法糖,那么其背后究竟是如何来实现的呢?将上面反例的代码反编译后的结果以下:
public class com.hys.util.Test { public com.hys.util.Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: new #2 // class java/util/ArrayList 3: dup 4: invokespecial #3 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #4 // String 1 11: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z 16: pop 17: aload_1 18: ldc #6 // String 2 20: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z 25: pop 26: aload_1 27: invokeinterface #7, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator; 32: astore_2 33: aload_2 34: invokeinterface #8, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z 39: ifeq 72 42: aload_2 43: invokeinterface #9, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object; 48: checkcast #10 // class java/lang/String 51: astore_3 52: ldc #6 // String 2 54: aload_3 55: invokevirtual #11 // Method java/lang/String.equals:(Ljava/lang/Object;)Z 58: ifeq 69 61: aload_1 62: aload_3 63: invokeinterface #12, 2 // InterfaceMethod java/util/List.remove:(Ljava/lang/Object;)Z 68: pop 69: goto 33 72: return }
上面的内容不须要彻底看懂,只须要看到第23行代码处、第26行代码处和第29行代码处后面的解释,也就是foreach循环是经过调用List.iterator方法来生成一个迭代器,经过Iterator.hasNext方法和Iterator.next方法来实现的遍历操做(普通的for循环不是经过这种方式,也就是说普通的for循环不会有这种问题)。
那么首先来看一下ArrayList中iterator方法的实现:
public Iterator<E> iterator() { return new Itr(); }
能够看到是返回了一个内部类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; Itr() {} 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]; } //... }
而抛出异常是在上面第17行代码处的checkForComodification方法里面抛出的,下面来看一下它的实现:
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
能够看到若是modCount和expectedModCount不等就会抛出ConcurrentModificationException异常。而上面说过,在add方法和remove方法中,会对modCount修改标志位作+1的操做。这里的modCount是为了作快速失败用的。快速失败指的是若是在遇到并发修改时,迭代器会快速地抛出异常,而不是在未来某个不肯定的时间点冒着任意而又不肯定行为的风险来进行操做,也就是将可能出现的bug点推前。在包括HashMap在内的绝大部分集合类都是有快速失败机制的。注意:这里的并发修改指的并不都是发生在并发时的修改,也有多是在单线程中所作的修改致使的,就如同上面的反例同样。
这里拿上面的反例来举例,ArrayList调用了两次add方法,也就是此时的modCount应该为2。而expectedModCount如上所示,一开始会初始化为modCount的值,也就是也为2。
首先来看一下删除“2”的状况:
第一次循环:
由于此时的modCount和expectedModCount都为2,因此第一次循环中不会抛出异常,抛出异常都是发生在不是第一次循环的状况中。在next方法走完后,foreach循环方法体中的remove方法的if条件判断不知足,就结束了本次循环。
第二次循环:
第二次循环的hasNext和next方法都是能成功走完的,在这以后会进入到foreach循环方法体中的remove方法中,进行删除元素。而此时的size-1变为了1。上面分析过,在remove方法中的fastRemove方法中,会对modCount+1,也就变为了3。
第三次循环:
而后会走入到第三次循环中的hasNext方法中。按照正常的状况下该方法是会返回false的,但由于此时的size已经变为了1,而此时的cursor为2(cursor表明下一次的索引位置),因此二者不等,错误地返回了true,因此会继续走入到next方法中的checkForComodification方法中,判断此时的modCount和expectedModCount是否相等。由于此时的modCount已经变为了3,和expectedModCount的值为2不等,因此在此抛出了ConcurrentModificationException异常。
再来看一下删除“1”的时候为何不会抛出异常:
第一次循环:
同上,此时的modCount和expectedModCount都为2,因此第一次循环中的hasNext和next方法都不会抛异常。在这以后会进入到foreach循环方法体中的remove方法中,进行删除元素。同上,size-1变为了1,而modCount+1变为了3。
第二次循环:
在第二次循环的hasNext方法中,此时的cursor为1,而size也是1,二者相等。因此hasNext方法返回false,就跳出了foreach循环,不会走到随后的next方法中,也就不会抛出异常。
而后来看一下add操做的状况,其实在第一次循环中添加元素和不是第一次循环中添加元素、从而抛出异常的缘由是相似的,这里就以第一次循环中添加元素来举例:
List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); for (String item : list) { if ("1".equals(item)) { list.add(item); } }
第一次循环:
同上,此时的modCount和expectedModCount都为2,因此第一次循环中的hasNext和next方法都不会抛异常。在这以后会进入到foreach循环方法体中的add方法中,进行添加元素。size+1变为了3,而modCount+1也变为了3。
第二次循环:
在第二次循环的hasNext方法中,此时的cursor为1,而size为3,二者不等,因此hasNext方法返回true,会走到随后的next方法中。而在next方法中的checkForComodification方法中,此时的modCount已经变为了3,而expectedModCount仍是为2。二者不等,因此在此抛出了ConcurrentModificationException异常。
其实从上面的几回分析中就能够看出:只要在foreach循环方法体中有进行修改过modCount和size的操做,就都有可能会是抛出异常的。
既然如今已经知道了foreach循环中使用remove/add操做抛出异常的缘由,那么就能够分析一下为何使用迭代器进行相关操做就不会有问题呢?上面正例代码中的第5行代码处的iterator方法、第6行和第7行代码处的hasNext和next方法都是跟foreach循环里的实现是同样的,而区别在于第9行代码处的remove操做。这里的remove不是ArrayList中的remove操做,而是Itr内部类中的remove操做:
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(); } }
能够看到第7行代码处是调用了ArrayList的remove操做进行删除的,但同时注意第10行代码处会将expectedModCount更新为此时modCount的最新值,这样在next方法中就不会抛出异常了;在第8行代码处会将cursor更新为lastRet(lastRet表明上一次的索引位置),即将cursor-1(由于此时要remove,因此cursor指针须要减一)。这样在hasNext方法中就会返回正确的值了。
虽然iterator方法能够提供remove操做来使删除能正确执行,但其却没有提供相关add方法的API。无妨, ArrayList中为咱们提供了listIterator方法,其中就有add操做(若是必定要用迭代器方式来实现的话)。相关的示例代码以下:
List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); ListIterator<String> iterator = list.listIterator(); while (iterator.hasNext()) { String item = iterator.next(); if ("1".equals(item)) { iterator.add(item); } }
同上,首先来看一下第5行代码处的listIterator方法:
public ListIterator<E> listIterator() { return new ListItr(0); }
listIterator方法返回了一个ListItr内部类。那么就来看一下ListItr的代码实现:
private class ListItr extends Itr implements ListIterator<E> { ListItr(int index) { super(); cursor = index; } //... public void add(E e) { checkForComodification(); try { int i = cursor; ArrayList.this.add(i, e); cursor = i + 1; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } }
能够看到ListItr内部类是继承了Itr类,包括hasNext和next等方法都是直接复用的。而在add方法中的第14行代码处,是调用了ArrayList的add操做进行添加的。另外和Itr的remove方法同样,第17行代码处也是在更新expectedModCount为此时modCount的最新值,第15行代码处的cursor更新为+1后的结果(由于此时是在作add操做)。这样后续的hasNext和next操做就不会有问题了。
在应用业务里待过久不少底层的东西每每容易忽略掉,今年的年初计划是把经常使用的JDK源码工具作一次总结,眼看年末将近,乘着最近有空,赶忙的给补上。在这行干的越久真是越以为:万丈高楼平地起,这绝B是句真理!
每一次总结都是对知识点掌握程度的审视,技术不易,每日精进一点,与你们共勉。