小白學Java:奇怪的RandomAccess


小白學Java:奇怪的RandomAccess

我們之前在分析那三個集合源碼的時候,曾經說到:ArrayList和Vector繼承了RandomAccess接口,但是LinkedList並沒有,我們還知道繼承了這個接口,就意味着其中元素支持快速隨機訪問(fast random access)

RandomAccess是個啥

出於好奇,我特意去查看了RandomAccess的官方文檔,讓我覺得異常驚訝的是!這個接口中啥也沒有!是真的奇怪!(事實上和它類似的還有Cloneablejava.io.Serializable,這倆之后會探討)只留下一串冰冷的英文。

Marker interface used by List implementations to indicate that they support fast (generally constant time) random access. The primary purpose of this interface is to allow generic algorithms to alter their behavior to provide good performance when applied to either random or sequential access lists.

哎,不管他,翻譯就完事了,今天的生活也是斗志滿滿的搬運工生活呢!

我用我自己的語言組織一下:

  • 它是個啥呢?這個接口本身只是一個標記接口,所以沒有方法也是情有可原的。
  • 標記啥呢?它用來作為List接口的實現類們是否支持快速隨機訪問的標志,這樣的訪問通常只需要常數的時間。
  • 為了啥呢?在訪問列表時,根據它們是否是RandomAccess的標記,來選擇訪問他們的方法以提升性能。

我們知道,ArrayList和Vector底層基於數組實現,內存中占據連續的存儲空間,每個元素的下標其實是偏移首地址的偏移量,這樣子查詢元素只需要根據:元素地址 = 首地址+(元素長度*下標),就可以迅速完成查詢,通常只需要花費常數的時間,所以它們理應實現該接口。但是鏈表不同,鏈表依據不同節點之間的地址相互引用完成聯系,本身不要求地址連續,查詢的時候需要遍歷的過程,這樣子會導致,在數據量比較大的時候,查詢元素消耗的時間會很長。

RandomAccess接口的所有實現類:
ArrayList, AttributeList, CopyOnWriteArrayList, RoleList, RoleUnresolvedList, Stack, Vector

the given list is an instanceof this interface before applying an algorithm that would provide poor performance if it were applied to a sequential access list, and to alter their behavior if necessary to guarantee acceptable performance.

可以通過xxList instanceof RandomAccess)判斷該列表是否為該接口的實例,如果是順序訪問的列表(如LinkedList),就不應該通過下標索引的方式去查詢其中的元素,這樣效率會很低。

/*for循環遍歷*/
for (int i=0, n=list.size(); i < n; i++)
    list.get(i);
/*Iterator遍歷*/
for (Iterator i=list.iterator(); i.hasNext();)
    i.next();

對於實現RandomAccess接口,支持快速隨機訪問的列表來說,for循環+下標索引遍歷的方式比迭代器遍歷的方式要更快。

forLoop與Iterator的區別

對此,我們是否可以猜想,如果是LinkedList這樣並不支持隨即快速訪問的列表,是否是Iterator更快呢?於是我們進行一波嘗試:

  • 定義關於for循環和Iterator的測試方法
    /*for循環遍歷的測試*/
    public static void forTest(List list){
        long start = System.currentTimeMillis();
        for (int i = 0,n=list.size(); i < n; i++) {
            list.get(i);
        }
        long end = System.currentTimeMillis();
        long time = end - start;
        System.out.println(list.getClass()+" for循環遍歷測試 cost:"+time);
    }
    /*Iterator遍歷的測試*/
    public static void iteratorTest(List list){
        long start = System.currentTimeMillis();
        Iterator iterator = list.iterator();
        while(iterator.hasNext()){
            iterator.next();
        }
        long end = System.currentTimeMillis();
        long time = end-start;
        System.out.println(list.getClass()+"迭代器遍歷測試 cost:"+time);
    }
  • 測試如下
    public static void main(String[] args) {    
        List<Integer> linkedList = new LinkedList<>();
        List<Integer> arrayList = new ArrayList<>();
        /*ArrayList不得不加大數量觀察它們的區別,其實差別不大*/
        for (int i = 0; i < 5000000; i++) {
            arrayList.add(i);
        }
        /*LinkedList 這個量級就可以體現比較明顯的區別*/
        for(int i = 0;i<50000;i++){
            linkedList.add(i);
        }
        /*方法調用*/
        forTest(arrayList);
        iteratorTest(arrayList);
        forTest(linkedList);
        iteratorTest(linkedList);
    }
  • 測試效果想當的明顯

我們可以發現:

  • 對於支持隨機訪問的列表(如ArrayList),for循環+下標索引的方式和迭代器循環遍歷的方式訪問數組元素,差別不是很大,在加大數量時,for循環遍歷的方式更快一些。
  • 對於不支持隨機訪問的列表(如LinkedList),兩種方式就相當明顯了,用for循環+下標索引是相當的慢,因為其每個元素存儲的地址並不連續。
  • 綜上,如果列表並不支持快速隨機訪問,訪問其元素時,建議使用迭代器;若支持,則可以使用for循環+下標索引。

判斷是否為RandomAccess

上面也提到了, 這個空空的接口就是承擔着標記的職責(Marker),標記着是否支持隨機快速訪問,如果不支持的話,還使用索引來遍歷的話,效率相當之低。既然有標記,那我們一定有方法去區分標記。這時,我們需要使用instanceof關鍵字幫助我們做區分,以選擇正確的訪問方式。

public static void display(List<?> list){
    if(list instanceof RandomAccess){
        //如果支持快速隨機訪問
        forTest(list);
    }else {
        //不支持快速隨機訪問,就用迭代器
        iteratorTest(list);
    }
}

再進行一波測試看看,他倆都找到了自己的歸宿:

事實上,集合工具類Collections中有許多操作集合的方法,我們隨便舉一個從前往后填充集合的方法:

    public static <T> void fill(List<? super T> list, T obj) {
        int size = list.size();
            
        if (size < FILL_THRESHOLD || list instanceof RandomAccess) {
            //for遍歷
            for (int i=0; i<size; i++)
                list.set(i, obj);
        } else {
            //迭代器遍歷
            ListIterator<? super T> itr = list.listIterator();
            for (int i=0; i<size; i++) {
                itr.next();
                itr.set(obj);
            }
        }
    }

還有許多這樣的方法,里面有許多值得學習的地方。我是一個正在學習Java的小白,也許我的知識還未有深度,但是我會努力把自己學習到的做一個體面的總結。對了,如果文章有理解錯誤,或敘述不清之處,還望大家評論區批評指正。

參考資料:JDK1.8官方文檔


免責聲明!

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



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