在學習 Java
集合時, 最先學習的便是 List
中的 ArrayList
和 LinkedList
, 學習集合很關鍵的是學習其原始碼, 瞭解底層實現方式, 那麼今天就講講 ArrayList
實現的一個介面 RandomAccess
。
好奇心的產生
檢視 ArrayList
的原始碼, 發現它實現了 RandomAccess
這個介面, 出於好奇點進去看看, 結果發現這介面是空的, 這當然引發了更大的好奇心:這空架子到底有何用?
深入探究
JDK 官方文件是不可少的工具, 先看看它是怎麼說的:RandomAccess
是 List
實現所使用的標記介面,用來表明其支援快速(通常是固定時間)隨機訪問。此介面的主要目的是允許一般的演算法更改其行為,從而在將其應用到隨機或連續訪問列表時能提供良好的效能。
標記介面(Marker):這就說明了 RandomAccess
為空的原因,這個介面的功能僅僅起到標記的作用。
這不是與序列化介面 Serializable
差不多嗎? 只要你認真觀察, 其實不只這一個標記介面, 實際上 ArrayList
還實現了另外兩個這樣的空介面:
Cloneable
介面 :實現了 Cloneable
介面,以指示 Object.clone()
方法可以合法地對該類例項進行按欄位複製。 如果在沒有實現 Cloneable
介面的例項上呼叫 Object
的 clone
方法,則會導致丟擲 CloneNotSupportedException
異常。
Serializable
介面: 類通過實現 java.io.Serializable
介面以啟用其序列化功能。未實現此介面的類將無法使其任何狀態序列化或反序列化。
繼續探討
標記介面都有什麼作用呢? 繼續討論 RandomAccess 的作用,其他兩個在此不作討論。
如果 List
子類實現了 RandomAccess
介面,那表示它能快速隨機訪問儲存的元素, 這時候你想到的可能是陣列, 通過下標 index
訪問, 實現了該介面的 ArrayList
底層實現就是陣列, 同樣是通過下標訪問, 只是我們需要用 get()
方法的形式 , ArrayList
底層仍然是陣列的訪問形式。
同時你應該想到連結串列, LinkedList
底層實現是連結串列, LinkedList
沒有實現 RandomAccess
介面,發現這一點就是突破問題的關鍵點。
陣列支援隨機訪問, 查詢速度快, 增刪元素慢; 連結串列支援順序訪問, 查詢速度慢, 增刪元素快。所以對應的 ArrayList
查詢速度快,LinkedList
查詢速度慢, RandomAccess
這個標記介面就是標記能夠隨機訪問元素的集合, 簡單來說就是底層是陣列實現的集合。
為了提升效能,在遍歷集合前,我們便可以通過 instanceof
做判斷, 選擇合適的集合遍歷方式,當資料量很大時, 就能大大提升效能。
隨機訪問列表使用迴圈遍歷,順序訪問列表使用迭代器遍歷。
先看看 RandomAccess
的使用方式
import java.util.*;
public class RandomAccessTest {
public static void traverse(List list){
if (list instanceof RandomAccess){
System.out.println("實現了RandomAccess介面,不使用迭代器");
for (int i = 0;i < list.size();i++){
System.out.println(list.get(i));
}
}else{
System.out.println("沒實現RandomAccess介面,使用迭代器");
Iterator it = list.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
}
}
public static void main(String[] args) {
List<String> arrayList = new ArrayList<>();
arrayList.add("a");
arrayList.add("b");
traverse(arrayList);
List<String> linkedList = new LinkedList<>();
linkedList.add("c");
linkedList.add("d");
traverse(linkedList);
}
}
複製程式碼
下面我們加入大量資料進行效能測試:
import java.util.*;
public class RandomAccessTimeTest {
//使用for迴圈遍歷
public static long traverseByLoop(List list){
long startTime = System.currentTimeMillis();
for (int i = 0;i < list.size();i++){
list.get(i);
}
long endTime = System.currentTimeMillis();
return endTime-startTime;
}
//使用迭代器遍歷
public static long traverseByIterator(List list){
Iterator iterator = list.iterator();
long startTime = System.currentTimeMillis();
while (iterator.hasNext()){
iterator.next();
}
long endTime = System.currentTimeMillis();
return endTime-startTime;
}
public static void main(String[] args) {
//加入資料
List<String> arrayList = new ArrayList<>();
for (int i = 0;i < 30000;i++){
arrayList.add("" + i);
}
long loopTime = RandomAccessTimeTest.traverseByLoop(arrayList);
long iteratorTime = RandomAccessTimeTest.traverseByIterator(arrayList);
System.out.println("ArrayList:");
System.out.println("for迴圈遍歷時間:" + loopTime);
System.out.println("迭代器遍歷時間:" + iteratorTime);
List<String> linkedList = new LinkedList<>();
//加入資料
for (int i = 0;i < 30000;i++){
linkedList.add("" + i);
}
loopTime = RandomAccessTimeTest.traverseByLoop(linkedList);
iteratorTime = RandomAccessTimeTest.traverseByIterator(linkedList);
System.out.println("LinkedList:");
System.out.println("for迴圈遍歷時間:" + loopTime);
System.out.println("迭代器遍歷時間:" + iteratorTime);
}
}
複製程式碼
結果:
ArrayList: for 迴圈遍歷時間: 3 迭代器遍歷時間: 7
LinkedList: for 迴圈遍歷時間: 2435 迭代器遍歷時間: 3
結論
根據結果我們可以得出結論:
ArrayList
使用 for 迴圈遍歷優於迭代器遍歷
LinkedList
使用 迭代器遍歷優於 for 迴圈遍歷
根據以上結論便可利用 RandomAccess
在遍歷前進行判斷,根據 List
的不同子類選擇不同的遍歷方式, 提升演算法效能。
學習閱讀原始碼, 發現底層實現的精妙之處, 改變自己的思維, 從每一個小細節提升程式碼的效能。