Java入門記(四):容器關系的梳理(上)——Collection


      目錄

一、Collection及子類/接口容器繼承關系

二、List

  2.1 ArrayList

    2.1.1 序列化的探討

    2.1.2 刪除元素

    2.1.3 調整大小

  2.2 Vector和Stack(不建議繼續使用)

  2.3 抽象類AbstractSequentialList

三、Set

  3.1 HashSet和LinkedHashSet

  3.2 TreeSet

四、Queue

  4.1 PriorityQueue

  4.2 LinkedList

五、一些瑣碎的話題

  5.1 線程安全

  5.2 clone()

  5.3 foreach

  5.4 null對象

 

  Java.util中的容器又被稱為Java Collections framework。雖然被稱為框架,但是其主要目的是提供一組接口盡量簡單而且相同、並且盡量高效、以便於開發人員按照場景選用,而不是自己重復實現的類。容器按接口可以分為兩大類:Collection和Map。本文主要關注Collection,以后會將Map這塊也進行研究。

一、Collection及子類/接口容器繼承關系

  先從Collection說起。可以看出:

1.Collection接口並不是一個根接口,它的超級接口是Iterator,需要提供其遍歷、移除元素(可選操作)的能力。

2.Collection接口定義了基本的容器操作方法。

 

  除此以外,

1.remove()和contains()判斷元素是否相等的依據是類似的。

對於remove(Object o),若Collection中包含的元素e,滿足(o==null ? e==null : o.equals(e)),移除其中的一個

對於contains(Object o),若Collection中包含至少一個或多個元素e,滿足(o==null ? e==null : o.equals(e)),則返回true。

2.AbstractCollection抽象類實現了一部分Collection接口的方法,主要是基於iterator實現的,如remove()、toArray(),以及利用本身的屬性size實現的size()。如果讀一下源碼,可以發現雖然AbstractCollection利用add()實現了addAll(),但是add()本身的實現是直接拋UnsupportedOperationException異常的。實際上add()是一種“可選操作”,目的是延遲到需要時再實現。

 

二、List

  了解了通用的Collection后,接下來,看看三大類的Collection:List、Set、Queue。首先從List說起。List中的元素是有序的,因而我們可以按序訪問List中的元素,以及訪問指定位置上的元素。對於“按順序遍歷訪問元素”的需求,使用List的超級接口Iterator即可以做到,這也是對應抽象類AbstractList中的實現;而訪問特定位置的元素(也即按索引訪問)、元素的增加和刪除涉及到了List中各個元素的連接關系,並沒有在AbstractList中提供。

2.1 ArrayList

  ArrayList是最常用的List的實現,其包裝了一個用於存放元素的數組,並用size屬性來標識該容器里的元素個數,而非這個被包裝數組的大小。如果對數組有所了解,很容易理解ArrayList的元素是怎么編排的,各個數組的元素如何隨機訪問(通過索引)、元素之間如何跳轉(索引增減)。閱讀源碼可以發現,這個數組用transient關鍵字修飾,表示其不會被序列化。當然,ArrayList的元素最終還是會被序列化的,要不然,這個最常用的List之一,不能持久化、不能網絡傳輸,簡直不可想象。在序列化/反序列化時,會調用ArrayList的writeObject()/readObject()方法,將該ArrayList中的元素(即0...size-1下標對應的元素)寫入流/從流讀出。這樣做的好處是,只保存/傳輸有實際意義的元素,最大限度的節約了存儲、傳輸和處理的開銷。

2.1.1 序列化的探討

  提到序列化,有個問題是,ArrayList的writeObject()/readObject()是如何被調用的?它們並不屬於ArrayList的任何一個接口,甚至是Serializabe!其實,序列化是ObjectOutputStream對象調用自身的writeObject()方法時,由它通過反射檢查入參——也即待序列化的對象——是否有writeObject()方法,並進行調用,這和接口無關,確實很古怪(可以參考《Java編程思想·第四版(中文)》第581頁)。

2.1.2 刪除元素

  ArrayList在刪除元素時,不僅要將其他元素前移來占用被移除的元素並縮小size,對於原來位置的元素,如(size-1)位置的元素前移至(size-2)位,那么(size-1)位置是要設置為null的,這樣才能讓垃圾回收機制發揮作用。這種數據的用法在Java中比較常見,比如利用Vector實現的Stack,也是這樣。而在C語言中,一種利用數組實現的棧是可以在pop()后只修改當前棧對應的數組下標而不作清理的。

2.1.3 調整大小

  利用Arrays.copyOf()方法做數組的調整。

2.2 Vector和Stack(不建議繼續使用)

   雖然Vector經過了改造,但這么做只是為了兼容Java2之前的代碼,不建議繼續使用。

  Java1.6的源碼中,和ArrayList類似,Vector底層也是數組,但是這個數組並沒有transient修飾,其序列化要低效不少。

  Stack是繼承Vector實現的,而不是包裝一個Vector。這並不是一個很好的設計,如果要使用棧行為,應該使用LinkedList。Java1.6源碼中,Stack每次擴大都需要new新的數組並作拷貝,效率並不好。

  新代碼中誤用這兩個容器的原因,可能是之前在C++中使用過STL的Vector和Stack。我剛接觸Java時,總以為這兩個類在Java中的地位類似C++。

2.3 抽象類AbstractSequentialList

  滿足“連續訪問”數據存儲而非“隨機訪問”需求的List,對於指定index元素的操作,都需要利用抽象方法listIterator()獲得一個迭代器。其唯一實現是LinkedList,對其的討論放在Queue這部分。

