你真的了解for循環遍歷么(Java集合容器)


你真的了解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的實現原理

  1. 在類編譯時,動態生成會生成一個私有靜態方法+一個內部類;
  2. 在內部類中實現了函數式接口,在實現接口的方法中,會調用編譯器生成的靜態方法,這個靜態方法與遍歷對象的方法一致;
  3. 在使用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機制。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM