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循環隨機訪問,通過索引值去遍歷。
// 隨機訪問 List<String> list = new ArrayList<>(); int size = list.size(); for (int i = 0; i < size; i++) { value = list.get(i); }
- 第2種,通過迭代器遍歷。即通過Iterator去遍歷。
// 增強for循環
for (String s : list) { value = s; }
- 第3種,增強for循環遍歷。
// 迭代器遍歷 Iterator<String> iter = list.iterator(); while (iter.hasNext()) { value = iter.next(); }
- 第4種 forEach + lambda 循環遍歷
list.forEach(p -> { p.hashCode(); });
既然有4種遍歷,那我們看看哪種遍歷效率下面我們通過一個實驗來看下這四種循環的耗時吧:
測試代碼
/** * @Date: 2020/4/23 * @Description: */ public class ArrayListTest { public static void main(String[] args) { // 數據預熱 /* List<String> testList = createTestList(10); testForEach(testList); testFor(testList); testRandFor(10,testList);*/ List<Integer> integers = Arrays.asList(10, 50, 100,500,1000, 10000, 50000, 100000, 5000000, 10000000,30000000); for (Integer i : integers) { testRand(i); } } private static void testRand(int size) { System.out.println("-----------次數:" + size + "------------"); List<String> list = createTestList(size); // 隨機訪問通過索引值去遍歷。 long time1 = System.nanoTime(); testRandFor(size, list); long time2 = System.nanoTime(); // 增強for循環 testFor(list); long time3 = System.nanoTime(); // 迭代器遍歷 testIterator(list); long time4 = System.nanoTime(); // forEach + lambda testForEach(list); long time5 = System.nanoTime(); System.out.println("隨機訪問\t\t" + (time2 - time1) / 1000 + " ms"); System.out.println("增強for遍歷\t\t" + (time3 - time2) / 1000 + " ms"); System.out.println("迭代器遍歷\t\t" + (time4 - time3) / 1000 + " ms"); System.out.println("forEach遍歷\t\t" + (time5 - time4) / 1000 + " ms"); System.out.println(); } private static void testRandFor(int size, List<String> list) { for (int i = 0; i < size; i++) { list.get(i).hashCode(); } } private static void testFor(List<String> list) { for (String s : list) { s.hashCode(); } } private static void testIterator(List<String> list) { Iterator<String> iter = list.iterator(); while (iter.hasNext()) { iter.next().hashCode(); } } private static void testForEach(List<String> list) { list.forEach(p -> { p.hashCode(); }); } public static List<String> createTestList(int size) { List<String> list = new ArrayList<>(size); for (int i = 0; i < size; i++) { list.add(UUID.randomUUID().toString()); } return list; } }
測試數據結果如下:
-----------次數:10------------ 隨機訪問 8 ms 增強for遍歷 5 ms 迭代器遍歷 2 ms forEach遍歷 40358 ms -----------次數:50------------ 隨機訪問 4 ms 增強for遍歷 8 ms 迭代器遍歷 7 ms forEach遍歷 5 ms -----------次數:100------------ 隨機訪問 13 ms 增強for遍歷 18 ms 迭代器遍歷 14 ms forEach遍歷 10 ms -----------次數:500------------ 隨機訪問 54 ms 增強for遍歷 28 ms 迭代器遍歷 24 ms forEach遍歷 57 ms -----------次數:1000------------ 隨機訪問 106 ms 增強for遍歷 56 ms 迭代器遍歷 50 ms forEach遍歷 37 ms -----------次數:10000------------ 隨機訪問 1192 ms 增強for遍歷 892 ms 迭代器遍歷 861 ms forEach遍歷 594 ms -----------次數:50000------------ 隨機訪問 3651 ms 增強for遍歷 2908 ms 迭代器遍歷 2563 ms forEach遍歷 2712 ms -----------次數:100000------------ 隨機訪問 10693 ms 增強for遍歷 5273 ms 迭代器遍歷 9294 ms forEach遍歷 3638 ms -----------次數:5000000------------ 隨機訪問 238922 ms 增強for遍歷 29914 ms 迭代器遍歷 30533 ms forEach遍歷 28016 ms -----------次數:10000000------------ 隨機訪問 431047 ms 增強for遍歷 47151 ms 迭代器遍歷 46371 ms forEach遍歷 38943 ms -----------次數:30000000------------ 隨機訪問 1163935 ms 增強for遍歷 137710 ms 迭代器遍歷 139211 ms forEach遍歷 129960 ms
- 結論:如果數據量比較少的話貌似四種循環耗時都差不多,但是隨着數據量的增長會發現foreach的效率是最好的。
但是從上面我們會發現一個奇怪的現象,第一次循環的時候forEach遍歷的時間是最長的盡管數據量非常少也會這樣。但是后面的耗時就正常了。如果放開測試里面的預熱代碼,每次跑出來的耗時也是正常的。 - 這個結論貌似和網上的一些結論有點誤差:如果你在百度上搜索java for foreach java8 等關鍵詞會出現很多的搜索結果,比如這幾個循環效率的對比。並且很多博主的結論是java8的foreach循環是真的菜,效率不是差的一點點!!!慎用,之類的。
若java8的foreach效率如此低下,為何還要推出?難道jdk的開發人員不會優化一下?帶着這個思考,我仔細看了“已往之不諫”的博主最后為java8 正名的博客,寫的不錯,測試也很充分(說實話,沒有仔細的閱讀)但是結論很明顯。java8勝了。作者為了證明java8不是吃素的,確實下了不少功夫。最后的最后,作者提到了,“java8的foreach預熱是jvm級別的,需要預熱。”原文鏈接感興趣的可以去看下。
ArrayList刪除數據
雖然有四種遍歷方式,但是能夠正確刪除數據的方式只有兩種第1種通過迭代器進行刪除。這種方式的話,也是《阿里代碼規約》所推薦的。
Iterator<String> iter = list.iterator(); while (iter.hasNext()) { iter.next().hashCode(); iter.remove(); }
- 第2種倒序循環刪除
for(int i = list.size()-1;i>=0;i--){ list.remove(i); }
下面再演示下錯誤的刪除操作
- 普通for循環正序刪除,刪除過程中元素向左移動,不能刪除連續的元素
List<String> list = new ArrayList<>(); list.add("1"); list.add("1"); list.add("2"); for(int i=0;i<list.size();i++){ list.remove(i); } System.out.println(String.join(",",list));
結果輸出:第二個1沒有被刪除掉。
- 增強for循環刪除會拋出 java.util.ConcurrentModificationException
ArrayList注意點
- 謹慎使用ArrayList中的subList方法(Arrays.asListfa)
- ArrayList的subList結果不可強轉成ArrayList,否則會拋出ClassCastException 異常,即 java.util.RandomAccessSubList cannot be cast to java.util.ArrayList. 說明:subList 返回的是 ArrayList 的內部類 SubList,並不是 ArrayList ,而是 ArrayList 的一個視圖,對於 SubList 子列表的所有操作最終會反映到原列表上。
List<String> list = new ArrayList<>(); list.add("1"); list.add("1"); list.add("2"); ArrayList<String> strings = (ArrayList)list.subList(0, 1); //運行結果: Exception in thread "main" java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayList at com.workit.demo.listener.ArrayListTest.main(ArrayListTest.java:29)
- 在 subList 場景中,高度注意對原集合元素個數的修改,會導致子列表的遍歷、增加、刪除均會產ConcurrentModificationException 異常。
List<String> list = new ArrayList<>(); list.add("1"); list.add("1"); list.add("2"); List<String> subList = list.subList(0, 1); // 對原List增加一個值 list.add("10"); subList.add("11"); // 這一行會報 java.util.ConcurrentModificationException
- 初始化List的時候盡量指定它的容量大小。(盡量減少擴容次數)
- ArrayList線程非安全的。
來源: 程序員DD, 程序員小灰, macrozheng