1. 程式人生 > >Java實踐(二)---集合

Java實踐(二)---集合

在實現方法時,選擇不同的資料結構會導致其實現風格以及效能存在很大差異;利用Java類庫幫助我們在程式設計中實現傳統的資料結構;介紹使用標準庫中的集合類

1.集合介面

Java最初版本只為最常用的資料結構提供了很少的一組類:Vector、Stack、Hashtable、BitSet和Enumeration介面,其中的Enumeration介面提供了一種用於訪問任意容器中各個元素的抽象機制

1.將集合的介面與實現分離

Java集合類庫將介面(interface)和實現(implementation)分離,例如,當需要收集物件,並按照“先進先出”的規則檢索物件時,就應該使用佇列;一個佇列的最小形式如下:

interface Queue<E>
{
    void add(E element);
    E remove();
    int size();
}

這個介面並沒有說明佇列是如何實現的;佇列通常有2中實現方式:

1.使用迴圈陣列
2.使用連結串列

每一個實現都可以通過一個實現了Queue介面的類表示:
迴圈陣列:

class CircularArrayQueue<E> implements Queue<E>//not an actual library class
{   
    CircularArrayQueue(int capacity){...
} public void add(E element){...} public E remove(){...} private E[] elements; private int head; private int tail; }

連結串列:

class LinkedListQueue<E> implements Queue<E>//not an actual library class
{
    LinkedListQueue(){...}
    public void add(E element){...}
    public E remove(){...
} public int size(){...} private E[] elements; private int head; private int tail; }

以上這2個在Java中並不存在,如果需要一個 迴圈陣列佇列,就可以使用ArrayDeque類,如果需要一個連結串列佇列,就可以直接使用LinkedList類,這個類實現了Queue介面

使用介面型別存放集合的引用

Queue<Customer> expressLane = new CircularArrayQueue<>(100);
expressLane.add(new Customer("Harry"));

利用這種方式,如果改變想法,只需要修改呼叫構造器的地方,也可以選擇LinkedListQueue,如下:

Queue<Customer> expressLane = new LinkedListQueue<>();
expressLane.add(new Customer("Harry"));

迴圈陣列比連結串列更高效,多數人優先選擇迴圈陣列;迴圈陣列是一個有限集合,容量有限,如果程式中要收集的物件沒有上限,最好使用連結串列實現

在API文件中存在著以Abstract開頭的類,例如AbstractQueue,這些類是為類庫實現者設計的

2.Java類庫中的集合介面和迭代器介面

在Java類庫中,集合類的基本介面是Collection介面,這個介面有2個基本的方法:

public interface Collection<E>
{
    boolean add(E element);
    Iterator<E> iterator();
    ...
}  

1.add方法用於向集合中新增元素,如果新增元素確實改變了集合就返回true,如果集合沒有發生變化,就返回false;例如,試圖向集中新增一個物件,而這個物件在集中已經存在,這個新增請求就沒有實效,因為集中不允許有重複的物件
2.iterator方法用於返回一個實現了Iterator介面的物件,可以使用這個迭代器物件依次訪問集合中的元素

1.迭代器

Iterator介面包含3個方法:

public interface Iterator<E>
{
    E next();
    boolean hasNext();
    void remove();
}  

通過反覆呼叫next方法,可以逐個訪問集合中的每個元素,如果集合到達末尾,next方法會丟擲一個NoSuchElementException,因此需要呼叫hasNext方法,如果迭代器物件還有可供訪問的元素,就會返回true

Collection<String> c = ...;
Iterator<String> iter = .iterator();
while(iter.hasNext())
{
    String element = iter.next();
    do something with element
}

請求一個迭代器,並在hasNext方法返回true時反覆呼叫next方法,來檢視集合中的所有元素
上面程式碼可以簡化為for each 迴圈

for(String element : c)
{
    do something with element
}

編譯器簡單的將“for each”迴圈翻譯為帶有迭代器的迴圈,“for each”迴圈可以與任何實現了Iterable介面的物件一起工作,這個介面只包含一個方法:

public interface Iterable<E>
{
    Iterable<E> iterator();
}

Collection介面擴充套件了Iterable介面,因此對於標準類庫中的任何集合都有可以使用“for each”迴圈

元素被訪問的順序取決於集合的型別:

  • 1.對ArrayList進行迭代,迭代器將從索引0開始,沒迭代一次,索引值家1
  • 2.對HashSet進行迭代,每個元素將會按照某種隨機的次序出現(雖然可以確定在迭代的過程中,能夠遍歷到集合中的所有元素,但卻無法預知元素被訪問的次序)【對於與順序無關的操作來說,沒有問題】

Java的迭代器不能像運算元組那樣進行操作,查詢操作與位置變更緊密相連;查詢一個元素的唯一方法是呼叫next,而在執行查詢操作的同時,迭代器的位置隨之向前移動;因此將Java的迭代器認為是位於2個元素之間,當呼叫next時,迭代器就越過下一個元素,並返回剛才越過的那個元素的引用

【可以將Iterator.next與InputStream.read看作是等效的,從資料流中讀取一個位元組,就會自動的“消耗掉”這個位元組,下次呼叫read將會消耗並返回輸入的下一個位元組,用同樣的方式,反覆地呼叫next就可以讀取集合中所有元素】

2.刪除元素

Iterator介面的remove方法將會刪除上次呼叫next方法是返回的元素(在大多數情況下,在決定刪除某個元素之前應該先看一下這個元素是很具有意義的),如果想要刪除指定位置上的元素,仍然需要越過這個元素,例如:

Iterator<String> it = c.iterator();
it.next();//skip over first element
it.remove();//remove it

對於remove方法和next方法的呼叫具有相互依賴性,如果remove之前沒有呼叫next將是不合法的,會丟擲IllegalStateException,如果要刪除相鄰的2個元素,不能如下呼叫:

it.remove();
it.remove();

必須先呼叫next越過將要刪除的元素:

it.remove();
it.next();
it.remove();

3.泛型實用方法

由於Collection與Iterator都是泛型介面,可以編寫操作任何集合型別的實用方法,例如,下面是一個檢測任意集合是否包含指定元素的泛型方法:

public static <E> boolean contains(Collection<E> c , Object obj)
{
    for(E element : c)
    {
        if(element.equals(obj))
        {
            return true;
        }
    return false;
    }
}       

Collection介面聲明瞭很多有用的方法,所有的實現類都必須提供這些方法,如下列舉了部分:

int size()
boolean isEmpty()
boolean contains(Object obj)
boolean containsAll(Collection<?> c)
boolean equals(Object other)
boolean addAll(Collection<? extends E> from)
boolean remove(Object obj)
boolean removeAll(Collection<?> c)
void clear()
boolean retainAll(Collection<?> c)
Object[] toArray()
<T> T[] toArray(T[] arrayToFill)

如果實現Collection介面的每一個類都要提供如此多的例行方法將是一件很煩人的事情,為了能夠讓實現者更容易地實現這個介面,Java類庫提供了一個類AbstractCollection,它將基礎方法size和iterator抽象化了,但是在此提供了例行方法,如:

public abstract class AbstractCollection<E> implements Collection<E>
{
    ...
    public abstract Iterator<E> iterator();
    public boolean contains(Object obj)
    {
        for(E element : this)//calls iterator()
        {
            if(element.equals(obj))
            {
                return true;
            }
        return false;
        }
        ...
    }

此時,一個具體的集合類可以擴充套件AbstractCollection類了,現在要由具體的集合類提供iterator方法,而contains方法由AbstractCollection超類提供,如果子類有更加有效的方式實現contains方法,也可以由子類提供

集合類的使用者可以使用泛型介面中一組更加豐富的方法,而實際的資料結構實現者並沒有需要實現所有例行方法的負擔

2.具體集合

Java類庫中的集合(不包含執行緒安全集合),除Map結尾的類之外,其他類都實現了Collection介面;以Map結尾的類實現了Map介面:

集合型別 描述
ArrayList 一種可以動態增長和縮減的索引序列
LinkedList 一種可以在任何位置進行高效地插入和刪除操作的有序序列
ArrayDeque 一種用迴圈陣列實現的雙端佇列
HashSet 一種沒有重複元素無序集合
TreeSet 一種有序
EnumSet 一種包含列舉型別值的集合
LinkedHashSet 一種可以記住元素插入次序的集合
PriorityQueue 一種允許高效刪除最小元素的集合
HashMap 一種儲存鍵/值關聯的資料結構
TreeMap 一種鍵值有序排列的對映表
EnumMap 一種鍵值屬於列舉型的對映表
LinkedHashMap 一種可以記住鍵/值項新增次序的對映表
WeakHashMap 一種其值無用武之地後可以被垃圾回收器回收的對映表
IdentityMap 一種用==而不是equals比較鍵值的對映表

1.連結串列

陣列和陣列列表都有一個重大的缺陷,從陣列的中間位置刪除一個元素要付出很大的代價,其原因是陣列中處於被刪除元素之後的所有元素都要向陣列的前端移動(插入元素亦是如此)
【連結串列可以解決這個問題】

陣列在連續的位置上存放物件的引用,連結串列卻將每個物件存放在獨立的節點中(每個節點總還存放著序列中下一個節點的引用)【在Java中,所以連結串列都是雙向連結串列】

在連結串列中刪除一個元素是很輕鬆的操作,只需要對被刪除元素附近的節點進行更新即可

連結串列和泛型集合之間有一個重要的區別:連結串列是一個有序集合,每個物件的位置十分重要

LinkedList.add方法將物件新增到連結串列的尾部;由於迭代器是描述集合中位置的,所以這種依賴於位置的add方法將有迭代器負責;只有對自然有序的集合使用迭代器新增元素才有實際意義

集型別(Set),其中的元素完無序,因此在Iterator介面中沒有add方法,集合類庫提供了子介面ListIterator,其中包含add方法:

interface ListIterator<E> extends Iterator<E>
{
    void add(E element);
    ...
}

與Collection.add不同,這個方法不返回boolean型別的值,它假定新增操作總是會改變連結串列

ListIterator介面有2個方法,可以反向遍歷連結串列:

  • 1.E previous()
  • 2.boolean hasPrevious()

與next方法一樣previous方法返回越過的物件

LinkedList類的listIterator方法返回一個實現了ListIterator介面的迭代器物件

ListIterator<String> iter = staff.listIterator();

add方法在迭代器位置之前新增一個新物件;如果多次呼叫add方法,將按照提供的次序把元素新增到連結串列中,它們被依次新增到迭代器當前位置之前

當用一個剛剛由Iterator方法返回,並且指向連結串列表頭的迭代器呼叫add操作時,新新增的元素將變成列表的新表頭;當迭代器越過連結串列的最後一個元素時(即hasNext方法返回false),新增的元素將變為新表尾

如果連結串列有n個元素,有n+1個位置可以新增新的元素,這些位置與迭代器的n+1個可能的位置相對應

刪除元素時要注意:

  • 1.在呼叫next方法之後,remove方法將會刪除迭代器左側的元素(即迭代器剛越過的那個元素)
  • 2.在呼叫previous方法之後,remove方法將會刪除迭代器右側的元素(即迭代器剛越過的那個元素)
  • 3.不能再同一行中呼叫2次remove方法
  • 4.add方法只依賴於迭代器的位置,remove方法依賴於迭代器的狀態
  • 5.set方法用一個新元素取代呼叫next方法或previous方法返回的上一個元素

一個迭代器指向另一個迭代器剛剛刪除的元素前,現在這個迭代器就是無效的,連結串列迭代器可以檢測出這種修改,如果迭代器發現它的集合被另一個迭代器修改了或被該集合自身的方法修改了,就會丟擲ConcurrentModificationException

為了避免發生併發修改的的異常,請遵循以下原則:

  • 1.可以根據需要給容器附加許多的迭代器,但這些迭代器只能讀取列表
  • 2.在單獨附加一個既能讀又能寫的的迭代器

有一個簡單的方法可以檢測到併發修改的問題:

  • 1.集合可以跟蹤改寫操作(如新增或刪除元素)的次數
  • 2.每個迭代器都維護一個獨立的計數器
  • 3.在每個迭代器的方法開始處檢查自己改寫操作的計數值是否與集合的改寫操作計數值一致;如果不一致,丟擲ConcurrentModificationException

【連結串列只負責跟蹤對列表的結構性修改(如新增或刪除元素),set操作不被視為結構性修改】

連結串列不支援快速地隨機訪問,如果要檢視連結串列中的第n個元素,必須從頭開始越過n-1個元素,鑑於這個原因,在程式需要採用整數索引訪問元素時,通常不採用連結串列;儘管如此,LinkedList還是提供了一個用來訪問某個特定元素的get方法(get方法做了微小的優化,如果所有大於size()/2就從表尾開始搜尋元素):

LinkedList<String> list = ...;
String obj = list.get(n);

這個方法效率不高,如果發現自己正在使用這個方法,說明有可能對於所要解決的問題使用了錯誤的資料結構(絕對不能使用這種讓人誤解的隨機訪問方法來遍歷連結串列)如下程式碼效率極低:

for(int i = 0; i < list.size(); i++)
{
    do something with list.get(i);
}

每次查詢一個元素都要從列表的頭部重新開始搜尋,LinkedList物件根本不做任何快取位資訊的操作

由於Java迭代器指向兩個元素之間的位置,所以可以同時產生2個索引,nextIndex方法返回下一次呼叫next方法時返回元素的整數索引,previousIndex方法返回下一次呼叫previous方法時返回元素的整數索引(這2個方法的效率非常高,因為迭代器保持著當前位置的計數器)

如果有1個整數索引n,list.listIterator(n)將返回一個迭代器,這個迭代器指向索引為n的元素的前一個位置,然後呼叫next和list.get(n)會產生同一個元素,只是獲得這個迭代器的效率比較低

使用連結串列的唯一理由是儘可能地減少在連結串列中插入或刪除元素所付出的代價,如果列表只有少數幾個元素,就完全可以使用ArrayList

建議避免使用以整數索引表示連結串列中位置的所有方法,如果需要對集合進行隨機的訪問,就使用ArrayList或者陣列,而不是連結串列

List介面用於描述一個有序集合,並且集合中每個元素的位置十分重要,有2種訪問元素的協議,一個是迭代器,另一個是用get和set方法隨機地訪問每個元素(不適用於連結串列,但對陣列有用)

2.陣列列表

ArrayList封裝了一個動態再分配的物件陣列,ArrayList方法不是同步的

在需要使用動態陣列時,可以使用Vector類,因為Vector類的所有方法都是同步的,可以由2個執行緒安全的訪問一個Vector物件,如果用一個執行緒訪問Vector,程式碼要在同步操作上消耗大量的時間,

3.雜湊集

連結串列和陣列按照人們的意願排列元素的次序,如果要查詢元素,又忘記了位置,需要訪問所有的元素

如果不在意元素的順序,可以有幾種能夠快速查詢元素的資料結果,其缺點是無法控制元素出現的次序,它們將按照有利於其操作目的的原則組織資料

散列表可以快速查詢所需要的物件,散列表為每個物件計算一個整數,稱為雜湊碼(由物件例項域產生的一個整數【具有不同資料域的物件將產生不同的雜湊碼】)

字串由hashCode方法產生雜湊碼,如果自定義類,就要負責實現這個類的hashCode方法【自己實現的hashCode方法應該與equals方法相容(即如果a.equals(b)為true,a與b必須具有相同的雜湊碼)】

現在最重要的問題是,雜湊碼能夠快速的計算出來,並且這個計算只與要雜湊的物件的狀態有關,與散列表中的其他物件無關

在Java中,散列表用連結串列陣列實現,每個列表被稱為桶,要想查詢表中物件的位置,就要先計算它的雜湊碼,然後與桶的總數(桶數是指用於收集具有相同雜湊值的桶的數目)取餘,所得到的結果就是儲存這個元素的桶的索引

  • 1.有時候會遇到桶被佔滿的情況,這是不可避免的,這種現象稱為雜湊衝突,這時需要用新物件與桶中的所有物件進行比較,檢視這個物件是否已經存在
  • 2.如果大致知道最終會有多少個元素要插入到散列表中,可以設定桶數,通常桶數設定為預計元素個數的75%-150%,最好將桶數設定為一個素數,以防鍵的聚集
  • 3.如果散列表太滿就需要再雜湊,這需要建立一個桶數更多的表,並將所有元素插入到這個新表中,然後丟棄原有的表【裝填因子決定何時對散列表進行再雜湊,預設為0.75】

散列表可以用於實現幾個重要的資料結構,其中最簡單的是set型別,set是沒有重複元素的元素集合;set的add方法首先在集合中查詢要新增的物件,如果不存在,就將這個物件新增進去

Java集合類庫提供了一個HashSet類,它實現了基於散列表的集:

  • 1.可以用add方法新增元素
  • 2.contains方法被重新定義,用來快速的檢視是否某個元素已經出現在集中(只在某個桶中查詢元素,不必檢視集合中的所有元素)

雜湊集迭代器將以此訪問所有的桶,由於雜湊將元素分散在各個位置,所有訪問的順序幾乎是隨機的,只有不關心集合中順序時,才應該使用HashSet

在更改集中的元素時要格外小心,如果元素的雜湊碼發生了變化,元素在資料結構中的位置也會發生變化

4.樹集

樹集是一個有序集合,可以以任意順序將元素插入到集合中;在對集合進行遍歷的時候,每個值將自動地按照排序後的順序呈現

每次將一個元素新增在樹中時,都被放置在正確的排序位置上,迭代器總是以排好的順序訪問每個元素;將一個元素新增到樹中,要比新增到散列表中慢,但是,與元素新增到陣列或連結串列的正確位置上相比要快很多

如果樹中包含n個元素,查詢新元素的正確位置平均需要log2n次比較

5.物件的比較

預設情況下,,樹集假定插入的元素實現了Comparable介面,這個介面定義了一個方法:

public interface Comparable<T>
{
    int compareTo(T other);
}

如果要插入自定義的物件,就必須通過實現Comparable介面自定義排列順序,在Object類中沒有提供任何compareTo介面的預設實現

也可以通過將Comparator物件傳遞給TreeSet構造器來告訴樹集使用不同的比較器方法,Comparator介面聲明瞭一個帶有2個顯示引數的compare方法:

public interface Comparator<T>
{
    int compare(T a, T b);
}

注意比較器沒有任何資料,它只是比較方法的持有器,有時候將這種物件稱為函式物件,函式物件通常動態定義(即定義為匿名內部類的例項):

SortedSet<Item> sortByDescription = new TreeSet<>(
    new Compatator<Item>()
    {
        public int compare(Item a, Item b)
        {
            String desceA = a.getDescription();
            String desceB = b.getDescription();
            return descrA.compareTo(descrB);
        }
    });

樹的排序必須是全序,任何2個元素必須是可比的,並且只有在2個元素相等時結果才是0

6.佇列與雙端佇列

