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

      目錄算法

1、Collection及子類/接口容器繼承關係編程

2、List數組

  2.1 ArrayList安全

    2.1.1 序列化的探討網絡

    2.1.2 刪除元素數據結構

    2.1.3 調整大小框架

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

  2.3 抽象類AbstractSequentialListspa

3、Set線程

  3.1 HashSet和LinkedHashSet

  3.2 TreeSet

4、Queue

  4.1 PriorityQueue

  4.2 LinkedList

5、一些瑣碎的話題

  5.1 線程安全

  5.2 clone()

  5.3 foreach

  5.4 null對象

 

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

1、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()是一種「可選操做」,目的是延遲到須要時再實現。

 

2、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這部分。

3、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修飾,以及序列化策略。

4、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、一些瑣碎的話題

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取出來時須要判空。

相關文章
相關標籤/搜索