遍歷中刪除
List或Queue等資料結構中,如何一邊遍歷一遍刪除?
1. 常犯錯誤
ArrayList
可能沒遇到坑過的人會用增強for迴圈這麼寫:
public static void main(String[] args) {
1 List<Integer> list = new ArrayList<>();
2 list.add(1);
3 list.add(2);
4 list.add(3);
5
6 for (Integer value : list) {
7 if (value.equals(2)) {
8 list.remove(value);
9 }
10 }
11 System.out.println(list);
}
但是一執行,結果卻拋 java.util.ConcurrentModificationException
異常
即併發修改異常
簡單看看異常是怎麼產生的:
首先,從丟擲異常的位置入手,發現原因是因為: modCount
的值不等於 expectedModCount
modCount值是什麼呢?
The number of times this list has been structurally modified. Structural modifications are those that change the size of the list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results.
從原始碼註釋中可以看出來,表示的是 修改該表結構的次數, 包括修改列表大小等。
所以原因很簡單了: 剛開始迴圈時 modCount 與期待值相等
但是在迴圈中進行了刪除操作後,modeCount 進行了自增。
最後由於 modCount
的值不等於 expectedModCount
,丟擲異常。
HashMap:
HashMap中也同理, 如果我們簡單使用forEach,然後remove。同樣報java.util.ConcurrentModificationException
1 HashMap<Integer,String> hashMap = new HashMap<>();
2 hashMap.put(1, "張三");
3 hashMap.put(2, "李四");
4 hashMap.put(3, "王五");
5 hashMap.forEach((key, value) -> {
6 logger.info("當前值: key" + key + " value " + value) ;
7 if ((hashMap.get(key) == "李四")) {
8 hashMap.remove(key);
9 }
10 });
11 logger.info(hashMap.toString());
原因也一樣: 在 modCount 與期待值不一樣, 發生了併發修改異常
Queue
我們再來看看佇列
1 Queue<Integer> queue = new LinkedList<>();
2 queue.offer(1);
3 queue.offer(2);
4 queue.offer(3);
5
6 logger.info("刪除前" + queue.toString());
7 for(Integer value : queue) {
8 logger.info("當前值" + value);
9 if (value.equals(2)) {
11 queue.remove(value);
12 }
13 }
14 logger.info("刪除後" + queue.toString());
如果我們用 LinkList 結構作為佇列,可以成功刪除。
但是我們按理來說迴圈3次, 實際上只迴圈2次。如下圖。 所以可能會導致漏判斷。(有空在進行研究)
如果我們把 LinkList 換成 一些功能性比較好的佇列,就不會有這種情況。 比如 帶有ReentrantLock 鎖的LinkedBlockingQueue。
Queue<Integer> queue = new LinkedBlockingQueue<>();
就能正常迴圈3次, 並正常刪除。
迭代器遍歷
那麼應該使用哪些遍歷呢?
比較推薦使用Iterator迭代器進行遍歷:
使用它的 iterator.remove()方法不會產生併發修改錯誤。
1 List<Integer> list = new ArrayList<>();
2 list.add(1);
3 list.add(2);
4 list.add(3);
5 Iterator<Integer> iterator = list.iterator();
6 while (iterator.hasNext()) {
7 Integer integer = iterator.next();
8 if (integer == 2) {
9 logger.info("刪除" + integer);
10 iterator.remove();
11 }
12 }
13 System.out.println(list);
原因可以從原始碼中找到: 每次刪除一個元素,都會將 modCount
的值重新賦值給expectedModCount
,這樣2個變數就相等了,不會觸發異常。
同時好處還有一個,無論是什麼結構的資料,一般都可以用迭代器訪問。
從JDK1.8開始,可以使用removeIf()方法來代替 Iterator的remove()方法實現一邊遍歷一邊刪除,IDEA中也會提示:
其原理也是使用迭代器的remove。
mysql5.6版本新增unique失敗
專案使用jpa自動生成資料庫, 新增unique欄位的時候,發現報錯:
Specified key was too long; max key length is 767 bytes
反覆測試後,發現,Long物件的 unique 可以新增成功,String 物件的不行,然後我把 String 物件加上length限制
@Column(unique = true, nullable = false, length = 20)
private String acctName;
結果就成功了。
原因:
資料庫檢視:
原來是因為指定欄位長度過長。未指定長度的字串,預設為255 varchar,utf8mb4字符集每個 varchar 為4bytes,即為總長255x4=1020bytes,大於了767bytes。
因此,unique 欄位的最大長度為767/4 = 191 varchar。(注:utf8mb4字符集)
MySQL 5.6 版(及之前的版本)中的 767 位元組是 InnoDB 表的規定字首限制。在 MySQL 5.7 版(及更高版本)中,此限制已增加到3072 位元組。
可以選擇升級版本,或者設定長度