  • 1.佇列可以有效地在尾部新增一個元素,在頭部刪除一個元素
  • 2.雙端佇列可以讓人們有效地在頭部和尾部同時新增或刪除元素,不支援在佇列中間新增元素

在Java SE6 中引入了Deque介面,並由ArrayDeque和LinkedList類實現,這2個類都提供了雙端佇列,而且必要時可以增加佇列的長度

7.優先順序佇列

優先順序佇列中的元素可以按照任意的順序插入,卻總是按照排序的順序進行檢索,也就是說,無論何時呼叫remove方法,總會獲得當前優先順序佇列中最小的元素,然而優先順序佇列並沒有對所有的元素進行排序

如果用迭代的方式處理這些元素,並不需要對它們進行排序,優先佇列使用堆(heap),堆是一個可以自我調整的二叉樹,對樹執行新增(add)和刪除(remove)操作,可以讓最小的元素移動到根,而不必花費時間對元素進行排序

與TreeSet一樣,一個優先順序佇列既可以儲存實現了Comparable介面的類物件,也可以儲存在構造器中提供比較器的物件

使用優先順序佇列的典型示例是任務排程,每一個任務都有一個優先順序,任務以隨機順序新增到佇列中,每當啟動一個新任務時,都將優先順序最高的任務從佇列中刪除(習慣將1設定為最高階,所以會將最小的元素刪除)

優先順序佇列的迭代並不是按照元素的排列順序訪問的,而刪除卻總是刪掉剩餘元素中優先順序數最小的那個元素

8.對映表

  • 1.集是一個集合,它可以快速地查詢現有的元素
  • 2.對映表用來存放鍵/值對

Java類庫為對映表提供了2個通用的實現:HashMap和TreeMap,這2個類都實現了Map介面

  • 1.雜湊對映表對鍵進行雜湊
  • 2.樹對映表用鍵的整體順序對元素進行排序,並將其組織成搜尋樹

雜湊或比較函式只能作用於鍵,與鍵關聯的值不能進行雜湊或比較

選擇雜湊還是樹呢?與集一樣,雜湊稍微快一些,如果不需要按照排列屬性訪問鍵,最好選擇雜湊

如果在對映表中沒有與給定的鍵對應的資訊,get方法返回null

鍵必須是唯一的,不能對同一個鍵存放2個值;如果對同一個鍵呼叫了2個put方法,第二個值就會取代第一個值,實際上,put方法將返回這個鍵引數儲存的上一個值

  • 1.remove方法用於從對映表中刪除給定鍵對應的元素
  • 2.size方法用於返回對映表中的元素數

集合框架並沒將對映表本身視為一個集合(其他的資料結構框架則將對映表視為對(pairs)的集合,或視為用鍵作為索引的值的集合),然而,可以獲得對映表的檢視,這是一組實現了Collection介面物件,或它的子介面的檢視

有3種試圖:

  • 1.鍵集:Set<K> keySet()
  • 2.值集合:Collection<V> values()
  • 3.鍵值對集:Set<Map.Enty<k,v>> entrySet()

keySet既不是HashSet也不是TreeSet,而是實現了Set介面的某個其他的物件,Set介面擴充套件了Collection介面,可以與使用任何集合一樣使用keySet

例如,列舉對映表中的所有鍵:

Set<String> keys = map.keySet();
for(String key : keys)
{
    do somthing with key
}

如果同時檢視鍵與值,可以通過列舉各個條目(entries)檢視,以避免對值進行查詢,可以使用如下框架:

for(Map.Entry<String, Employee> entry : staff.entrySet())
{
    String key = entry.getKey();
    Employee value = entry.getValue();
    do something with key . value 
}

如果呼叫迭代器remove方法,實際上就從對映表中刪除了鍵以及對應的值;但是不能將元素新增到鍵集的檢視中,如果調add方法,會丟擲UnsupportedOperationException

9.專用集與對映表類

在集合類庫中有幾個專用的對映表集:

  • 1.弱雜湊對映表(WeakHashMap)

設計WeakHashMap類是為了解決一個有趣的問題,如果有一個值,對應的鍵已經不在使用了,將會出現什麼情況?假定對某個鍵的最後一次引用已經消亡,不再有任何途徑引用這個值的物件了,但是,由於在程式中的任何部分沒有再出現這個鍵,所以這個鍵值對無法從對映表中刪除

垃圾回收器跟蹤活動的物件,只要對映表物件是活動的,其中的所有桶也都是活動的,它們不能被回收;因此需要程式負責從長期活動的對映表中刪除那些無用的值,或者使用WeakHashMap完成這件事,當鍵值對的唯一引用來自散列表條目時,這一資料結構將與垃圾回收器協同工作一起刪除鍵值對

WeakHashMap使用弱引用儲存鍵,WeakReference物件將引用儲存到另外一個物件中,在這裡就是散列表鍵(對於這種型別的物件,垃圾回收器用一種特有的方式進行處理,通常,如果垃圾回收器發現某個特定的物件已經沒有他人引用了,就將其回收)

如果某個物件只能由WeakReference引用,垃圾回收器仍然回收它,但要將引用這個物件的弱引用放入佇列中,WeakHashMap將週期性的檢查佇列,以便找出新新增的弱引用,一個弱引用進入佇列意味著這個鍵不再被他人使用,並且已經被收集起來,於是,WeakHashMap將刪除對應的條目

