1. 程式人生 > >【Java編程思想】11.持有對象

【Java編程思想】11.持有對象

影響 快速查詢 等待 類型轉換 字母 行為 repl over 結果

如果一個程序只包含固定數量的且生命周期都是已知的對象,那麽這是一個非常簡單的程序。

Java 類庫中提供一套容器類,來存儲比較復雜的一組對象。其中有 ListSetQueueMap 等。這些類也被稱為集合類,Java 的類庫中使用 Collection 這個名字指代該類庫的一個特殊子集(其實 Java 中大部分容器類都實現了 Collection 接口)。


11.1 泛型和類型安全的容器

在 Java SE5 之前的容器,編譯器是允許向容器中插入不正確的類型的。因此在獲取容器中對象時,一旦轉型就會拋出一個異常。
一個類如果沒有顯式地聲明繼承自哪個類,那麽他就自動地繼承自 Object,因此對於容器來說,添加不同的類,無論是編譯器還是運行期都不會有問題,問題在於使用容器中存儲的對象時,會引發意想不到的錯誤。
因此,Java SE5引入了泛型的支持(15章有詳解)。通過使用泛型,可以在編譯期

防止將錯誤類型的對象放置到容器中。

List<Apple> list = new ArrayList();

在定義泛型之後,從容器中取出對象時,容器會直接轉成泛型對應的類型。同時向上轉型也可以作用在泛型上。


11.2 基本概念

Java 容器類庫劃分為兩種:

  1. Collection。一個獨立元素的序列,這些元素都服從一條或多條規則。List 必須按照插入的順序保存元素,Set 不能有重復元素,Queue 按照派對規則來確定對象產生的順序(通常與其元素被插入的順序相同)。
  2. Map。一組成對的鍵值對對象,允許使用鍵來查找值。映射表允許我們使用另一個對象來查找某個對象,它也被稱作關聯數組
    ,因為他將某些對象與另外一些對象關聯在了一起;或者被稱為字典,因為你可以使用鍵對象來查找值對象

使用容器的時候,可能有些情況不需要使用向上轉型的方式,例如 LinkedList 具有 List 接口中未包含的方法;TreeMap 具有 Map 中未包含的方法,因此如果要使用這些方法,就不能將他們向上轉型為接口。

所有的 Collection 都可以用 foreach 語法遍歷。


11.3 添加一組元素

添加一組元素有多種方法:

  • Arrays.asList() 方法,接收一個數組或者是逗號分隔的元素列表(使用可變參數),並將其轉換為一個 List 對象。
  • Collections.addAll()
    方法,接收一個 Collection 對象,以及一個數組或是用逗號分隔的列表,並將元素添加到 Collection 中。

兩者的使用都有限制,Arrays.asList() 方法的輸出在底層的表示是數組,因此不能調整尺寸。而 Collections.addAll() 方法只能接受另一個 Collection 對象作為參數。

像如下這種初始化,可以告訴編譯器,由 Arrays.asList() 方法產生的 List 類型(實際的目標類型)。這種成為顯式類型參數說明

List<Snow> snow4 = Arrays.<Snow>asList(new Light(), new Heavy());

11.4 容器的打印

數組的打印必須借助 Arrays.toString() 來表示(或者遍歷打印);但是容器的打印可以直接使用 print(默認就能生成可讀性很好的結果)。

  • Collection 在每個“槽”中只能保存一個元素
    • List 以特定的順序保存一組元素。
    • Set 元素不能重復。
    • Queue 只允許在容器的一“端”插入對象,並從另一“端”移出對象。
  • Map 在每個“槽”內保存兩個對象,即和與之相關聯的

關於各種容器的實現類型特點:

  • ArrayList:按插入順序保存元素,性能高
  • LinkedList:按插入順序保存元素,性能略低於 ArrayList
  • HashSet:最快的獲取元素方式
  • TreeSet:按照比較結果升序保存對象,註重存儲順序
  • LinkedHashSet:按照添加順序保存對象
  • HashMap:提供最快的查找技術,沒有特定順序
  • TreeMap:敖釗比較結果的升序保存鍵
  • LinkedHashMap:按照插入順序保存鍵

11.5 List

  • ArrayList:擅長於隨機訪問元素,在 List 的中間插入和移出元素時較慢。
  • LinkedList:通過代價較低的在 List 中間進行的插入和刪除操作,提供優化的順序訪問。另外其特性集更大。

與數組不同,List 允許在他被創建之後添加元素、移除元素或者自我調整尺寸,是一種可修改的序列。

關於 List 的方法:

  • contains():確定對象是否在列表中
  • remove():從列表中移出元素
  • indexOf():獲取對象在列表中的索引編號
  • subList():從較大的列表中創建出一個片段
  • containsAll():判斷片段是否包含於列表
  • retainAll():求列表交集
  • removeAll():移出指定的全部元素
  • replace():在指定索引處,用指定參數替換該位置的元素
  • addAll():在初始列表中插入新的列表
  • isEmpty():校驗列表是否為空
  • clear():清空列表
  • toArray():列表轉數組

