RandomAccess 這個空架子有何用?

leozzy發表於2017-12-05

在學習 Java 集合時, 最先學習的便是 List 中的 ArrayListLinkedList, 學習集合很關鍵的是學習其原始碼, 瞭解底層實現方式, 那麼今天就講講 ArrayList 實現的一個介面 RandomAccess

好奇心的產生

檢視 ArrayList 的原始碼, 發現它實現了 RandomAccess 這個介面, 出於好奇點進去看看, 結果發現這介面是空的, 這當然引發了更大的好奇心:這空架子到底有何用?

RandomAccess 這個空架子有何用?

深入探究

JDK 官方文件是不可少的工具, 先看看它是怎麼說的:RandomAccessList 實現所使用的標記介面,用來表明其支援快速(通常是固定時間)隨機訪問。此介面的主要目的是允許一般的演算法更改其行為,從而在將其應用到隨機或連續訪問列表時能提供良好的效能。

標記介面(Marker):這就說明了 RandomAccess 為空的原因,這個介面的功能僅僅起到標記的作用。

這不是與序列化介面 Serializable 差不多嗎? 只要你認真觀察, 其實不只這一個標記介面, 實際上 ArrayList 還實現了另外兩個這樣的空介面:

Cloneable 介面 :實現了 Cloneable 介面,以指示 Object.clone() 方法可以合法地對該類例項進行按欄位複製。 如果在沒有實現 Cloneable 介面的例項上呼叫 Objectclone 方法,則會導致丟擲 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 的不同子類選擇不同的遍歷方式, 提升演算法效能。

學習閱讀原始碼, 發現底層實現的精妙之處, 改變自己的思維, 從每一個小細節提升程式碼的效能。

相關文章