  • 2.連結雜湊集和連結對映表

Java SE 1.4增加了LinkedHashSet和LinkedHashMap用來記住插入元素項的順序(這樣可以避免在散列表中的項從表面上看是隨機排列的),當條目插入到表中,就會併入到雙向連結串列中

連結雜湊對映表將用訪問順序,而不是插入順序,對對映表條目進行迭代;每次呼叫get或put,受到影響的條目將從當前位置刪除,並放到條目連結串列尾部(只有條目在連結串列中的位置會受影響,而散列表中的桶不會受到影響,一個條目總位於與鍵雜湊碼對應的桶中)

構造這樣一個雜湊對映表,呼叫如下:

LinkedHashMap<K, V>(initialCapacity, loadFactor, true)

訪問順序對於實現快取記憶體的“最近最少使用”原則十分重要

  • 3.列舉集與對映表

EnumSet是一個列舉型別元素集的高效實現,由於列舉型別只有有限個例項,所以EnumSet內部用位序列實現,如果對應的值在集中,則相應的位置被置為1

可以使用Set介面的常用方法來修改EnumSet

EnumMap是一個鍵型別為列舉型別的對映表,它可以直接且高效地用一個值陣列實現,在使用時,需要在構造器中指定鍵型別:

EnumMap<Weekday, Employee> personInCharge = new EnumMap<>(Weekday.class);
  • 4.標識雜湊對映表

Java SE 1.4 增加了IdentityHashMap,這個類中,鍵的雜湊值不是用hashCode函式計算的,而是用System.identityHashCode方法計算的(這是Object.hashCode方法根據記憶體地址來計算雜湊碼時所使用的方式)在對2個物件進行比較的時候IdentityHashMap類使用==而不是equals

不同的鍵物件,即使內容相同,也被視為不同的物件

3.集合框架

Java集合類庫構成了集合類的框架,它為集合的實現定義了大量的介面和抽象類,並且對其中的某些機制給予了描述,集合框架的介面如下所示:
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述

集合有2個基本介面:Collection和Map,使用下列方法向集合中插入元素:

boolean add(E element)
V put(K key, V value) 

從集合中讀取元素:

V get(K key)

List是一個有序集合,元素可以新增到容器中某個特定位置,將元素放置在某個位置上可以採用2中方式:

  • 1.使用整數索引
  • 2.使用列表迭代器

List介面定義了幾個用於隨機訪問的方法:

void add(int index, E element)
E get(int index)
void remove(int index)

List在提供這些隨機訪問的方法時,並不管它們對某種特定實現是否高效

為了避免執行成本較高,Java SE 1.4引入了一個標記介面RandomAccess,這個介面沒有任何方法,但可以用來檢測一個特定的集合是否支援高效的隨機訪問:

if(c instanceof RandomAccess)
{
    usr random access algorithm
}
else
{
    use sequential access algorithm
}

ArrayList和Vector類都實現了RandomAccess介面

ListIterator介面定義了一個方法,用於將元素新增到迭代器所處位置的前面:

void add(E element)

要想獲取和刪除給定位置的元素,只需要呼叫Iterator介面中的next方法和remove方法即可

*Set介面與Collection介面是一樣的,只是其方法的行為有著更加嚴謹 定義:

  • 1.集的add方法拒絕新增重複的元素*
  • 2.集的equals方法定義2個集相等的條件是它們包含相同的元素但順序不必相同
  • 3.hashCode方法定義應該保證具有相同元素的集將會得到相同的雜湊碼

在Java第一版“遺留”下來的容器類:

Vector
Stack
HashTable
Properties

這些類已經被整合到集合框架中

1.檢視與包裝器

通過使用檢視(views)可以獲得其他的實現了集合介面和對映表介面的物件(對映表類的keySet方法就是一個這樣的示例)

keySet方法返回一個實現Set介面的類物件,這個類的方法對原對映表進行操作,這種集合稱為檢視

檢視技術在集框架中有許多非常有用的應用

  • 1.輕量級包裝器

Arrays類的靜態方法asList將返回一個包裝了普通Java陣列的List包裝器,這個方法可以將陣列傳遞給一個期望得到列表或集合變元的方法,如:

Card[] cardEdck = new Card[52];
...
List<Card> cardList = Arrays.asList(cardDeck);

返回的物件不是ArrayList,它是一個檢視物件,帶有訪問底層陣列的get和set方法;改變陣列大小的所有方法(add或remove)都會丟擲一個UnsupportedOperationException

在Java SE 5.0開始,asList方法宣告為一個具有可變數量引數的方法,除了可以傳遞一個數組之外,還可以將各個元素直接傳遞給這個方法,如:

List<String> names = Arrays.asList("Amy","Bob","Carl");

這個方法呼叫Collections.nCopies(n, anObject),將返回一個實現了List介面的不可修改的物件,並給人一種包含n個元素,每個元素都想一個 anObject的錯覺

Collections類包含很多實用方法,這些方法的引數和返回值都是集合,不要將它與Collection介面混淆