11.6 叠代器

叠代器(也是一種設計模式)是一個對象那個,它的工作室遍歷並選擇序列中的對象,使用者不需要關心該序列的底層結構。

Java 中有叠代器 Iterator,只能單向移動,Iterator 只能用來:

  1. 使用方法 iterator() 要求容器返回一個 IteratorIterator 將準備好返回序列的第一個元素。
  2. 使用 next() 獲得序列中的下一個元素。
  3. 使用 hasNext() 檢查序列中是否還有元素。
  4. 使用 remove() 將叠代器新近返回的元素刪除。

使用 Iterator 時不需要關心容器中的元素數量,只需要向前遍歷列表即可。
Iterator 可以移出有 next() 產生的最後一個元素,這意味著在調用 remove() 之前需要先調用 next()

ListIterator 是加強版的 Iterator 子類型,只能用於 List 類的訪問

  • 區別於 IteratorListIterator 是可以雙向移動的
  • 還可以產生相對於叠代器在列表中指向的當前位置的前一個和後一個元素的索引
  • 可以使用 set() 方法替換它訪問過的最後一個元素
  • 可以通過調用 listIterator() 方法產生一個指向 List 開始處的 ListIterator
  • 還可以通過調用 listIterator(n) 方法創建一個一開始就指向列表索引為 n 的元素處的 ListIterator

11.7 LinkedList

LinkedList 在執行插入和刪除時效率更高,隨機訪問操作方面要遜色一些。
除此之外,LinkedList 還添加了可以使其用作棧、隊列和雙端隊列的方法。

  • getFirst()/element():返回列表頭,列表為空是拋出 NoSuchElementException
  • peek():返回列表頭,列表為空返回 null。
  • removeFirst()/remove():移出並返回列表頭,列表為空是拋出 NoSuchElementException
  • poll():移出並返回列表頭,列表為空返回 null。
  • addFirst()/add()/addLst():將某元素插入到列表尾部。
  • removeLast():移出並返回列表最後一個元素。

LinkedListQueue 的一個實現。對比 Queue 接口,可以發現 QueueLinkedList 的基礎上添加了 element()offer()peek()poll()remove()方法。


11.8 Stack

通常是指”後進先出”(LIFO)的容器。有時棧也被稱為疊加棧,因為最後壓棧的元素最先出棧。
LinkedList 具有能夠直接實現一個棧的所有功能和方法,因此可以直接將 LinkedList 作為棧使用。

public class Stack<T> {
    private LinkedList<T> storage = new LinkedList<T>();
    public void push(T v) { storage.addFirst(v); }
    public T peek() { return storage.getFirst(); }
    public T pop() { return storage.removeFirst(); }
    public boolean empty() { return storage.isEmpty(); }
    public String toString() { return storage.toString(); }
}

11.9 Set

Set 是基於對象的值來確定歸屬性的,具有與 Collection 完全一樣的接口(實際上 Set 就是 Collection,只是行為不同--一種繼承與多態的典型應用,表現不同的行為)。

  • HashSet 使用了散列(更多17章介紹),因此存儲元素沒有任何規律。
  • TreeSet 將元素存儲在紅黑樹數據結構中,輸出結果是排序的。默認按照字典序排序(A-Z,a-z),如果想按照字母序排序(Aa-Zz),可以指定 new TreeSet<String>(String.CASE_INSENSITIVE_ORDER)
  • LinkedHashSet 使用了散列,但是也用了鏈表結構來維護元素的插入順序。

11.10 Map

get(key) 方法會返回與鍵關聯的值,如果鍵不在容器中,則返回 null。
containKey()containValue() 可以查看鍵和值是否包含在 Map 中。

Map 實際上就是講對象映射到其他對象上的工具。


11.11 Queue

隊列是一個典型的先進先出(FIFO)的容器。從容器的一端放入事物,從另一端取出,並且事物放入容器的順序與取出的順序是相同的。
隊列常被當做一種可靠的將對象從程序的某個區域傳輸到另一個區域的途徑-->例如並發編程中,安全的將對象從一個任務傳輸給另一個任務。

LinkedListQueue 的一個實現,可以將其向上轉型為 Queue
Queue 接口窄化了對 LinkedList 的方法的訪問權限,以使得只有恰當的方法才可以使用,因此能夠訪問的 LinkedList 的方法會變少。

隊列規則是指在給定一組隊列中的元素的情況下,確定下一個彈出隊列的元素的規則。先進先出聲明的是下一個元素應該是等待時間最長的元素。
優先級隊列聲明下一個彈出元素時最需要的元素(具有最高的優先級)。PriorityQueue 提供了這種實現。當在 PriorityQueue 上調用 offer() 方法插入一個對象時,這個對象會在隊列中被排序(通常這類隊列的實現,會在插入時排序-維護一個堆-但是他們也可能在移出時選擇最重要的元素)。默認的排序會使用對象在隊列中的自然順序,但是可以通過提供自己的 Comparator 來修改這個順序。 這樣能保證在對 PriorityQueue 使用 peek()poll()remove() 方法時,會先處理隊列中優先級最高的元素。