三、Set

  Set接口模仿了數學概念上的set,各個元素不要求重復。除了這一點,幾乎和Collection本身是一樣的。

  Set接口有一個直接子接口SortedSet,該接口要求Set中所有元素有序。和通過Iterable接口依次訪問所有元素的“有序”不同,這個“有序”要求Set包括一個比較器,可以判斷兩個元素的大於、小於或等於關系。此外,該接口提供了訪問第一個元素、訪問最后一個元素、訪問一定范圍內元素的方法。SortedSet的子接口NavigableSet進一步擴展了這個系列的方法,提供了諸如返回大於/小於元素E的第一個元素的方法。

  由於Set的操作與底層的實現關聯性很強,AbstractSet中實現的方法有限,在Java1.6中只有equals()、hashCode()、removeAll()進行了實現。

3.1 HashSet和LinkedHashSet

  HashSet之所以命名中包含了“Hash”,是因為其底層是用HashMap實現的。Map有個特點,各個Key是唯一的,這和Set的元素唯一很類似。對HashSet的元素E進行的操作,實際上是對其包裝的HashMap中對應的<E,PRESENT>的操作,其中PRESENT是一個private static final的Object。因此,HashSet的原理,放到HashMap那一塊來研究。

  HashSet有一個很特別的構造方法:HashSet(int initialCapacity, float loadFactor, boolean dummy)。這個方法第三個參數的唯一作用是,與其他兩個參數的構造方法相區分。使用這個構造方法,在底層使用的是HashMap的子類LinkedHashMap。而LinkedHashSet,正是使用了這個構造方法,在內部創建並封裝了一個LinkedHashMap而非一般的HashMap。

  假如先有HashSet,后有HashMap,用HashSet實現HashMap,是否是一個好的主意?這也放在HashMap處研究。

   HashSet的包裝的HashMap也使用transient關鍵字修飾,采用了和ArrayList一樣的序列化策略。

3.2 TreeSet

  TreeSet是SortedSet的一個實現,也是其子接口NavigableSet的實現。

   與HashSet/LinkedHashSet類似,TreeSet底層封裝了一個NavigableMap,同樣使用transient修飾,以及序列化策略。

四、Queue

  Queue和List有兩個區別:前者有“隊頭”的概念,取元素、移除元素、均為對“隊頭”的操作(通常但不總是FIFO,即先進先出),而后者只有在插入時需要保證在尾部進行;前者對元素的一些同一種操作提供了兩種方法,在特定情況下拋異常/返回特殊值——add()/offer()、remove()/poll()、element()/peek()。不難想到,在所謂的兩種方法中,拋異常的方法完全可以通過包裝不拋異常的方法來實現,這也是AbstractQueue所做的。

  Deque接口繼承了Queue,但是和AbstractQueue沒有關系。Deque同時提供了在隊頭和隊尾進行插入和刪除的操作。

4.1 PriorityQueue

   PriorityQueue用於存放含有優先級的元素,插入的對象必須可以比較。該類內部同樣封裝了一個數組。與其抽象父類AbstractQueue不同,PriorityQueue的offer()方法在插入null時會拋空指針異常——null是無法與其他元素比較通常意義下的優先級的;此外,add()方法是直接包裝了offer(),沒有附加的行為。

  由於其內部的數據結構是數組的緣故,很多操作都需要先把元素通過indexOf()轉化成對應的數組下標,再進行進一步的操作,如remove()、removeEq()、contains()等。其實這個數組保持優先級隊列的方式,是采用堆(Heap)的方式,具體可以參考任意一本算法書籍,比如《算法導論》等,這里就不展開解釋了。和堆的特性有關,在尋找指定元素時,必須從頭至尾遍歷,而不能使用二分查找。

4.2 LinkedList

  很有趣的是,LinkedList既是List,也是Queue(Deque),其原因是它是雙向的,內部的元素(Entry)同時保留了上一個和下一個元素的引用。使用頭部的引用header,取其previous,就可以獲得尾部的引用。通過這一轉換,可以很容易實現Deque所需要的行為。也正因此,可以支持棧的行為,天生就有push()和pop()方法。簡而言之,是Java中的雙向鏈表,其支持的操作和普通的雙向鏈表一樣。

  和數組不同,根據下標查找特定元素時,只能遍歷地獲取了,因而在隨機訪問時效率不如ArrayList。盡管如此,作者還是盡可能地利用了LinkedList的特性做了點優化,盡量減少了訪問次數:

    private Entry<E> entry(int index) {
        if (index < 0 || index >= size)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+size);
        Entry<E> e = header;
        if (index < (size >> 1)) {
            for (int i = 0; i <= index; i++)
                e = e.next;
        } else {
            for (int i = size; i > index; i--)
                e = e.previous;
        }
        return e;
    }

  LinkedList對首部和尾部的插入都支持,但繼承自Collection接口的add()方法是在尾部進行插入。

五、一些瑣碎的話題

5.1 線程安全

  ArrayList、HashSet/LinkedHashSet、PriorityQueue、LinkedList是線程不安全的,可以使用synchronized關鍵字,或者類似下面的方法解決:

 List list = Collections.synchronizedList(new ArrayList(...));

5.2 clone()

  ArrayList、LinkedList、HashMap/LinkedHashMap、TreeSet的clone()是淺拷貝,元素的引用和拷貝前相同;PriorityQueue的clone()繼承自Object。

5.3 foreach

  在for(Element e : collection)中:

  collection == null,直接拋異常;

  容器內容為空,即剛剛被new出來,里面什么也沒有,直接跳過循環;

  容器中放了null(如果允許的話),則將這個null取出並賦值給e,執行循環中的語句。

5.4 null對象

  List可以放無限多個,set只能放一個。EnumSet、PriorityQueue是不能放null的。這個null也在計數中。所以放進去null用foreach取出來時需要判空。

 


免責聲明!

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



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