【Java編程思想】11.持有對象
如果一個程序只包含固定數量的且生命周期都是已知的對象,那麽這是一個非常簡單的程序。
Java 類庫中提供一套容器類,來存儲比較復雜的一組對象。其中有 List
、Set
、Queue
、Map
等。這些類也被稱為集合類,Java 的類庫中使用 Collection 這個名字指代該類庫的一個特殊子集(其實 Java 中大部分容器類都實現了 Collection
接口)。
11.1 泛型和類型安全的容器
在 Java SE5 之前的容器,編譯器是允許向容器中插入不正確的類型的。因此在獲取容器中對象時,一旦轉型就會拋出一個異常。
一個類如果沒有顯式地聲明繼承自哪個類,那麽他就自動地繼承自 Object,因此對於容器來說,添加不同的類,無論是編譯器還是運行期都不會有問題,問題在於使用容器中存儲的對象時,會引發意想不到的錯誤。
因此,Java SE5引入了泛型的支持(15章有詳解)。通過使用泛型,可以在編譯期
List<Apple> list = new ArrayList();
在定義泛型之後,從容器中取出對象時,容器會直接轉成泛型對應的類型。同時向上轉型也可以作用在泛型上。
11.2 基本概念
Java 容器類庫劃分為兩種:
Collection
。一個獨立元素的序列,這些元素都服從一條或多條規則。List
必須按照插入的順序保存元素,Set
不能有重復元素,Queue
按照派對規則來確定對象產生的順序(通常與其元素被插入的順序相同)。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
只能用來:
- 使用方法
iterator()
要求容器返回一個Iterator
。Iterator
將準備好返回序列的第一個元素。 - 使用
next()
獲得序列中的下一個元素。 - 使用
hasNext()
檢查序列中是否還有元素。 - 使用
remove()
將叠代器新近返回的元素刪除。
使用 Iterator
時不需要關心容器中的元素數量,只需要向前遍歷列表即可。
Iterator
可以移出有 next()
產生的最後一個元素,這意味著在調用 remove()
之前需要先調用 next()
。
ListIterator
是加強版的 Iterator
子類型,只能用於 List
類的訪問
- 區別於
Iterator
,ListIterator
是可以雙向移動的 - 還可以產生相對於叠代器在列表中指向的當前位置的前一個和後一個元素的索引
- 可以使用
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()
:移出並返回列表最後一個元素。
LinkedList
是 Queue
的一個實現。對比 Queue
接口,可以發現 Queue
在 LinkedList
的基礎上添加了 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)的容器。從容器的一端放入事物,從另一端取出,並且事物放入容器的順序與取出的順序是相同的。
隊列常被當做一種可靠的將對象從程序的某個區域傳輸到另一個區域的途徑-->例如並發編程中,安全的將對象從一個任務傳輸給另一個任務。
LinkedList
是 Queue
的一個實現,可以將其向上轉型為 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()
方法可以返回一個 Map
,System.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()
方法沒有影響到原來的數組-->只是打亂了列表中的引用。
如果用 ArrayList
將 Arrays.asList(ia)
方法產生的結果包裝起來,那麽只會打亂引用;而不包裝的時候,就會直接修改低層的數組。因此 Java 中數組和 List
的本質是不同的。
11.14 總結
Java 提供的持有對象的方式:
- 數組:數組保存明確類型的對象,查詢對象的時候不需要對結果做類型轉換。數組可以是多維的,也可以保存基本類型數據。但是數組的容量,在生成後就不能改變了。
- Collection/Map:
Collection
保存單一的元素,Map
保存關聯的鍵值對。在使用了泛型指定了容器存放的對象類型後,查詢對象時便也不需要進行類型轉換。Collection
和Map
的尺寸是動態的,不能持有基本類型(但是自動包裝機制-裝箱-會將存入容器的基本類型進行數據轉換)。 - List:數組和
List
都是排好序的容器,但是List
能夠自動擴充容量。大量隨機訪問使用ArrayList
,插入刪除元素使用LinkedList
。 - Queue:各種
Queue
以及棧的行為,由LinkedList
提供支持。 - Map:
Map
是一種將對象與對象相關聯的設計。快速訪問使用HashMap
,保持鍵的排序狀態使用TreeMap
,但是速度略慢,保持元素插入順序以及快速訪問能力使用LinkedHashMap
。 - Set:
Set
不接收重復元素。需要快速查詢使用HashSet
,保持元素排序狀態使用TreeSet
,保持元素插入順序使用LinkedHashSet
。 - 有一部分的容器已經過時不應該使用:
Vector
,Hashtable
,Stack
。
下面是 Java 容器的簡圖:
- 黑框:常用容器
- 點線框:接口
- 實線框:普通的(具體的)類
- 空心箭頭點線:一個特定的類實現了接口
- 實心箭頭:某個類可以生成箭頭所指向類的對象
【Java編程思想】11.持有對象