ArrayList哪種遍歷效率最好,你真的弄明白了嗎?


ArrayList簡介

聲明:以下內容都是基於jdk1.8的

  • ArrayList 是一個數組隊列,相當於 動態數組。與Java中的數組相比,它的容量能動態增長。它繼承於AbstractList,實現了List, RandomAccess, Cloneable, java.io.Serializable這些接口。
    看過ArrayList 源碼的同學有沒有注意過有這么一個細節:為什么ArrayList實現了RandomAccess這個接口,但是 LinkedList卻沒有實現這個接口?這是一個空接口,里面沒有任何的方法,有什么作用呢?
    答案: RandomAccess 是一個標志接口,表明實現這個這個接口的 List 集合是支持快速隨機訪問的。也就是說,實現了這個接口的集合是支持 快速隨機訪問 策略的。而LinkedList是不能實現隨機訪問的。

ArrayList數據結構

ArrayList包含了兩個重要的對象:elementData 和 size。

  • elementData 是"Object[]類型的數組",它保存了添加到ArrayList中的元素。實際上,elementData是個動態數組
    那是不是有人就會問既然ArrayList本質是數組,那為啥它的長度可以改變?
    首先,數組的確長度不能改變。不過,ArrayList內部有一系列騷操作,大概就是它每次覺得長度不夠就會 創建一個新數組,這個新數組的容量比原來多出50%,把原來的數組copy過來,然后把以前的數組銷毀掉。

  • size 則是動態數組的實際大小。

ArrayList遍歷方式

  • 第1種,普通for循環隨機訪問,通過索引值去遍歷。

1     // 隨機訪問
2     List<String> list = new ArrayList<>();
3     int size = list.size();
4     for (int i = 0; i < size; i++) {
5         value = list.get(i);
6     }
  • 第2種,通過迭代器遍歷。即通過Iterator去遍歷。

1     // 增強for循環
2    for (String s : list) {
3        value = s;
4    }
  • 第3種,增強for循環遍歷。

1     // 迭代器遍歷
2    Iterator<String> iter = list.iterator();
3    while (iter.hasNext()) {
4        value = iter.next();
5    }
  • 第4種 forEach + lambda 循環遍歷

1     list.forEach(p -> {
2                p.hashCode();
3            });

既然有4種遍歷,那我們看看哪種遍歷效率下面我們通過一個實驗來看下這四種循環的耗時吧:
測試代碼

 1/**
 2 * @Date: 2020/4/23 
 3 * @Description:
 4 */
 5public class ArrayListTest {
 6    public static void main(String[] args) {
 7        // 數據預熱
 8       /* List<String> testList = createTestList(10);
 9        testForEach(testList);
10        testFor(testList);
11        testRandFor(10,testList);*/
12        List<Integer> integers = Arrays.asList(10, 50, 100,500,1000, 10000, 50000, 100000, 5000000, 10000000,30000000);
13        for (Integer i : integers) {
14            testRand(i);
15        }
16
17    }
18
19    private static void testRand(int size) {
20        System.out.println("-----------次數:" + size + "------------");
21        List<String> list = createTestList(size);
22        // 隨機訪問通過索引值去遍歷。
23        long time1 = System.nanoTime();
24        testRandFor(size, list);
25        long time2 = System.nanoTime();
26        // 增強for循環
27        testFor(list);
28        long time3 = System.nanoTime();
29        // 迭代器遍歷
30        testIterator(list);
31        long time4 = System.nanoTime();
32        // forEach + lambda
33        testForEach(list);
34        long time5 = System.nanoTime();
35
36        System.out.println("隨機訪問\t\t" + (time2 - time1) / 1000 + " ms");
37        System.out.println("增強for遍歷\t\t" + (time3 - time2) / 1000 + " ms");
38        System.out.println("迭代器遍歷\t\t" + (time4 - time3) / 1000 + " ms");
39        System.out.println("forEach遍歷\t\t" + (time5 - time4) / 1000 + " ms");
40        System.out.println();
41    }
42
43    private static void testRandFor(int size, List<String> list) {
44        for (int i = 0; i < size; i++) {
45            list.get(i).hashCode();
46        }
47    }
48
49    private static void testFor(List<String> list) {
50        for (String s : list) {
51            s.hashCode();
52        }
53    }
54
55    private static void testIterator(List<String> list) {
56        Iterator<String> iter = list.iterator();
57        while (iter.hasNext()) {
58            iter.next().hashCode();
59        }
60    }
61
62    private static void testForEach(List<String> list) {
63        list.forEach(p -> {
64            p.hashCode();
65        });
66    }
67
68    public static List<String> createTestList(int size) {
69        List<String> list = new ArrayList<>(size);
70        for (int i = 0; i < size; i++) {
71            list.add(UUID.randomUUID().toString());
72        }
73        return list;
74    }
75}

測試數據結果如下:

 1-----------次數:10------------
 2隨機訪問        8 ms
 3增強for遍歷        5 ms
 4迭代器遍歷        2 ms
 5forEach遍歷        40358 ms
 6
 7-----------次數:50------------
 8隨機訪問        4 ms
 9增強for遍歷        8 ms