  • 2.子範圍

可以為很多集合建立子範圍(subrange)檢視,假設有一個列表staff,想從中去除第10-19個元素,可以使用subList得到來獲取一個列表的子範圍檢視

List group2 = staff.subList(10, 20);

第一個索引包含在內,第二個索引則不包含在內

可以將任何操作應用於子範圍,並且能夠自動地反應整個列表的情況,例如刪除整個子範圍:

group2.clear();

元素自動從staff列表中清除了,並且group2為空

對於有序集合對映表,可以使用排序順序而不是元素位置建立子範圍,SortedSet介面和SortedMap介面聲明瞭3個方法:

SortedSet<E> subSet(E from, E to)
SortedSet<E> headSet(E to)
SortedSet<E> tailSet(E from)

SortedMap<K, V> subMap(K from, V to)
SortedMap<K, V> headMap(K to)
SortedMap<K, V> tailMap(K from)

這些方法將返回>=from<to的所有元素子集
返回對映表檢視,該對映表包含鍵落在指定範圍內的所有元素

Java SE 6中引入了NavigableSet介面賦予子範圍操作更多的控制能力,可以指定是否包含邊界:

NavigableSet<E> subSet(E from, boolean fromInclusive, E to, boolean toInclusive)
NavigableSet<E> headSet(E to, boolean toInclusive)
NavigableSet<E> tailSet(E from, boolean fromInclusive)
  • 3.不可修改的檢視

Collections還有幾個方法,用於產生集合的不可修改檢視,可以使用下面6種方法獲得不可修改檢視:

Collections.unmodifiableCollection//equals方法只檢測2個物件是否是同一個物件
Collections.unmodifiableList//使用底層集合的equals和hashCode方法
Collections.unmodifiableSet//使用底層集合的equals和hashCode方法
Collections.unmodifiableSortedSet
Collections.unmodifiableMap
Collections.unmodifiableSortedMap

每個方法都定義了一個介面

不可修改檢視並不是本身不可修改,仍然可以通過集合的原始引用對集合進行修改,並且仍然可以讓集合的元素呼叫更改器方法

檢視只是包裝了介面而不是實際的集合物件。所以只能訪問介面中定義的方法

  • 4.同步檢視

如果由多個執行緒訪問集合,就必須確保不會被意外地破壞

類庫的設計者使用檢視機制來確保常規集合的執行緒安全,而不是實現執行緒安全的集合類,例如:Collections類的靜態synchronizedMap方法可以將任何一個對映錶轉換成具有同步訪問方法的Map:

Map<String, Employee> map = Collections.synchronizedMap(new HashMap<String, Employee>());

現在就可以由多執行緒訪問map物件了;像get和put這類方法都是同步操作

  • 5.檢查檢視
    Java SE 5.0增加了一組“檢查”檢視,用來對泛型型別發生問題時提供除錯支援
List<String> safeString = Collections.checkList(String, String.class);

檢視的add方法將檢測插入的物件是否屬於給定的類,如果不屬於給定的類,就立即丟擲一個ClassCastException,這樣做的好處是錯誤可以在正確的位置得以報告

被檢查檢視受限於虛擬機器可以執行的執行時檢測

