這是最近面試時被問到的1道面試題,本篇博客對此問題進行總結分享。
1. 新手常犯的錯誤
可能很多新手(包括當年的我,哈哈)第一時間想到的寫法是下面這樣的:
public static void main(String[] args) {
List<String> platformList = new ArrayList<>();
platformList.add("博客園");
platformList.add("CSDN");
platformList.add("掘金");
for (String platform : platformList) {
if (platform.equals("博客園")) {
platformList.remove(platform);
}
}
System.out.println(platformList);
}
然后滿懷信心的去運行,結果竟然拋java.util.ConcurrentModificationException
異常了,翻譯成中文就是:並發修改異常。
是不是很懵,心想這是為什么呢?
讓我們首先看下上面這段代碼生成的字節碼,如下所示:
由此可以看出,foreach循環在實際執行時,其實使用的是Iterator
,使用的核心方法是hasnext()
和next()
。
然后再來看下ArrayList類的Iterator是如何實現的呢?
可以看出,調用next()
方法獲取下一個元素時,第一行代碼就是調用了checkForComodification();
,而該方法的核心邏輯就是比較modCount
和expectedModCount
這2個變量的值。
在上面的例子中,剛開始modCount
和expectedModCount
的值都為3,所以第1次獲取元素"博客園"是沒問題的,但是當執行完下面這行代碼時:
platformList.remove(platform);
modCount
的值就被修改成了4。
所以在第2次獲取元素時,modCount
和expectedModCount
的值就不相等了,所以拋出了java.util.ConcurrentModificationException
異常。
既然不能使用foreach來實現,那么我們該如何實現呢?
主要有以下3種方法:
- 使用Iterator的remove()方法
- 使用for循環正序遍歷
- 使用for循環倒序遍歷
接下來一一講解。
2. 使用Iterator的remove()方法
使用Iterator的remove()方法的實現方式如下所示:
public static void main(String[] args) {
List<String> platformList = new ArrayList<>();
platformList.add("博客園");
platformList.add("CSDN");
platformList.add("掘金");
Iterator<String> iterator = platformList.iterator();
while (iterator.hasNext()) {
String platform = iterator.next();
if (platform.equals("博客園")) {
iterator.remove();
}
}
System.out.println(platformList);
}
輸出結果為:
[CSDN, 掘金]
為什么使用iterator.remove();
就可以呢?
讓我們看下它的源碼:
可以看出,每次刪除一個元素,都會將modCount
的值重新賦值給expectedModCount
,這樣2個變量就相等了,不會觸發java.util.ConcurrentModificationException
異常。
3. 使用for循環正序遍歷
使用for循環正序遍歷的實現方式如下所示:
public static void main(String[] args) {
List<String> platformList = new ArrayList<>();
platformList.add("博客園");
platformList.add("CSDN");
platformList.add("掘金");
for (int i = 0; i < platformList.size(); i++) {
String item = platformList.get(i);
if (item.equals("博客園")) {
platformList.remove(i);
i = i - 1;
}
}
System.out.println(platformList);
}
這種實現方式比較好理解,就是通過數組的下標來刪除,不過有個注意事項就是刪除元素后,要修正下下標的值:
i = i - 1;
為什么要修正下標的值呢?
因為剛開始元素的下標是這樣的:
第1次循環將元素"博客園"刪除后,元素的下標變成了下面這樣:
第2次循環時i的值為1,也就是取到了元素”掘金“,這樣就導致元素"CSDN"被跳過檢查了,所以刪除完元素后,我們要修正下下標,這也是上面代碼中i = i - 1;
的用途。
4. 使用for循環倒序遍歷
使用for循環倒序遍歷的實現方式如下所示:
public static void main(String[] args) {
List<String> platformList = new ArrayList<>();
platformList.add("博客園");
platformList.add("CSDN");
platformList.add("掘金");
for (int i = platformList.size() - 1; i >= 0; i--) {
String item = platformList.get(i);
if (item.equals("掘金")) {
platformList.remove(i);
}
}
System.out.println(platformList);
}
這種實現方式和使用for循環正序遍歷類似,不過不用再修正下標,因為剛開始元素的下標是這樣的:
第1次循環將元素"掘金"刪除后,元素的下標變成了下面這樣:
第2次循環時i的值為1,也就是取到了元素”CSDN“,不會導致跳過元素,所以不需要修正下標。
5. 評論區釋疑(2020-06-15更新)
5.1 使用removeIf()方法(推薦)
從JDK1.8開始,可以使用removeIf()
方法來代替 Iterator
的remove()
方法實現一邊遍歷一邊刪除,其實,IDEA中也會提示:
所以原來的代碼:
Iterator<String> iterator = platformList.iterator();
while (iterator.hasNext()) {
String platform = iterator.next();
if (platform.equals("博客園")) {
iterator.remove();
}
}
就可以簡化為如下所示的1行代碼,非常簡潔:
platformList.removeIf(platform -> "博客園".equals(platform));
看下removeIf()方法的源碼,會發現其實底層也是用的Iterator
的remove()
方法:
5.2 使用for循環正序遍歷,是否需要修正下標?
先說結論:需要。
不過之前文中舉得例子不是太好,所以好多讀者看完認為不修正下標也是可以的,其實不是,我們換個例子來理解:
List<String> platformList = new ArrayList<>();
platformList.add("博客園");
platformList.add("博客園");
platformList.add("CSDN");
platformList.add("掘金");
for (int i = 0; i < platformList.size(); i++) {
String item = platformList.get(i);
if ("博客園".equals(item)) {
platformList.remove(i);
}
}
System.out.println(platformList);
輸出結果:
[博客園, CSDN, 掘金]
可以發現,如果不修正下標,第2個元素“博客園”在循環遍歷時被跳過了,也就無法刪除,所以一定要修正下標: