栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

腾讯一面!说说ArrayList的遍历foreach与iterator时remove的区别,我一脸懵逼

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

腾讯一面!说说ArrayList的遍历foreach与iterator时remove的区别,我一脸懵逼

本文基于JDK-8u261源码分析

1 简介

​ ArrayList作为最基础的集合类,其底层是使用一个动态数组来实现的,这里“动态”的意思是可以动态扩容(虽然ArrayList可以动态扩容,但却不会动态缩容)。但是与HashMap不同的是,ArrayList使用的是1.5的扩容策略,而HashMap使用的是2的方式。还有一点与HashMap不同:ArrayList的默认初始容量为10,而HashMap为16。

有意思的一点是:在Java 7之前的版本中,ArrayList的无参构造器是在构造器阶段完成的初始化;而从Java 7开始,改为了在add方法中完成初始化,也就是所谓的延迟初始化。在HashMap中也有同样的设计思路。

另外,同HashMap一样,如果要存入一个很大的数据量并且事先知道要存入的这个数据量的固定值时,就可以往构造器里传入这个初始容量,以此来避免以后的频繁扩容。


2 构造器


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);
    }
}
3 add方法 3.1 add(E e)

添加指定的元素:



public boolean add(E e) {
    //查看是否需要扩容
    ensureCapacityInternal(size + 1);
    //size记录的是当前元素的个数,这里就直接往数组最后添加新的元素就行了,之后size再+1
    elementData[size++] = e;
    return true;
}
 

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
 
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
 
 return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    //如果调用的是有参构造器,或者调用无参构造器但不是第一次进来,就直接返回size+1
    return minCapacity;
}
 

private void ensureExplicitCapacity(int minCapacity) {
    //修改次数+1(快速失败机制)
    modCount++;
 
    
    if (minCapacity - elementData.length > 0)
 grow(minCapacity);
}
 
private void grow(int minCapacity) {
    //获取扩容前的旧数组容量
    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) {
    //minCapacity对应于size+1,所以如果minCapacity<0就说明发生了数据溢出,就抛出错误
    if (minCapacity < 0)
 throw new OutOfMemoryError();
    
    return (minCapacity > MAX_ARRAY_SIZE) ?
     Integer.MAX_VALUE :
     MAX_ARRAY_SIZE;
}
3.2 add(int index, E element)

在指定的位置处添加指定的元素:



public void add(int index, E element) {
    //index参数校验
    rangeCheckForAdd(index);
 
    //查看是否需要扩容
    ensureCapacityInternal(size + 1);
    
    System.arraycopy(elementData, index, elementData, index + 1,
     size - index);
    //然后将需要插入的元素插入到上面挪出的index位置处就可以了
    elementData[index] = element;
    //最后size+1,代表添加了一个元素
    size++;
}
 

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;
}
4 get方法


public E get(int index) {
    //index参数校验
    rangeCheck(index);
 
    //获取数据
    return elementData(index);
}
 

private void rangeCheck(int index) {
    if (index >= size)
 throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
 

@SuppressWarnings("unchecked")
E elementData(int index) {
    return (E) elementData[index];
}
5 remove方法 5.1 remove(Object o)

删除指定的元素:



public boolean remove(Object o) {
    if (o == null) {
 //如果要删除的元素为null
 for (int index = 0; index < size; index++)
     //遍历数组中的每一个元素,找到第一个为null的元素
     if (elementData[index] == null) {
  
  fastRemove(index);
  return true;
     }
    } else {
 //如果要删除的元素不为null
 for (int index = 0; index < size; index++)
     //找到和要删除的元素是一致的数组元素
     if (o.equals(elementData[index])) {
  
  fastRemove(index);
  return true;
     }
    }
    
    return false;
}
 

private void fastRemove(int index) {
    //修改次数+1
    modCount++;
    //numMoved记录的是移动元素的个数
    int numMoved = size - index - 1;
    if (numMoved > 0)
 
 System.arraycopy(elementData, index + 1, elementData, index,
  numMoved);
    
    elementData[--size] = null;
}
5.2 remove(int index)

删除指定位置处的元素:



public E remove(int index) {
    //index参数校验
    rangeCheck(index);
 
    //修改次数+1
    modCount++;
    //获取指定index位置处的元素
    E oldValue = elementData(index);
 
    //numMoved记录的是移动元素的个数
    int numMoved = size - index - 1;
    if (numMoved > 0)
 
 System.arraycopy(elementData, index + 1, elementData, index,
  numMoved);
    //同上,将最后一个位置(--size)置为null
    elementData[--size] = null;
 
    //删除之后,将旧值返回就行了
    return oldValue;
}
6 不要在foreach循环里进行元素的remove/add操作

这是《阿里巴巴编码规范》中的一条。正例:

List list = new ArrayList<>();
list.add("1");
list.add("2");
 
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    if ("2".equals(item)) {
 iterator.remove();
    }
}

反例:


List 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."":()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."":()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 iterator() {
    return new Itr();
}
可以看到是返回了一个内部类Itr: 


private class Itr implements Iterator {
    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。

6.1 remove操作

首先来看一下删除“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方法中,也就不会抛出异常。

6.2 add操作

​ 然后来看一下add操作的情况,其实在第一次循环中添加元素和不是第一次循环中添加元素、从而抛出异常的原因是类似的,这里就以第一次循环中添加元素来举例:

List 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的操作,就都有可能会是抛出异常的。

6.3 迭代器操作 6.3.1 remove操作

​ 既然现在已经知道了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方法中就会返回正确的值了。

6.3.2 add操作

​ 虽然iterator方法可以提供remove操作来使删除能正确执行,但其却没有提供相关add方法的API。无妨, ArrayList中为我们提供了listIterator方法,其中就有add操作(如果一定要用迭代器方式来实现的话)。相关的示例代码如下:

List list = new ArrayList<>();
list.add("1");
list.add("2");
 
ListIterator iterator = list.listIterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    if ("1".equals(item)) {
 iterator.add(item);
    }
}
同上,首先来看一下第5行代码处的listIterator方法: 
public ListIterator listIterator() {
    return new ListItr(0);
}

​ listIterator方法返回了一个ListItr内部类。那么就来看一下ListItr的代码实现:

private class ListItr extends Itr implements ListIterator {
  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是句真理!

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号