  • 6.關於可選操作的說明

檢視有一些侷限性,即可能只可讀、無法改變大小,只支援刪除而不支援插入,這些與對映表的鍵檢視情況相同,如果進行不恰當的操作,會丟擲異常

2.批操作

絕大多數示例都使用迭代器遍歷集合,一次遍歷一個元素,然而可以使用類庫中的批操作(bulk operation)避免頻繁地使用迭代器

3.集合與陣列之間的轉換

如果有一個數組需要轉換為集合,Arrays.aList的包裝器就可以實現這個目的,例如:

String[] valus = ...;
HashSet<String> staff = new HashSet<>(Arrays.asList(values));

將集合轉換為陣列有點困難,可以使用toArray方法:

Object[] values = staff.toArrays();

這樣將產生一個物件陣列,即使知道集合中包含一個特定型別的物件,也不能進行型別轉換:

String[] values = (String[]) staff.toArray();//error!

由toArray方法返回的陣列是一個Object陣列,無法改變其型別

應該採用如下方法:

String[] values =  staff.toArray(new String[staff.size()]);

4.演算法

泛型集合介面有一個很大的優點,即演算法只需要實現一次,下面是找到陣列中的最大元素的程式碼:

陣列:

if(a.length == 0)throw new NoSuchElementException();
T largest = a[0];
for(int i = 1; i < a.length; i++)
{
    if(largest.compareTo(a[i]) < 0)
    {
        largest = a[i];
    }
}

陣列列表:

if(v.size() == 0)throw new NoSuchElementException();
T largest = v.get();
for(int i = 1; i < v.size(); i++)
{
    if(largest.compareTo(v.get(i)) < 0)
    {
        largest = v.get(i);
    }
}

連結串列:

if(l.isEmpty())throw new NoSuchElementException();
Iterator<T> iter = l.iterator();
T largest = iter.next();
while(iter.hasNext())
{
    T next = iter.next();
    if(largest.compareTo(next) < 0)
    {
        largest = next;
    }
}

可以將max方法實現為能夠接受任何實現了Collection介面的物件:

public static <T extends Comparable> T max(Collection<T> c)
{
    if(c.isEmpty()) throw new NoSuchElementException();
    Iterator<T> iter = c.iterator();
    T largest = iter.next();
    while(iter.hasNext())
    {
        T next = iter.next();
        if(largest.compareTo(next) < 0)
        {
            largest = next;
        }
    return largest;
    }
}

現在就可以用一個方法計算連結串列、陣列列表、陣列中的最大元素

1.排序與混排

Collections類中的sort方法可以對實現了List介面的集合進行排序

List<String> staff = new LinkedList<>();
fill collection
Collections.sort(staff);

這個方法假定列表元素實現了Comparable介面,如果想採用其他方式進行排序,可以將Comparator物件作為第二個引數傳遞給sort方法

Comparator<Item> itemComparator = new Comparator<Item>()
{
    public int compare(Item a, Item b)
    {
        return a.partumber - b.partNumber;
    }
};
Collections.sort(items,itemComparator);

如果想按照降序排列,可以使用一個靜態方法Collections.reverseOrder(),這個方法將返回一比較器,比較器則返回b.compareTo(a),例如:

Collections.sort(staff, Collections.reverseOrder())

這個方法將根據元素型別的compareTo方法給定排序順序,按照逆序對列表staff進行排序,同樣

Collections.sort(staff, Collections.reverseOrder(itemComparator))

將逆置itemComparator的次序

  • 1.如果列表支援set方法,則是可修改的
  • 2.如果列表支援add和remove方法,則是可改變大小的

Collections類有一個演算法shuffle,隨機的混排列表中的順序:

ArrayList<Card> cards = ...;
Collections.shuffle(cards);

如果提供的列表沒有實現RandomAccess介面,shuffle方法將元素複製到陣列中,然後打亂陣列元素的順序,最後再將打亂的元素複製回列表

2.二分查詢

Collections類的binarySearch方法實現了二分演算法,注意,集合必須是排好序的,否則演算法將返回錯誤的答案,想要查詢某個元素,必須提供集合(這個集合要實現List介面)以及要查詢的元素;如果集合沒有采用Comparable介面的compareTo方法進行排序,就要提供比較器物件:

i = Collections.binarySearch(c, element);
i = Collections.binarySearch(c, element, comparator);

如果返回為>=0,表示匹配到了,否則沒有匹配到,可以利用返回值計算要將element插入到集合的哪個位置,以保持有序性,插入位置應該是:insertionPoint = -i -1;其中i應該小於0

如果為binarySearch演算法提供一個連結串列,它將自動地變為線性查詢

binarySearch方法檢查列表引數是否實現了RandomAccess介面,如果實現了這個介面,這個方法就採用二分查詢,否則,將採用線性查詢

3.簡單演算法

Collections類中包含很多實用的簡單演算法,檢視API文件吧

4.編寫自己的演算法

如果編寫自己的演算法(實際上是以集合作為引數的任何方法),應該儘可能使用介面,而不是使用具體的實現

5.遺留的集合

1.Hashtable類

Hashtable類與HashMap類的作用一樣,它們擁有相同的介面,與Vector類的方法一樣,Hashtable的方法是同步的,如果對同步性和遺留程式碼的相容性沒有要求,可以使用HashMap

Java編譯器對大小寫敏感

2.列舉

遺留集合使用Enumeration介面對元素進行遍歷,有2個方法,hasMoreElement和nextElement方法

3.屬性對映表

屬性對映表是一個型別非常特殊的對映表結構,有3個特性:

  • 1.鍵與值都是字串
  • 2.便可以儲存到一個檔案中,也可以從檔案中載入
  • 3.使用一個預設的輔助表

實現屬性對映表的Java平臺稱為Properties

4.棧

對Stack的操作,push和pop方法,stack類擴充套件了Vector,這樣棧可以使用不屬於棧操作的insert和remove方法,即可以在任何位置進行插入和刪除 不僅僅是在棧頂

5.位集

Java平臺的BitSet類用於存放一個位序列,由於位集將位包裝在位元組裡,所以,使用位集要比使用Boolean物件的ArrayList更加高效

BitSet類提供了一個便於讀取、設定和清除各個為的介面,使用這個介面可以避免遮蔽和其他麻煩的位操作