文章目錄
- 快速失敗機制(fail-fast)
- for-each刪除元素為什么報錯
- 原因分析
- 邏輯分析
- 如何正確的刪除元素
- remove 后 break
- for 循環
- 使用 Iterator
- 總結
快速失敗機制(fail-fast)
In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process. Such designs often check the system’s state at several points in an operation, so any failures can be detected early. The responsibility of a fail-fast module is detecting errors, then letting the next-highest level of the system handle them.
這是快速失敗機制的英文解釋。翻譯過來就是:系統設計中,“fail-fast”指的是一種策略,系統或模塊被設計成在出現錯誤或失敗時立即檢測并報告。這種方法旨在通過停止正常操作而不是繼續可能存在缺陷的過程來最小化失敗的影響。fail-fast系統通常在操作的多個點檢查系統狀態,以便及早發現任何失敗。fail-fast模塊的責任是檢測錯誤,然后讓系統的更高級別處理它們。
這段話的大致意思就是,fail-fast 是一種通用的系統設計思想,一旦檢測到可能會發生錯誤,就立馬拋出異常,程序將不再往下執行
很多時候,我們會把 fail-fast 歸類為 Java 集合框架的一種錯誤檢測機制,但其實 fail-fast 并不是 Java 集合框架特有的機制
for-each刪除元素為什么報錯
下面這段代碼:
List<String> list = new ArrayList<>();list.add("1");list.add("2");list.add("3");for (String str : list) {if ("1".equals(str)) {list.remove(str);}}System.out.println(list);
在執行完之后就會報錯
看一下報錯的原因是在checkForComodification這里報的錯。下面是具體的代碼
final void checkForComodification() {if (modCount != expectedModCount)throw new ConcurrentModificationException();}
也就是說,remove 的時候觸發執行了 checkForComodification 方法,該方法對 modCount 和 expectedModCount 進行了比較,發現兩者不等,就拋出了 ConcurrentModificationException 異常。
原因分析
為什么會執行checkForComodification 方法呢?是因為for-each的底層是迭代器Iterator配合while來實現的
List<String> list = new ArrayList();
list.add("1");
list.add("2");
list.add("3");
Iterator var2 = list.iterator();while(var2.hasNext()) {String str = (String)var2.next();if ("1".equals(str)) {list.remove(str);}
}System.out.println(list);
看一下list的迭代器,點進iterator這個方法,發現它實現了Iterator接口
再去看一下 Itr 這個類。
private class Itr implements Iterator<E> {int cursor; // index of next element to returnint lastRet = -1; // index of last element returned; -1 if no suchint expectedModCount = modCount;// prevent creating a synthetic constructorItr() {}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();}}@Overridepublic void forEachRemaining(Consumer<? super E> action) {Objects.requireNonNull(action);final int size = ArrayList.this.size;int i = cursor;if (i < size) {final Object[] es = elementData;if (i >= es.length)throw new ConcurrentModificationException();for (; i < size && modCount == expectedModCount; i++)action.accept(elementAt(es, i));// update once at end to reduce heap write trafficcursor = i;lastRet = i - 1;checkForComodification();}}final void checkForComodification() {if (modCount != expectedModCount)throw new ConcurrentModificationException();}}
也就是說 new Itr() 的時候 expectedModCount 被賦值為 modCount,而 modCount 是 ArrayList 中的一個計數器,用于記錄 ArrayList 對象被修改的次數。ArrayList 的修改操作包括添加、刪除、設置元素值等。每次對 ArrayList 進行修改操作時,modCount 的值會自增 1。
在迭代 ArrayList 時,如果迭代過程中發現 modCount 的值與迭代器的 expectedModCount 不一致,則說明 ArrayList 已被修改過,此時會拋出 ConcurrentModificationException 異常。這種機制可以保證迭代器在遍歷 ArrayList 時,不會遺漏或重復元素,同時也可以在多線程環境下檢測到并發修改問題。
邏輯分析
List<String> list = new ArrayList<>();list.add("1");list.add("2");list.add("3");for (String str : list) {if ("1".equals(str)) {list.remove(str);}}System.out.println(list);
由于 list 此前執行了 3 次 add 方法。
- add 方法調用 ensureCapacityInternal 方法
- ensureCapacityInternal 方法調用ensureExplicitCapacity 方法
- ensureExplicitCapacity 方法中會執行 modCount++
所以 modCount 的值在經過三次 add 后為 3,于是 new Itr() 后 expectedModCount 的值也為 3(回到前面去看一下 Itr 的源碼)。
接著來執行 for-each 的循環遍歷。
執行第一次循環時,發現“沉默王二”等于 str,于是執行 list.remove(str)。
- remove 方法調用 fastRemove 方法
- fastRemove 方法中會執行 modCount++
modCount 的值變成了 4。
第二次遍歷時,會執行 Itr 的 next 方法(String str = (String) var3.next();),next 方法就會調用 checkForComodification 方法。
此時 expectedModCount 為 3,modCount 為 4,就只好拋出 ConcurrentModificationException 異常了。
如何正確的刪除元素
remove 后 break
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");for (String str : list) {if (1".equals(str)) {list.remove(str);break;}
}
break 后循環就不再遍歷了,意味著 Iterator 的 next 方法不再執行了,也就意味著 checkForComodification 方法不再執行了,所以異常也就不會拋出了。
但是呢,當 List 中有重復元素要刪除的時候,break 就不合適了。
for 循環
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
for (int i = 0; i < list.size(); i++) {String str = list.get(i);if ("1".equals(str)) {list.remove(str);}
}
for 循環雖然可以避開 fail-fast 保護機制,也就說 remove 元素后不再拋出異常;但是呢,這段程序在原則上是有問題的。為什么呢?
第一次循環的時候,i 為 0,list.size() 為 3,當執行完 remove 方法后,i 為 1,list.size() 卻變成了 2,因為 list 的大小在 remove 后發生了變化,也就意味著“2”這個元素被跳過了。能明白嗎?
remove 之前 list.get(1) 為“2”;但 remove 之后 list.get(1) 變成了“3”,而 list.get(0) 變成了“2”
使用 Iterator
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");Iterator<String> itr = list.iterator();while (itr.hasNext()) {String str = itr.next();if ("1".equals(str)) {itr.remove();}
}
為什么使用 Iterator 的 remove 方法就可以避開 fail-fast 保護機制呢?看一下 remove 的源碼就明白了。
public void remove() {if (lastRet < 0) // 如果沒有上一個返回元素的索引,則拋出異常throw new IllegalStateException();checkForComodification(); // 檢查 ArrayList 是否被修改過try {ArrayList.this.remove(lastRet); // 刪除上一個返回元素cursor = lastRet; // 更新下一個元素的索引lastRet = -1; // 清空上一個返回元素的索引expectedModCount = modCount; // 更新 ArrayList 的修改次數} catch (IndexOutOfBoundsException ex) {throw new ConcurrentModificationException(); // 拋出異常}
}
刪除完會執行 expectedModCount = modCount,保證了 expectedModCount 與 modCount 的同步
總結
在使用 foreach 循環(或稱 for-each 循環)遍歷集合時,通常不能直接刪除集合中的元素,原因如下:
Concurrent Modification Exception:
當使用 foreach 循環遍歷集合時,集合的結構不能被修改(例如添加或刪除元素),否則會導致 ConcurrentModificationException 異常。這是因為 foreach 循環在背后使用迭代器來遍歷集合,而迭代器在遍歷時會維護一個 expected modCount(修改計數器),如果在遍歷過程中修改了集合的結構,迭代器會檢測到并拋出異常。
Invalidation of Iterator:
刪除元素后,集合的結構發生變化,這可能會使當前的迭代器失效。如果集合的結構發生了變化,迭代器可能無法正確遍歷集合的剩余部分或者導致未定義行為。
Potential Logical Errors:
直接在 foreach 循環內刪除元素可能會導致邏輯錯誤。例如,如果不正確地更新迭代器或集合的大小,可能會導致遍歷的元素不完整或錯誤。
為了安全地從集合中刪除元素,應該使用迭代器的 remove() 方法。迭代器的 remove() 方法允許在遍歷時安全地刪除當前元素,同時更新集合的結構和迭代器的狀態,避免了上述問題。