10迭代器遍歷        7 ms
11forEach遍歷        5 ms
12
13-----------次數:100------------
14隨機訪問        13 ms
15增強for遍歷        18 ms
16迭代器遍歷        14 ms
17forEach遍歷        10 ms
18
19-----------次數:500------------
20隨機訪問        54 ms
21增強for遍歷        28 ms
22迭代器遍歷        24 ms
23forEach遍歷        57 ms
24
25-----------次數:1000------------
26隨機訪問        106 ms
27增強for遍歷        56 ms
28迭代器遍歷        50 ms
29forEach遍歷        37 ms
30
31-----------次數:10000------------
32隨機訪問        1192 ms
33增強for遍歷        892 ms
34迭代器遍歷        861 ms
35forEach遍歷        594 ms
36
37-----------次數:50000------------
38隨機訪問        3651 ms
39增強for遍歷        2908 ms
40迭代器遍歷        2563 ms
41forEach遍歷        2712 ms
42
43-----------次數:100000------------
44隨機訪問        10693 ms
45增強for遍歷        5273 ms
46迭代器遍歷        9294 ms
47forEach遍歷        3638 ms
48
49-----------次數:5000000------------
50隨機訪問        238922 ms
51增強for遍歷        29914 ms
52迭代器遍歷        30533 ms
53forEach遍歷        28016 ms
54
55-----------次數:10000000------------
56隨機訪問        431047 ms
57增強for遍歷        47151 ms
58迭代器遍歷        46371 ms
59forEach遍歷        38943 ms
60
61-----------次數:30000000------------
62隨機訪問        1163935 ms
63增強for遍歷        137710 ms
64迭代器遍歷        139211 ms
65forEach遍歷        129960 ms
  • 結論:如果數據量比較少的話貌似四種循環耗時都差不多,但是隨着數據量的增長會發現foreach的效率是最好的。
    但是從上面我們會發現一個奇怪的現象,第一次循環的時候forEach遍歷的時間是最長的盡管數據量非常少也會這樣。但是后面的耗時就正常了。如果放開測試里面的預熱代碼,每次跑出來的耗時也是正常的。

  • 這個結論貌似和網上的一些結論有點誤差:如果你在百度上搜索java for foreach java8 等關鍵詞會出現很多的搜索結果,比如這幾個循環效率的對比。並且很多博主的結論是java8的foreach循環是真的菜,效率不是差的一點點!!!慎用,之類的。
    若java8的foreach效率如此低下,為何還要推出?難道jdk的開發人員不會優化一下?帶着這個思考,我仔細看了“已往之不諫”的博主最后為java8 正名的博客,寫的不錯,測試也很充分(說實話,沒有仔細的閱讀)但是結論很明顯。java8勝了。作者為了證明java8不是吃素的,確實下了不少功夫。最后的最后,作者提到了,“java8的foreach預熱是jvm級別的,需要預熱。”原文鏈接感興趣的可以去看下。

ArrayList刪除數據

雖然有四種遍歷方式,但是能夠正確刪除數據的方式只有兩種

  • 第1種通過迭代器進行刪除。這種方式的話,也是《阿里代碼規約》所推薦的。

    在這里插入圖片描述
1 Iterator<String> iter = list.iterator();
2        while (iter.hasNext()) {
3            iter.next().hashCode();
4            iter.remove();
5        }
  • 第2種倒序循環刪除

1  for(int i = list.size()-1;i>=0;i--){
2            list.remove(i);
3        }

下面再演示下錯誤的刪除操作

  • 普通for循環正序刪除,刪除過程中元素向左移動,不能刪除重復的元素

1        List<String> list = new ArrayList<>();
2        list.add("1");
3        list.add("1");
4        list.add("2");
5        for(int i=0;i<list.size();i++){
6           list.remove(i);
7        }
8        System.out.println(String.join(",",list));

結果輸出:1。第二個1沒有被刪除掉。

  • 增強for循環刪除會拋出 java.util.ConcurrentModificationException

ArryList注意點

  • 謹慎使用ArrayList中的subList方法(Arrays.asListfa)

    • ArrayList的subList結果不可強轉成ArrayList,否則會拋出ClassCastException 異常,即 java.util.RandomAccessSubList cannot be cast to java.util.ArrayList. 說明:subList 返回的是 ArrayList 的內部類 SubList,並不是 ArrayList ,而是 ArrayList 的一個視圖,對於 SubList 子列表的所有操作最終會反映到原列表上。

      1    List<String> list = new ArrayList<>();
      2        list.add("1");
      3        list.add("1");
      4        list.add("2");
      5        ArrayList<String> strings =  (ArrayList)list.subList(0, 1);
      6
      7運行結果:
      8Exception in thread "main" java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayList
      9  at com.workit.demo.listener.ArrayListTest.main(ArrayListTest.java:29)
      
    • 在 subList 場景中,高度注意對原集合元素個數的修改,會導致子列表的遍歷、增加、刪除均會產ConcurrentModificationException 異常。

1   List<String> list = new ArrayList<>();
2        list.add("1");
3        list.add("1");
4        list.add("2");
5        List<String> subList =  list.subList(0, 1);
6        // 對原List增加一個值
7        list.add("10");
8        subList.add("11"); // 這一行會報 java.util.ConcurrentModificationException
  • 初始化List的時候盡量指定它的容量大小。(盡量減少擴容次數)

  • ArrayList線程非安全的。

歡迎關注


免責聲明!

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



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