1. 程式人生 > >小白學Java:奇怪的RandomAccess

小白學Java:奇怪的RandomAccess

目錄

  • 小白學Java:奇怪的RandomAccess
    • RandomAccess是個啥
    • forLoop與Iterator的區別
    • 判斷是否為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官方