引言
前幾天有個讀者由於看了《ArrayList哪種遍歷效率最好,你真的弄明白了嗎?》問了個問題普通for循環ArrayList爲什麼不能刪除連續重複的兩個元素?其實這個描述是不正確的。正確的應該是普通for循環正序刪除,不能刪除連續的元素所以就產生了這個文章。
ArrayList刪除數據的方式
我們先看下ArrayList總共有幾種刪除元素的方法吧。
package com.workit.demo.array;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Predicate;
/**
* @author:
* @Date: 2020/6/9
* @Description:
*/
public class ArrayListDelete {
public static void main(String[] args) {
Predicate<String> predicate = p -> p.equals("a") || p.equals("b");
// 可以正常刪除結果正確
deleteByIterator(getList(), predicate);
// 可以正常刪除結果正確
deleteByReverseOrder(getList(), predicate);
// 可以刪除 結果不正確
deleteByOrder(getList(), predicate);
// 不能刪除 報錯java.util.ConcurrentModificationException
deleteByArrayList(getList(), predicate);
// 不能刪除 報錯java.util.ConcurrentModificationException
deleteByForeach(getList(), predicate);
//不能刪除 報錯 java.util.ConcurrentModificationException
deleteByEnhancedForLoop(getList(), predicate);
// 正常刪除數據
deleteAll(getList(), predicate);
}
public static List<String> getList() {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
return list;
}
/**
* 普通for循環倒序刪除
* 可以正常刪除 結果正確
* @param list
* @param predicate
*/
public static void deleteByReverseOrder(List<String> list, Predicate<String> predicate) {
for (int i = list.size() - 1; i >= 0; i--) {
if (predicate.test(list.get(i))) {
list.remove(list.get(i));
}
}
System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName()+":"+list.toString());
}
/**
* 普通for循環正序刪除
*可以刪除 結果不正確
* @param list
* @param predicate
*/
public static void deleteByOrder(List<String> list, Predicate<String> predicate) {
for (int i = 0; i < list.size(); i++) {
if (predicate.test(list.get(i))) {
list.remove(list.get(i));
}
}
System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName()+":"+list.toString());
}
/**
* 迭代器循環,使用ArrayList的remove()方法刪除
* 可以刪除 結果不正確
* @param list
* @param predicate
*/
public static void deleteByArrayList(List<String> list, Predicate<String> predicate) {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
if (predicate.test(iterator.next())) {
list.remove(iterator.next());
}
}
System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName()+":"+list.toString());
}
/**
* 迭代器循環,使用迭代器的remove()方法刪除
* 可以正常刪除結果正確
* @param list
* @param predicate
*/
public static void deleteByIterator(List<String> list, Predicate<String> predicate) {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
if (predicate.test(iterator.next())) {
iterator.remove();
}
}
System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName()+":"+list.toString());
}
/**
* java8 forEach方法刪除
*不能刪除 報錯 java.util.ConcurrentModificationException
* @param list
* @param predicate
*/
public static void deleteByForeach(List<String> list, Predicate<String> predicate) {
list.forEach(p -> {
if (predicate.test(p)) {
list.remove(p);
}
});
System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName()+":"+list.toString());
}
/**
* 增強版for循環刪除
*不能刪除 報錯 java.util.ConcurrentModificationException
* @param list
* @param predicate
*/
public static void deleteByEnhancedForLoop(List<String> list, Predicate<String> predicate) {
for (String string : list) {
if (predicate.test(string)) {
list.remove(string);
}
}
}
}
/**
* 調用批量刪除方法
* @param list
* @param predicate
*/
public static void deleteAll(List<String> list, Predicate<String> predicate) {
List<String> removeList = new ArrayList<>();
for (String string : list) {
if (predicate.test(string)) {
removeList.add(string);
}
}
list.removeAll(removeList);
System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName()+":"+list.toString());
}
下面我們來分析下爲什麼這些方法爲什麼有的可以正確刪除元素,有的不可以。引用大佬們經常說的一句話源碼之下無祕密那我們就把源碼搞起來吧。
增強版for循環刪除 && 迭代器循環使用ArrayList.remove()方法刪除
- 增強版for循環刪除(
deleteByEnhancedForLoop
)、迭代器循環,使用ArrayList的remove()方法刪除(deleteByArrayList
)這兩種姿勢都會拋出java.util.ConcurrentModificationException
他們本質都是迭代器循環,每次循環都會checkForComodification
這個方法檢查modCount
和expectedModCount
的值。
@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];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
而List
的刪除方法每次刪除之後modCount
都會進行加1
操作,expectedModCount
值不變還是原來的。
private void fastRemove(int index) {
modCount++; //modCount`都會進行加1操作
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
}
所以上面兩個方法都會拋出ConcurrentModificationException
異常。
java8 forEach方法刪除(拋出異常)
- java8 forEach方法刪除(
deleteByForeach
)爲什麼也會拋**ConcurrentModificationException
異常呢?答案還是在源碼裏面。
同上面一樣刪除一個元素後modCount
進行了加1
而expectedModCount
沒有變化。
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
action.accept(elementData[i]);
}
if (modCount != expectedModCount) { // 是不是又是這個判斷
throw new ConcurrentModificationException();
}
正序刪除不能刪除連續元素的原因
- 可以刪除但是結果不正確的方法for循環正序刪除(
deleteByOrder
)
先來張圖吧,看圖更直觀。
數組刪除元素後每次都需要移動。第一次刪除(i=0
)後b
的下標就爲0了,然後第二次(i=1
)進行刪除的時候是不是就成功的把b
給遺漏了。(倒序循環刪除就可以避免這種情況)那如果我們非要使用正序循環刪除數據那有什麼解決辦法嗎?辦法是有的只要在刪除後面把i的值進行修正下。代碼如下:
for (int i = 0; i < list.size(); i++) {
if (predicate.test(list.get(i))) {
list.remove(list.get(i));
// 新增這個修正i的值
i--;
}
}
是不是又get了一個騷操作。
使用迭代器的remove()方法刪除(推薦做法)
迭代器循環,使用迭代器的remove()方法刪除(deleteByIterator
)這個比較簡單我們直接看迭代器的刪除
關鍵代碼就一行 expectedModCount = modCount
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount; //調用ArrayList的刪除方法後多了這麼一句話。
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
這種方法也是《阿里開發手冊》(需要的可以公衆號回覆:泰山)推薦的。
總結
上面列舉了一系列的刪除方法,稍不小心使用不當就踩坑裏面了。這麼多我也記不住啊?最好的方法就是不要邊循環邊刪除數據。如果非要刪除咧?個人建議可以使用批量刪除方法(本人屢試不爽)或者迭代器的remove
()方法。
結束
- 由於自己才疏學淺,難免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。
- 如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。
- 感謝您的閱讀,十分歡迎並感謝您的關注。