你真的了解for循環遍歷么
今天講的for循環主要是針對Java語言的JDK1.8,在編程過程中或多或少的遇到過for循環遍歷,比如:List、Set、Map等等集合容器,有時候碰到需要對集合容器數據進行相應的增刪改操作的時候,都會糾結一番到底會不會出現修改問題呢,如何遍歷會更好呢。
等看完這篇你會覺得真的不一樣了。
日常遍歷的幾種方式
首先我們先了解一下集合容器中日常遍歷的幾種方式:
List集合遍歷方式(ArrayList)
// 遍歷list集合 private static void listTest() { List<String> list = new ArrayList<String>(); list.add("liubei"); list.add("guanyu"); list.add("zhangfei"); // 使用傳統for循環進行遍歷 for (int i = 0, size = list.size(); i < size; i++) { String value = list.get(i); System.out.println(value); } // 使用增強for循環進行遍歷 與iterator迭代器一致 for (String value : list) { System.out.println(value); } // 使用iterator遍歷 Iterator<String> it = list.iterator(); while (it.hasNext()) { String value = it.next(); System.out.println(value); } //ArrayList 繼承父類 Iterable 重寫forEach 方法,進行相應校驗判斷,傳統fori循環調用accept輸出 list.forEach(new Consumer<String>() { @Override public void accept(String key) { System.out.println(key); } }); //Lambda 函數式Consumer list.forEach(key -> { System.err.println(key); }); }
Set集合遍歷方式(HashSet)
private static void setTest() { Set<String> set = new HashSet<String>(); set.add("JAVA"); set.add("C"); set.add("C++"); // 使用iterator遍歷set集合 Iterator<String> it = set.iterator(); while (it.hasNext()) { String value = it.next(); System.out.println(value); } // 使用增強for循環遍歷set集合 字節碼查看底層實際也是Iterator迭代器實現,與上面一樣,寫法區別而已 for (String s : set) { System.out.println(s); } //HashSet 繼承父類 Iterable 直接調用父類forEach循環Consumer this 迭代器 循環accept方式 set.forEach(new Consumer<String>() { @Override public void accept(String key) { System.err.println(key); } }); //Lambda 函數式Consumer set.forEach(key -> { System.err.println(key); }); }
Map集合容器的遍歷方式(HashMap)
public static void mapTest() { Map<String, String> maps = new HashMap<String, String>(); maps.put("1", "PHP"); maps.put("2", "Java"); maps.put("3", "C"); maps.put("4", "C++"); maps.put("5", "HTML"); Set<Map.Entry<String, String>> set = maps.entrySet(); //取key的增強遍歷 實際 迭代器行為 Set<String> keySet = maps.keySet(); for (String s : keySet) { String key = s; String value = maps.get(s); System.out.println(key + " : " + value); } // 增強循環 實際 迭代器行為 for (Map.Entry<String, String> entry : set) { String key = entry.getKey(); String value = entry.getValue(); System.out.println(key + " : " + value); } // 迭代器遍歷。 Iterator<Map.Entry<String, String>> it = set.iterator(); while (it.hasNext()) { Map.Entry<String, String> entry = (Map.Entry<String, String>) it.next(); String key = entry.getKey(); String value = entry.getValue(); System.out.println(key + " : " + value); } //HashMap 重寫Map接口的forEach默認實現,進行相應的判斷,傳統fori遍歷數組形式 maps.forEach(new BiConsumer<String, String>() { @Override public void accept(String key, String value) { System.err.println(key + " : " + value); } }); //Lambda 函數式Consumer maps.forEach((key,value)->{ System.err.println(key + " : " + value); }); }
遍歷的問題
1、用那種遍歷更好呢!
2、遍歷的時候能操作(增刪)集合信息么!
3、遍歷中斷、跳過怎么玩的!
每次擼代碼的時候都會或多或少的思考一下這些問題,這也是基本工。
第一個問題:用那種遍歷更好呢!
個人理解,主要還是要看業務中需要遍歷的是什么類型的集合(數組),內部需要怎么操作,有時候就是需要獲取根據下標位置進行業務邏輯處理,那就需要傳統的for循環了。若是只是遍歷進行key處理不涉及下標位置的,一般會選擇foreach形式,比較簡單快捷,其內部原理還是Iterator迭代器行為,Iterator一般不寫主要原因比較麻煩復雜了點。
第二個問題:遍歷的時候能操作(增刪)集合信息么!
這個問題是本文中主要的部分,也是大多數人都會思考的問題,但是好像好多時候都理解錯了。接下來我要顛覆認知的操作了。(也有可能是小丑)
在遍歷增加刪除的時候,首先大部分人都會想用那種遍歷好,那種不會報錯呢!報錯的原因都以為是數組大小等問題,借着網上一堆解釋糊弄了自己,結果一群人都被糊弄了。
舉幾個栗子:
ArrayList的傳統的fori方式
應該都知道這個方式的增刪沒有問題吧
若是將傳統for循環換成這樣的,就會出現意想不到意思了--》【死循環】
出現以上問題的原因是,list的add每次添加的時候是都會將size增加【size++】,所有判斷一直有效,死循環。remove的時候會對size遞減【--size】,並不會出現null的出現,但是elementData數組大小是沒變的,判斷的是size。
說明在傳統的for循環中,對集合的操作沒有任何限制,只是寫法問題會出現邏輯死循環。
ArrayList的foreach和Iterator方式
通過查看字節碼信息了解到這兩種方式其實是一樣的原理
以下是上面的是字節碼體現,可根據行號,對號入座,可以發現原理是一致的。
所有對這個的研究就直接針對迭代器就OK了,先看看ArrayList是否有對迭代器進行實現重寫。一看還真有,對hasNext()、next()、remove()都進行了重寫,在迭代器中沒有元素的添加add行為,那我們來看看這些迭代器為啥有時候出問題有時候不會出問題。
其實關鍵這些都是圍繞這modCount 屬性做各種判斷檢查,主要意思是監控集合被修改的次數。
在ArrayList中使用迭代器遍歷,迭代器在初始化的時候就將modCount屬性賦值給迭代器自身的expectedModCount屬性,需要仔細好好的看看源碼,了解其中設計思想。
看看hasNext(),主要原理是看看cursor索引是否是到最后(size)了
public boolean hasNext() { return cursor != size; }
next(),主要原理是檢查元素是否被修改、索引的大小、與內部數組大小的比較
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]; }
我們在看一下checkForComodification()方法就大概知道啥意思了
final void checkForComodification() { //檢查集合的修改次數和迭代器預期的次數(初始化賦值那個)是否一致 if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
最后看一下迭代器中的remove()方法,主要先檢查是否有並發修改問題,然后利用ArrayList自身的remove()方法進行刪除,修改modCount,並賦值給expectedModCount,差不多就是哪些俗稱Fail-Fast以便下次checkForComodification()方法檢查時不會出現問題。
public void remove() { // if (lastRet < 0) throw new IllegalStateException(); //檢查是否被修改過 checkForComodification(); try { //調用ArrayList的自身的remove刪除元素 ArrayList.this.remove(lastRet); //將索引值賦值為當前索引值,因為next的時候cursor++了 cursor = lastRet; //防止同一次遍歷過程中刪除兩次 lastRet = -1; //將ArrayList中修改過的modCount 重新賦值給迭代器expectedModCount屬性 expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } }
總結:從以上代碼我們可以很容易的知道,在foreach和迭代器中刪除元素時不會出現問題的,原因是ArrayList自身實現迭代器Iterator進行了一些邏輯處理,迭代器檢查並調用ArrayList的刪除方法,修改modCount的值,主要是modCount的靈活運用。但是對於添加元素add,迭代器中未做相關處理,所有會出現modCount的修改,並未同步給迭代器的expectedModCount屬性,導致會出現同步修改問題ConcurrentModificationException。
ArrayList的foreach方法
應該知道集合循環有foreach方式底層原理是迭代器Iterator行為,但ArrayList中有一個foreach方法真實存在的,是實現Iterable重寫foreach方法。
其實大部分的集合容器都有foreach方法,也比較實用,使用方式在上面ArrayList遍歷方式中已經寫過。
主要原理跟迭代器的remove有些類似,也是fail-fast行為策略,判斷這個過程值modCount是否變化。
@Override public void forEach(Consumer<? super E> action) { Objects.requireNonNull(action); //將modCount值先保存一下 final int expectedModCount = modCount; @SuppressWarnings("unchecked") final E[] elementData = (E[]) this.elementData; final int size = this.size; //傳統的for循環,每次循環還要判讀modCount是否變化了 for (int i=0; modCount == expectedModCount && i < size; i++) { //業務邏輯點 action.accept(elementData[i]); } //判斷這個過程中modCount是否變化了 if (modCount != expectedModCount) { //變化,則拋出異常 throw new ConcurrentModificationException(); } }
ArrayList使用Lambda的foreach函數方式
先說一下Lambda的實現原理
- 在類編譯時,動態生成會生成一個私有靜態方法+一個內部類;
- 在內部類中實現了函數式接口,在實現接口的方法中,會調用編譯器生成的靜態方法,這個靜態方法與遍歷對象的方法一致;
- 在使用lambda表達式的地方,通過傳遞內部類實例,來調用函數式接口方法。
參考地址:https://blog.csdn.net/jiankunking/article/details/79825928
從以上可以理解到,其實Lambda的函數式是根據集合方法實現個殼,內部還是調用了集合foreach方法進行遍歷的,類似動態代理行為。
所以其原理和遍歷策略是與集合ArrayList的內部foreach方法一致。
總結
1、以上只是針對ArrayList進行了深入分析,每個集合都有自己相對應的foreach方法和Iterator迭代器的實現,所以是否遍歷有問題,遍歷時的集合操作是否有問題,需要根據不同的集合類型進行不同的判斷,而不是一味的理解操作的時候為foreach方法就是有問題,迭代器Iterator就是不會出現問題,傳統for循環不好啥的,一定得有一股勁深入研究,才會撥開雲霧。
2、還有好多栗子:如CopyOnWriteArrayList與ArrayList又有所不同,HashMap也不一樣,每個都有自己的個性,可以查看源碼,
3、這些都是對Collection或者Iterator進行相應的實現,其中差不多都是跟modCount有着千絲萬縷的關系,又有着所謂的fail-fast機制。