阿里Java开发手册剖析:
-
6.5【强制】ArrayList 的 subList 结果不可强转成 ArrayList,否则会抛出 ClassCastException
-
6.8【强制】在subList场景中,高度注意对父集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生 ConcurrentModificationException
-
6.9【强制】使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为 0 的空数组。
-
6.11【强制】使用工具类 Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法, 它的 add/remove
-
6.14【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式
-
6.16【推荐】集合泛型定义时,在 JDK7 及以上,使用 diamond 语法或全省略。 说明:菱形泛型,即 diamond,直接使用<>来指代前边已经指定的类型。
【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。
这里主要包含三层意思:
- 不要在 foreach 循环里进行元素的 remove/add 操作。
- 使用 Iterator方式 可以remove元素
- 高并发下,使用 Iterator方式时,需要加锁。
第3个好理解,直说前两个。
foreach中remove/add操作会报异常
演示demo:
private static void test57() {
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
for (String s : list) {
if ("2".equals(s)) {
list.remove(s);
}
}
System.out.println(list.toString());
日志输出:
异常意思是有多处在同时修改。那么是不是可以得出结论:
foreach 循环里进行元素的 remove/add 操作,就会报
java.util.ConcurrentModificationException
呢
再看下面例子,就知道没这么简单,我们就if("2".equals(s))
改成if("1".equals(s))
。移除第一个元素:
private static void test57() {
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
for (String s : list) {
if ("1".equals(s)) {
list.remove(s);
}
}
System.out.println(list.toString());
日志输出:
[2]
Process finished with exit code 0
发现这次并没报异常了。好奇下我改了下demo,list中添加了第三个元素,依旧移除第一元素:
private static void test57() {
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("3");
for (String s : list) {
if ("1".equals(s)) {
list.remove(s);
}
}
System.out.println(list.toString());
此时又报
java.util.ConcurrentModificationException
异常了。
所以上面问题可以分为两个:
- foreach中remove/add为什么会报ConcurrentModificationException异常
- foreach中remove倒数第二个元素为什么又不会报ConcurrentModificationException异常
foreach中remove/add为什么会报ConcurrentModificationException异常
我们可以先找到异常的抛出地方,然后再反推他的调用栈。除了直接看异常日志,还可以借助debug工作:
1.在任意断点处右击,然后点击More按钮(或者直接快捷键)
2.添加一个新断点
3.选择为一个java异常断点
- 搜索具体异常类型:java.util.ConcurrentModificationException
- 确认
6.debug程序:
7.发生异常后,程序会自动断在异常处
- 同时可以看到调用栈:
我们看到forech内部遍历调用了Iterator。异常也是在Iterator.next()方法中触发的。 我将程序的字节码打出来,看下foreach到底是怎么执行:
public class rongcheng.collection.arrayList.ConcurrentModification {
public rongcheng.collection.arrayList.ConcurrentModification();
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: ldc #7 // String 3
29: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
34: pop
35: aload_1
36: invokeinterface #8, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
41: astore_2
42: aload_2
43: invokeinterface #9, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
48: ifeq 81
51: aload_2
52: invokeinterface #10, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
57: checkcast #11 // class java/lang/String
60: astore_3
61: ldc #4 // String 1
63: aload_3
64: invokevirtual #12 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
67: ifeq 78
70: aload_1
71: aload_3
72: invokeinterface #13, 2 // InterfaceMethod java/util/List.remove:(Ljava/lang/Object;)Z
77: pop
78: goto 42
81: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream;
84: aload_1
85: invokevirtual #15 // Method java/lang/Object.toString:()Ljava/lang/String;
88: invokevirtual #16 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
91: return
}
看到第36行,当foreach开始的时候,通过List.iterator()的方法获得了一个Iterator对象,后面又有Iterator.hasNext()
和Iterator.next()
。结合异常的触发在Iterator.next()
。我大胆地猜测for (String s : list) {}
的本质就是:
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
...
}
我将上面foreach代码改成Iterator实现,并输出字节码返现确实一样:
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("3");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (item.equals("1")) {
iterator.remove();
}
}
System.out.println(list.toString());
}
字节码:
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: ldc #7 // String 3
29: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
34: pop
35: aload_1
36: invokeinterface #8, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
41: astore_2
42: aload_2
43: invokeinterface #9, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
48: ifeq 79
51: aload_2
52: invokeinterface #10, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
57: checkcast #11 // class java/lang/String
60: astore_3
61: aload_3
62: ldc #4 // String 1
64: invokevirtual #12 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
67: ifeq 76
70: aload_2
71: invokeinterface #13, 1 // InterfaceMethod java/util/Iterator.remove:()V
76: goto 42
79: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream;
82: aload_1
83: invokevirtual #15 // Method java/lang/Object.toString:()Ljava/lang/String;
86: invokevirtual #16 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
89: return
查看ArrayList源码看到内部确实有iterator()
方法,这验证了上面猜测了。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
/**
* Returns an iterator over the elements in this list in proper sequence.
*
* <p>The returned iterator is <a href="#fail-fast"><i>fail-fast</i></a>.
*
* @return an iterator over the elements in this list in proper sequence
*/
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;
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];
}
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();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
}
总结:ArrayList内部有个modCount变量,用来记录整个List的结构被修改的次数(比如:ArrayList.remove(),ArrayList.add()都会造成modeCount++), Itr内部有个expectedModCount:预期的改变次数,每次Iterator.next()会先检测modCount==expectedModCount,如果两者不同则>会报java.util.ConcurrentModificationException异常。modCount!=expectedModCount的原因是,list.remove导致modeCount++了。
同时上面源码也解释了为什么使用Iterator遍历元素时,remove元素后不会导致ConcurrentModificationException
。
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();
}
}
我们可以看到Iterator的
remove()
的本质还是调用了ArrayList.this.remove()
。 第一次checkForComodification();
时,两个count都没改变。而执行ArrayList.this.remove()
后她立即将expectedModCount = modCount
进行同步,所以后续执行checkForComodification()
方法不会再抛异常。
不过此处并非原子操作,所以高并发时是可能会报异常。所以前面第三条建议是:高并发情况下,需要对 Iterator 对象加锁
foreach中remove倒数第二个元素为什么又不会报ConcurrentModificationException异常
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
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;
}
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; // clear to let GC do its work
}
可以看到不管哪种形式的remove都会导致ArrayList.this的--size
。而这步结束后,会进行下一轮循环。此时会调用Iterator.hasNext()
方法:
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
Itr() {}
public boolean hasNext() {
return cursor != size;
}
}
此时就导致return false
。因此本次遍历就会结束从而导致不会继续后面逻辑(Iterator.next()
),也就无法触发ConcurrentModificationException
下面demo可以验证上述说法:
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("3");
for (String s : list) {
System.out.println(s);
if ("2".equals(s)) {
list.remove(s);
}
}
}
日志输出:
1
2
Process finished with exit code 0
removeIf
java 8 提供了更简单安全的方式
list.removeIf(item -> item.equals("1"));