11.12 Collection 和 Iterator

Collection 是描述所有序列容器的共性的根接口。java.util.AbstractCollection 類提供了 Collection 的默認實現,可以創建 AbstarctCollection 的子類型,其中不會有不必要的代碼重復。
實現了 Collection 意味著需要提供 iterator() 方法。
(這章沒太看懂,回頭再看一遍)


11.13 Foreach 與叠代器

foreach 是基於 Iterator 接口完成實現的,Iterator 接口被 foreach 用來在序列中移動。任何實現了 Iterator 接口的類,都可以用於 foreach 語句。

System.getenv() 方法可以返回一個 MapSystem.getenv().entrySet() 可以產生一個有 Map.Entry 的元素構成的 Set,並且這個 Set 是一個 Iterable,因此它可以用於 foreach 循環。

foreach 語句可以用於數組或其他任何實現了 Iterable 的類,但是並不意味這數組肯定也是一個 Iterable,而且任何自動包裝都不會自動發生。


在類實現 Iterable 接口時,如果想要添加多種在 foreach 語句中使用這個類的方法(在 foreach 中的條件中使用類的方法),有一種方案是適配器模式。-->在實現的基礎上,增加一個返回 Iterable 對象的方法,則該方法就可以用於 foreach 語句。例如

// 添加一個反向的叠代器
class ReversibleArrayList<T> extends ArrayList<T> {
    public ReversibleArrayList(Collection<T> c) { super(c); }
    public Iterable<T> reversed() {
        return new Iterable<T>() {
            public Iterator<T> iterator() {
                return new Iterator<T>() {
                    int current = size() - 1;
                    @Override
                    public boolean hasNext() { return current > -1; }
                    @Override
                    public T next() { return get(current--); }
                    @Override
                    public void remove() { // Not implemented
                        throw new UnsupportedOperationException();
                    }};
            }};
    }
}
// 調用
for(String s : ral.reversed()) {
    System.out.print(s + " ");
}

使用 Collection.shuffle() 時,可以看到包裝與否對於結果的影響。

Integer[] ia = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
List<Integer> list1 = new ArrayList<>(Arrays.asList(ia));
System.out.println("Before shuffling: " + list1);
Collections.shuffle(list1, rand);
System.out.println("After shuffling: " + list1);
System.out.println("array: " + Arrays.toString(ia));

List<Integer> list2 = Arrays.asList(ia);
System.out.println("Before shuffling: " + list2);
Collections.shuffle(list2, rand);
System.out.println("After shuffling: " + list2);
System.out.println("array: " + Arrays.toString(ia));

輸出:

Before shuffling: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
After shuffling: [4, 6, 3, 1, 8, 7, 2, 5, 10, 9]
array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Before shuffling: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
After shuffling: [9, 1, 6, 3, 7, 2, 5, 10, 4, 8]
array: [9, 1, 6, 3, 7, 2, 5, 10, 4, 8]

從結果中可以看到,Collection.shuffle() 方法沒有影響到原來的數組-->只是打亂了列表中的引用。
如果用 ArrayListArrays.asList(ia) 方法產生的結果包裝起來,那麽只會打亂引用;而不包裝的時候,就會直接修改低層的數組。因此 Java 中數組和 List 的本質是不同的。


11.14 總結

Java 提供的持有對象的方式:

  1. 數組:數組保存明確類型的對象,查詢對象的時候不需要對結果做類型轉換。數組可以是多維的,也可以保存基本類型數據。但是數組的容量,在生成後就不能改變了。
  2. Collection/Map:Collection 保存單一的元素,Map 保存關聯的鍵值對。在使用了泛型指定了容器存放的對象類型後,查詢對象時便也不需要進行類型轉換。CollectionMap 的尺寸是動態的,不能持有基本類型(但是自動包裝機制-裝箱-會將存入容器的基本類型進行數據轉換)。
  3. List:數組和 List 都是排好序的容器,但是 List 能夠自動擴充容量。大量隨機訪問使用 ArrayList,插入刪除元素使用 LinkedList
  4. Queue:各種 Queue 以及棧的行為,由 LinkedList 提供支持。
  5. Map:Map 是一種將對象與對象相關聯的設計。快速訪問使用 HashMap,保持的排序狀態使用 TreeMap,但是速度略慢,保持元素插入順序以及快速訪問能力使用 LinkedHashMap
  6. Set:Set 不接收重復元素。需要快速查詢使用 HashSet,保持元素排序狀態使用 TreeSet,保持元素插入順序使用 LinkedHashSet
  7. 有一部分的容器已經過時不應該使用:VectorHashtableStack

下面是 Java 容器的簡圖:
技術分享圖片

  • 黑框:常用容器
  • 點線框:接口
  • 實線框:普通的(具體的)類
  • 空心箭頭點線:一個特定的類實現了接口
  • 實心箭頭:某個類可以生成箭頭所指向類的對象

【Java編程思想】11.持有對象