集合 collection
講集合collection之前,我們先分清三個概念:
- colection 集合,用來表示任何一種數據結構
- Collection 集合接口,指的是 java.util.Collection接口,是 Set、List 和 Queue 接口的超類接口
- Collections 集合工具類,指的是 java.util.Collections 類。
要求瞭解的接口有:Collection , Set , SortedSet , List , Map , SortedMap , Queue , NavigableSet , NavigableMap, 還有一個 Iterator 接口也是必須瞭解的。
考試要求瞭解的類有: HashMap , Hashtable ,TreeMap , LinkedHashMap , HashSet , LinkedHashSet ,TreeSet , ArrayList , Vector , LinkedList , PriorityQueuee , Collections , Arrays
下面給出一個集合之間的關係圖:
上圖中加粗線的ArrayList 和 HashMap 是我們重點講解的對象。下面這張圖看起來層級結構更清晰些。
我們這裏說的集合指的是小寫的collection,集合有4種基本形式,其中前三種的父接口是Collection。
- List 關注事物的索引列表
- Set 關注事物的唯一性
- Queue 關注事物被處理時的順序
- Map 關注事物的映射和鍵值的唯一性
一、Collection 接口
Collection接口是 Set 、List 和 Queue 接口的父接口,提供了多數集合常用的方法聲明,包括 add()、remove()、contains() 、size() 、iterator() 等。
add(E e) |
將指定對象添加到集合中 |
remove(Object o) |
將指定的對象從集合中移除,移除成功返回true,不成功返回false |
contains(Object o) |
查看該集合中是否包含指定的對象,包含返回true,不包含返回flase |
size() |
返回集合中存放的對象的個數。返回值爲int |
clear() |
移除該集合中的所有對象,清空該集合。 |
iterator() |
返回一個包含所有對象的iterator對象,用來循環遍歷 |
toArray() |
返回一個包含所有對象的數組,類型是Object |
toArray(T[] t) |
返回一個包含所有對象的指定類型的數組 |
我們在這裏只舉一個把集合轉成數組的例子,因爲Collection本身是個接口所以,我們用它的實現類ArrayList做這個例子。
01 |
import
java.util.ArrayList; |
02 |
import
java.util.Collection; |
04 |
public
class
CollectionTest { |
06 |
public
static
void
main(String[] args) { |
08 |
String a = "a" ,b= "b" ,c= "c" ;
|
09 |
Collection list = new
ArrayList(); |
14 |
String[] array = list.toArray( new
String[ 1 ]);
|
16 |
for (String s : array){
|
17 |
System.out.println(s); |
編譯並運行程序,檢查結果:
二、幾個比較重要的接口和類簡介
1、List接口
List 關心的是索引,與其他集合相比,List特有的就是和索引相關的一些方法:get(int index) 、 add(int index,Object o) 、 indexOf(Object o) 。
ArrayList 可以將它理解成一個可增長的數組,它提供快速迭代和快速隨機訪問的能力。
LinkedList 中的元素之間是雙鏈接的,當需要快速插入和刪除時LinkedList成爲List中的不二選擇。
Vector 是ArrayList的線程安全版本,性能比ArrayList要低,現在已經很少使用
2、Set接口
Set關心唯一性,它不允許重複。
HashSet 當不希望集合中有重複值,並且不關心元素之間的順序時可以使用此類。
LinkedHashset 當不希望集合中有重複值,並且希望按照元素的插入順序進行迭代遍歷時可採用此類。
TreeSet 當不希望集合中有重複值,並且希望按照元素的自然順序進行排序時可以採用此類。(自然順序意思是某種和插入順序無關,而是和元素本身的內容和特質有關的排序方式,譬如“abc”排在“abd”前面。)
3、Queue接口
Queue用於保存將要執行的任務列表。
LinkedList 同樣實現了Queue接口,可以實現先進先出的隊列。
PriorityQueue 用來創建自然排序的優先級隊列。番外篇中有個例子http://android.yaohuiji.com/archives/3454你可以看一下。
4、Map接口
Map關心的是唯一的標識符。他將唯一的鍵映射到某個元素。當然鍵和值都是對象。
HashMap 當需要鍵值對錶示,又不關心順序時可採用HashMap。
Hashtable 注意Hashtable中的t是小寫的,它是HashMap的線程安全版本,現在已經很少使用。
LinkedHashMap 當需要鍵值對,並且關心插入順序時可採用它。
TreeMap 當需要鍵值對,並關心元素的自然排序時可採用它。
三、ArrayList的使用
ArrayList是一個可變長的數組實現,讀取效率很高,是最常用的集合類型。
1、ArrayList的創建
在Java5版本之前我們使用:
1 |
List list =
new
ArrayList(); |
在Java5版本之後,我們使用帶泛型的寫法:
1 |
List<String> list =
new
ArrayList<String>(); |
上面的代碼定義了一個只允許保存字符串的列表,尖括號括住的類型就是參數類型,也成泛型。帶泛型的寫法給了我們一個類型安全的集合。關於泛型的知識可以參見這裏。
2、ArrayList的使用:
01 |
List<String> list =
new
ArrayList<String>(); |
04 |
list.add( "konikiwa!" );
|
07 |
System.out.println(list.size()); |
08 |
System.out.println(list.contains( 21 ));
|
09 |
System.out.println(list.remove( "hi!" ));
|
10 |
System.out.println(list.size()); |
關於List接口中的方法和ArrayList中的方法,大家可以看看JDK中的幫助。
3、基本數據類型的的自動裝箱:
我們知道集合中存放的是對象,而不能是基本數據類型,在Java5之後可以使用自動裝箱功能,更方便的導入基本數據類型。
1 |
List<Integer> list =
new
ArrayList<Integer>(); |
2 |
list.add( new
Integer( 42 ));
|
4、ArrayList的排序:
ArrayList本身不具備排序能力,但是我們可以使用Collections類的sort方法使其排序。我們看一個例子:
01 |
import
java.util.ArrayList; |
02 |
import
java.util.Collections; |
03 |
import
java.util.List; |
07 |
public
static
void
main(String[] args) { |
08 |
List<String> list = new
ArrayList<String>(); |
11 |
list.add( "konikiwa!" );
|
15 |
System.out.println( "排序前:" + list);
|
17 |
Collections.sort(list); |
19 |
System.out.println( "排序後:" + list);
|
編譯並運行程序查看結果:
排序前:[nihao!, hi!, konikiwa!, hola, Bonjour]
排序後:[Bonjour, hi!, hola, konikiwa!, nihao!]
5、數組和List之間的轉換
從數組轉換成list,可以使用Arrays類的asList()方法:
01 |
import
java.util.ArrayList; |
02 |
import
java.util.Collections; |
03 |
import
java.util.List; |
07 |
public
static
void
main(String[] args) { |
09 |
String[] sa = { "one" , "two" , "three" , "four" };
|
10 |
List list = Arrays.asList(sa); |
11 |
System.out.println( "list:" +list);
|
12 |
System.out.println( "list.size()=" +list.size());
|
6、Iterator和for-each
在for-each出現之前,我們想遍歷ArrayList中的每個元素我們會使用Iterator接口:
01 |
import
java.util.Arrays; |
02 |
import
java.util.Iterator; |
03 |
import
java.util.List; |
07 |
public
static
void
main(String[] args) { |
10 |
List<String> list = Arrays.asList( "one" ,
"two" ,
"three" ,
"four" );
|
13 |
Iterator<String> it = list.iterator(); |
16 |
while
(it.hasNext()) { |
17 |
System.out.println(it.next()); |
在for-each出現之後,遍歷變得簡單一些:
01 |
import
java.util.Arrays; |
02 |
import
java.util.Iterator; |
03 |
import
java.util.List; |
07 |
public static
void main(String[] args) {
|
10 |
List<String> list = Arrays.asList( "one" ,
"two" , "three" ,
"four" );
|
12 |
for (String s : list) {
|
13 |
System.out.println(s); |
本講內容:Map HashMap
前面課程中我們知道Map是個接口,它關心的是映射關係,它裏面的元素是成對出現的,鍵和值都是對象且鍵必須保持唯一。這一點上看它和Collection是很不相同的。
一、Map接口
Map接口的常用方法如下表所示:
put(K key, V value) |
向集合中添加指定的鍵值對 |
putAll(Map <? extends K,? extends V> t) |
把一個Map中的所有鍵值對添加到該集合 |
containsKey(Object key) |
如果包含該鍵,則返回true |
containsValue(Object value) |
如果包含該值,則返回true |
get(Object key) |
根據鍵,返回相應的值對象 |
keySet() |
將該集合中的所有鍵以Set集合形式返回 |
values() |
將該集合中所有的值以Collection形式返回 |
remove(Object key) |
如果存在指定的鍵,則移除該鍵值對,返回鍵所對應的值,如果不存在則返回null |
clear() |
移除Map中的所有鍵值對,或者說就是清空集合 |
isEmpty() |
查看Map中是否存在鍵值對 |
size() |
查看集合中包含鍵值對的個數,返回int類型 |
因爲Map中的鍵必須是唯一的,所以雖然鍵可以是null,只能由一個鍵是null,而Map中的值可沒有這種限制,值爲null的情況經常出現,因此get(Object key)方法返回null,有兩種情況一種是確實不存在該鍵值對,二是該鍵對應的值對象爲null。爲了確保某Map中確實有某個鍵,應該使用的方法是 containsKey(Object key) 。
二、HashMap
HashMap是最常用的Map集合,它的鍵值對在存儲時要根據鍵的哈希碼來確定值放在哪裏。
1、HashMap的基本使用:
01 |
import
java.util.Collection; |
02 |
import
java.util.HashMap; |
08 |
public
static
void
main(String[] args) { |
10 |
Map<Integer,String> map = new
HashMap<Integer,String>(); |
19 |
System.out.println( "map.size()=" +map.size());
|
20 |
System.out.println( "map.containsKey(1)=" +map.containsKey( 2 ));
|
21 |
System.out.println( "map.containsKey(null)=" +map.containsKey( null ));
|
22 |
System.out.println( "map.get(null)=" +map.get( null ));
|
24 |
System.out.println( "map.get(2)=" +map.get( 2 ));
|
26 |
System.out.println( "map.get(null)=" +map.get( null ));
|
28 |
Set set = map.keySet(); |
29 |
System.out.println( "set=" +set);
|
31 |
Collection<String> c = map.values(); |
33 |
System.out.println( "Collection=" +c);
|
編譯並運行程序,查看結果:
2 |
map.containsKey( 1 )= true
|
3 |
map.containsKey( null )= true
|
8 |
Collection=[黃瓜, 白菜, 蘿蔔, 茄子,
null ]
|
2、HashMap 中作爲鍵的對象必須重寫Object的hashCode()方法和equals()方法
下面看一個我花了1個小時構思的例子,熟悉龍槍的朋友看起來會比較親切,設定了龍和龍的巢穴,然後把它們用Map集合對應起來,我們可以根據龍查看它巢穴中的寶藏數量,例子只是爲了說明hashCode這個知識點,所以未必有太強的故事性和合理性,湊合看吧:
01 |
import
java.util.HashMap; |
06 |
public
static
void
main(String[] args) { |
09 |
Map<dragon , Nest> map = new
HashMap<dragon , Nest>(); |
12 |
map.put( new
Dragon( "銳刃" ,
98 ),
new
Nest( 98 ));
|
13 |
map.put( new
Dragon( "明鏡" ,
95 ),
new
Nest( 95 ));
|
14 |
map.put( new
Dragon( "碧雷" ,
176 ),
new
Nest( 176 ));
|
15 |
map.put( new
Dragon( "瑪烈" ,
255 ),
new
Nest( 255 ));
|
18 |
System.out.println( "碧雷巢穴中有多少寶藏:"
+ map.get( new
Dragon( "碧雷" ,
176 )).getTreasure());
|
26 |
Dragon(String name, int
level) { |
37 |
public
int
getLevel() { |
41 |
public
void
setLevel( int
level) { |
45 |
public
String getName() { |
49 |
public
void
setName(String name) { |
59 |
final
int
DRAGON_M = 4162 ;
|
69 |
this .treasure = level * level * DRAGON_M;
|
76 |
public
int
getLevel() { |
80 |
public
void
setLevel( int
level) { |
82 |
this .treasure = level * level * DRAGON_M;
|
編譯並運行查看結果:
1 |
Exception in thread
"main"
java.lang.NullPointerException |
2 |
at Test.main(Test.java: 18 )
|
我們發現竟然報了錯誤,第18行出了空指針錯誤,也就是說get方法竟然沒有拿到預期的巢穴對象。
在這裏我們就要研究一下爲什麼取不到了。我們這裏先解釋一下HashMap的工作方式。
假設現在有個6張中獎彩票的存根,放在5個桶裏(彩票首位只有1-5,首位是1的就放在一號桶,是2的就放在2號桶,依次類推),現在你拿了3張彩票來兌獎,一個號碼是113,一個號碼是213,一個號碼是313。那麼現在先兌第一張,取出一號桶裏的存根發現存根號碼和你的號碼不符,所以你第一張沒中獎。繼續兌第二張,二號桶裏就沒存根所以就直接放棄了,把三號桶裏的所有彩票存根都拿出來對應一番,最後發現有一個存根恰好是313,那麼恭喜你中獎了。
HashMap在確定一個鍵對象和另一個鍵對象是否是相同時用了同樣的方法,每個桶就是一個鍵對象的散列碼值,桶裏放的就是散列碼相同的彩票存根,如果散列碼不同,那麼肯定沒有相關元素存在,如果散列碼相同,那麼還要用鍵的equals()方法去比較是否相同,如果相同才認爲是相同的鍵。簡單的說就是 hashCode()相同 && equals()==true 時纔算兩者相同。
到了這裏我們應該明白了,在沒有重寫一個對象的hashcode()和equals()方法之前,它們執行的是Object中對應的方法。而 Object的hashcode()是用對象在內存中存放的位置計算出來的,每個對象實例都不相同。Object的equals()的實現更簡單就是看兩個對象是否==,也就是兩個對象除非是同一個對象,否則根本不會相同。因此上面的例子雖然都是名字叫碧雷的龍,但是HashMap中卻無法認可它們是相同的。
因此我們只有重寫Key對象的hashCode()和equals()方法,才能避免這種情形出現,好在Eclipse可以幫我們自動生成一個類的hashCode()和equals(),我們把上面的例子加上這兩個方法再試試看:
001 |
import
java.util.HashMap; |
002 |
import
java.util.Map; |
006 |
public
static
void
main(String[] args) { |
009 |
Map<dragon , Nest> map = new
HashMap<dragon , Nest>(); |
012 |
map.put( new
Dragon( "銳刃" ,
98 ),
new
Nest( 98 ));
|
013 |
map.put( new
Dragon( "明鏡" ,
95 ),
new
Nest( 95 ));
|
014 |
map.put( new
Dragon( "碧雷" ,
176 ),
new
Nest( 176 ));
|
015 |
map.put( new
Dragon( "瑪烈" ,
255 ),
new
Nest( 255 ));
|
018 |
System.out.println( "碧雷巢穴中有多少寶藏:"
+ map.get( new
Dragon( "碧雷" ,
176 )).getTreasure());
|
026 |
Dragon(String name, int
level) { |
037 |
public
int
getLevel() { |
041 |
public
void
setLevel( int
level) { |
045 |
public
String getName() { |
049 |
public
void
setName(String name) { |
054 |
public
int
hashCode() { |
055 |
final
int
PRIME = 31 ;
|
057 |
result = PRIME * result + level; |
058 |
result = PRIME * result + ((name == null ) ?
0
: name.hashCode()); |
063 |
public
boolean
equals(Object obj) { |
068 |
if
(getClass() != obj.getClass()) |
070 |
final
Dragon other = (Dragon) obj; |
071 |
if
(level != other.level) |
074 |
if
(other.name != null )
|
076 |
} else
if
(!name.equals(other.name)) |
087 |
final
int
DRAGON_M = 4162 ;
|
090 |
private
int
treasure; |
097 |
this .treasure = level * level * DRAGON_M;
|
104 |
public
int
getLevel() { |
108 |
public
void
setLevel( int
level) { |
110 |
this .treasure = level * level * DRAGON_M;
|
編譯並運行查看結果:
這一次正常輸出了,真不容易^_^
好了本講就到這裏。
對象的集合
如果程序的對象數量有限,且壽命可知,那麼這個程序是相當簡單的。
數組
數組與其它容器的區別體現在三個方面:效率,類型識別以及可以持有primitives。數組是Java提供的,能隨機存儲和訪問reference序列的諸多方法中的,最高效的一種。數組是一個簡單的線性序列,所有它可以快速的訪問其中的元素。但是速度是有代價的;當你創建了一個數組之後,它的容量就固定了,而且在其生命週期裏不能改變。也許你會提議先創建一個數組,等到快不夠用的時候,再創建一個新的,然後將舊的數組裏的reference全部導到新的裏面。其實(我們以後會講的)ArrayList就是這麼做的。但是這種靈活性所帶來的開銷,使得ArrayList的效率比起數組有了明顯下降。
Java對數組和容器都做邊界檢查;如果過了界,它舊會給一個RuntimeException。這種異常表明這個錯誤是由程序員造成的,這樣你就用不着再在程序裏面檢查了。
還有一些泛型容器類包括List,Set和Map。他們處理對象的時候就好像這些對象都沒有自己的具體類型一樣。也就是說,容器將它所含的元素都看成是(Java中所有類的根類)Object的。這樣你只需要建一種容器,就能把所有類型的對象全都放進去。從這個角度來看,這種做法很不錯(只是苦了 primitive。如果是常量,你還可以用Java的primitive的Wrapper類;如果是變量,那就只能放在你自己的類裏了)。與其他泛型容器相比,這裏體現數組的第二革優勢:創建數組的時候,你也同時指明瞭它所持有的對象的類型(這又引出了第三點--數組可以持有primitives,而容器卻不行)。也就是說,它會在編譯的時候作類型檢查,從而防止你插入錯誤類型的對象,或者是在提取對象的時候把對象的類型給搞錯了。Java在編譯和運行時都能阻止你將一個不恰當的消息傳給對象。所有這並不是說使用容器就有什麼危險,只是如果編譯器能夠幫你指定,那麼程序運行會更快,最終用戶也會較少收到程序運行異常的騷擾。
從效率和類型檢查的角度來看,使用數組總是沒錯的。但是,如果你在解決一個更爲一般的問題,那數組就會顯得功能太弱了點。
數組是第一流的對象
不管你用的是那種類型的數組,數組的標識符實際上都是一個“創建在堆(heap)裏的實實在在的對象的”reference。實際上是那個對象持有其他對象的reference。你即可以用數組的初始化語句,隱含地創建這個對象,也可以用new表達式,明確地創建這個對象,只讀的length屬性能告訴你數組能存儲多少元素。它是數組對象的一部分(實際上也是你唯一能訪問的屬性或方法)。‘[]’語法是另一條訪問數組對象的途徑。
你沒法知道數組裏面究竟放了多少元素,因爲length只是告訴你數組能放多少元素,也就是說是數組對象的容量,而不是它真正已經持有的元素的數量。但是,創建數組對象的時候,它所持有的reference都會被自動地初始化爲null,所以你可以通過檢查數組的某個“槽位”是否爲null,來判斷它是否持有對象。以此類推,primitive的數組,會自動來數字初始化爲零,字符初始化爲(char)0,boolean初始化爲false。
primitive容器
容器類只能持有Object對象的reference。而數組除了能持有Objects的reference之外,還可以直接持有primitive。當然可以使用諸如Integer,Double之類的wrapper類。把primitive的值放到容器中,淡這樣總有點怪怪的。此外, primitive數組的效率要比wrapper類容器的高出許多。
當然,如果你使用primitive的時候,還需要那種“能隨需要自動擴展的”容器類的靈活性,那就不能用數組了。你只能用容器來存儲primitive的wrapper類。
返回一個數組
假設你寫了一個方法,它返回的不是一個而是一組東西。那麼在Java中就可以返回的“就是一個數組”。與C++不同,你永遠也不必爲Java的數組操心--只要你還需要它,它就還在;一旦你用完了,垃圾回收器會幫你把它打掃乾淨。
Arrays類
java.util裏面有一個Arrays類,它包括了一組可用於數組的static方法,這些方法都是一些實用工具。其中有四個基本方法:用來比較兩個數組是否相等的equals();用來填充的fill();用來對數組進行排序的sort();以及用於在一個已排序的數組中查找元素的 binarySearch()。所有這些方法都對primitive和Object進行了重載。此外還有一個asList()方法,它接受一個數組,然後把它轉成一個List容器。
雖然Arrays還是有用的,但它的功能並不完整。舉例來說,如果它能讓我們不用寫for循環就能直接打印數組,那就好了。此外,正如你所看到的fill()只能用一個值填數組。所以,如果你想把隨即生成的數字填進數組的話,fill()是無能爲力的。
複製一個數組
Java標準類庫提供了一個System.arraycopy()的static方法。相比for循環,它能以更快的速度拷貝數組。System.arraycopy()對所有類型都作了重載。
對象數組和primitive數組都能拷貝。但是如果你拷貝的是對象數組,那麼你只拷貝了它們的reference--對象本身不會被拷貝。這被成爲淺拷貝(shallow copy)。
數組的比較
爲了能比較數組是否完全相等,Arrays提供了經重載的equals()方法。當然,也是針對各種primitive以及Object的。兩個數組要想完全相等,他們必須有相同數量的元素,而且數組的每個元素必須與另一個數組的相對應的位置上的元素相等。元素的相等姓,用equals()判斷。(對於 primitive,它會使用其wrapper類的equals();比如int使用Integer.equals()。)。
數組元素的比較
Java裏面有兩種能讓你實現比較功能的方法。一是實現java.lang.Comparable接口,並以此實現類“自有的”比較方法。這是一個很簡單的接口,它只有一個方法compareTo()。這個方法能接受另一個對象作爲參數,如果現有對象比參數小,它就會返回一個負數,如果相同則返回零,如果現有的對象比參數大,它就返回一個正數。
static randInt()方法會生成一個介於0到100之間的正數。
現在架設,有人給你一個沒有實現Comparable接口的類,或者這個類實現了Comparable接口,但是你發現它的工作方式不是你所希望的,於是要重新定義一個新的比較方法。Java沒有強求你一定要把比較代碼塞進類裏,它的解決方案是使用“策略模式(strategy design pattern)”。有了策略之後,你就能把會變的代碼封裝到它自己的類裏(即所謂的策略對象strategy object)。你把策略對象交給不會變的代碼,然後用它運用策略完成整個算法。這樣,你就可以用不同的策略對象來表示不同的比較方法,然後把它們都交給同一個排序程序了。接下來就要“通過實現Comparator接口”來定義策略對象了。這個接口有兩個方法compare()和equals()。但是除非是有特殊的性能要求,否則你用不着去實現equals()。因爲只要是類,它就都隱含地繼承自Object,而Object裏面已經有了一個
equals()了。所以你儘可以使用缺省的Object的equals(),這樣就已經滿足接口的要求了。
Collections類裏專門有一個會返回與對象自有的比較法相反的Comparator的方法。它能很輕易地被用到CompType上面。
Collections.reverseOrder()返回了一個Comparator的reference。
compare()方法會根據第一個參數是小於,等於還是大於第二個參數,分別返回負整數,零或是正整數。
數組的排序
有了內置的排序方法之後,你就能對任何數組排序了,不論是primitive的還是對象數組的,只要它實現了Comparable接口或有一個與之相關的Comparator對象就行了。
Java標準類庫所用的排序算法已經作了優化--對primitive,它用的是“快速排序(Quicksort)”,對對象,它用的是“穩定合併排序(stable merge sort)”。所以除非是prolier表明排序算法是瓶頸,否則你不用爲性能擔心。
查詢有序數組
一旦數組排完序,你就能用Arrays.binarySearch()進行快速查詢了。但是切忌對一個尚未排序的數組使用binarySearch();因爲這麼做的結果是沒意義的。
如果Arrays.binarySearch()找到了,它就返回一個大於或等於0的值。否則它就返回一個負值,而這個負值要表達的意思是,如果你手動維護這個數組的話,這個值應該插在哪個爲止。這個值就是:
-(插入點)-1
“插入點”就是,在所有“比要找的那個值”更大值中,最小的那個值的下標,或者,如果數組中所有的值都比要找的值小,它就是a.size()。
如果數組裏面有重複元素,那它不能保證會返回哪一個。這個算法不支持重複元素,不過它也不報錯。所以,如果你需要的是一個無重複元素的有序序列的話,那麼可以考慮使用本章後面所介紹的TreeSet(支持【排序順序“sorted order”】)和LinkedHashSet(支持【插入順序“sorted order”】)。這兩個類會幫你照看所有細節。只有在遇到性能瓶頸的時候,你才應該用手動維護的數組來代替這兩個類。
如果排序的時候用到了Comparator(針對對象數組,primitive數組不允許使用Comparator),那麼binarySearch()的時候,也必須使用同一個Comparator(用這個方法的重載版)。
數組部分的總結
總而言之,如果你要持有一組對象,首選,同時也是效率最高的選擇,應該是數組。而且,如果這是一組primitive的話,你也只能用數組。還有一些更爲一般的情況,也就是寫程序的時候還不知道要用多少對象,或者要用一種更復雜方式來存儲對象情況。爲此,Java提供了“容器類(container class)”。其基本類型有List,Set和Map。
它們還有一些別的特性。比方說Set所持有的對象,個個都不同,Map則是一個“關聯性數組(associative array)”,它能在兩個對象之間建立聯繫。此外,與數組不同,它們還能自動調整大小,所以你可以往裏面放任意數量的對象。
容器簡介
Java2的重新設計了1.0和1.1裏面那個表現差勁的容器類。新的設計更緊湊也更合理。同時它也補齊了容器類庫的功能,提供了鏈表(linked list),隊列(queue)和雙向隊列(deques,讀成“decks”)這幾種數據結構的功能。
Java2的容器類要解決“怎樣持有對象”,而它把這個問題分成兩類:
1。Collection:通常是一組有一定規律的獨立元素。List必須按照特定的順序持有這些元素,而Set則不能保存重複的元素。(bag沒有這個限制,但是Java的容器類庫沒有實現它,因爲List已經提供這種功能了)
2。Map:一組以“鍵--值”(key-value)形式出現的pair。初看上去,它應該是一個pair的Collection,但是真這麼去做的話,它就會變得很滑稽,所以還是把這個概念獨立列出來爲好。退一步說,真的要用到Map的某個自己的時候,創建一個Collection也是很方便的。 Map可以返回“鍵(key)的”Set,值的Collection,或者pair的Set。和數組一樣,Map不需要什麼修改,就能很容易地擴展成多維。你只要直接把Map的值設成Map就可以了(然後它的值再是Map,依此類推)。
Java的容器類分成兩種基本類型。它們的區別就在,每個位置能放多少對象。Collection只允許每個位置上放一個對象(這個名字有點誤導,因爲容器類庫也常被統稱爲collections)。它包括“以一定順序持有一組對象”的List,以及“只能允許添加不重複的對象”的Set。 ArrayList是一種List,而HashSet則是一種Set。你可以用add()方法往Collection裏面加對象。
Map保存的是“鍵(key)--值”形式的pair,很像是一個微型數據庫。
Map又被稱爲關聯性數組(associative array)。你可以用put()方法往Map裏面加元素。它接受鍵--值形式pair作參數。
fill()方法還爲Collection和Map作了重載。輸出在默認情況下使用容器類的toString()方法。打印出來的Collection會用方括號括起來,元素與元素之間用逗號分開。Map會用花括號括起來,鍵和值之間用等號聯起來(鍵在左邊,值在右邊)。
List會老老實實地持有你所輸入的所有對象,既不做排序也不做編輯。Set則每個對象只接受一次,而且還要用它自己的規則對元素進行重新排序(一般情況下,你關心的只是Set包沒包括某個對象,而不是它到底排在哪裏--如果是那樣,你最好還是用List)。而Map也不接收重複的pair,至於是不是重複,要由key來決定。此外,它也有它自己的內部排序規則,不會受輸入順序影響。如果插入順序是很重要的,那你就只能使用LinkedHashSet或 LinkedHashMap了。
填充容器
和Arrays一樣,Collection也有一個叫Collections的輔助類,它包含了一些靜態的實用工具方法,其中就有一個fill()。這個 fill()也只是把同一個對象的reference複製到整個容器,而且它還只能爲List,不能爲Set和Map工作。
容器的缺點:不知道對象的類型
Java的容器有個缺點,就是往容器裏面放對象的時候,會把對象的類型信息給弄丟了。這是因爲開發容器類的程序員不會知道你要用它來保存什麼類型的對象,而讓容器僅只保存特定類型的對象又會影響它的通用性。所以容器被做成只有持有Object,也就是所有對象的根類的reference,這樣它就能持有任何類型的對象了。(當然不包括primitive,因爲它們不是對象,也沒有繼承別的對象。)這是一個很了不起的方案,只是:
1,由於在將對象放入容器的時候,它的類型信息被扔掉了,所以容器對“能往裏面加什麼類型的對象”沒有限制。比方說,即使你想讓它只持有cat,別人也能很輕易地把dog放進去。
2,由於對象的類型信息沒了,容器只知道它持有的Object的reference,所以對象在使用之前還必須進行類型轉換。
好的一面是,Java不會讓你誤用放進容器裏的對象。假設你往cat的容器裏面扔了個dog,然後要把這個容器裏的所有對象都當cat來用,當你把dog 的reference從cat的容器裏面拉出來,並且試圖將它轉換成cat的時候,就會引發一個RuntimeException。
ArrayList的用法也是很簡單:先創建一個,用add()把對象放進去,要用的時候再給get()傳一個下標--就跟用數組差不多,只是不需要用方括號了。ArrayList也有一個size()方法,它會告訴你容器裏面有多少對象,這樣你就不會粗心大意地過了界然後引發異常了。
有時即使不正確它也能運行
有時,即使不把對象轉換成原先的類型,它好像也能正常工作。有一種情況比較特殊:String能從編譯器哪裏得到一些能使之平穩工作的特殊幫助。只要編譯器沒能得到它所期望的String對象,它就會調用toString()。這個方法油Object定義,能被任何Java類覆寫。它所返回的String 對象,會被用到任何要用它的地方。
於是只要覆寫了類的toString()方法,你就能打印對象了。
做一個類型敏感的ArrayList
參數化類型(Parameterized types)
迭代器
無論是哪種容器,你都得有辦法既能放東西進去,也能拿東西出來。畢竟,容器的主要任務就是存放對象。ArrayList的add()就是用來放東西的,而 get()則是把對象拿出來的辦法。ArrayList恨靈活;你可以隨時提取任何東西,並且換一個下標,馬上就能選擇另一個元素。
“迭代器(iterator 又是一個設計模式)”是一個對象,它的任務是,能在讓“客戶程序在不知道,或者不關心他所處理的是什麼樣的底層序列結構”的情況下,就能在一個對象序列中前後移動,並選取其中的對象。此外迭代器還是一種通常所說的“輕量級”的對象,既創建代價很小的對象。
Java的Iterator就屬於有這種限制的迭代器。它做不了很多事情,除了:
1,用iterator()方法叫容器傳給你一個Iterator對象。第一次調用Iterator的next()方法的時候,它就會傳給你序列中的第一個元素。
2,用next()方法獲取序列中的下一個對象。
3,用hasNext()方法查詢序列中是否還有其他對象。
4,用remove()方法刪除迭代器所返回的最後一個元素。
就這麼多了。這只是迭代器的一個恨簡單的實現,不過還是很強大(對List來說,還有一個更精巧的ListIterator)。
不經意的遞歸(Unintended recursion)
由於Java的標準容器類(同其它類一樣)也是繼承Object的,因此它們也有一個toString()方法。這個方法已經被覆寫了,所以它能生成一個表示它自己以及所有它所保存的對象的String。比如ArrayList的toString()方法就會遍歷ArrayList的每個元素,然後調用它們的toString()方法。假設你要打印類的地址。好像最直接的辦法就是使用this。但是會出現很多異常,解決的辦法就是去調用Object的 toString()方法,它就是幹這活的。所以不要用this,應該寫super.toString()。
容器分類學
根據編程的需要,Collection和Map分別有好幾個實現。實際上只有三種容器組件--Map,List和Set,而每種又有兩到三個實現。
與存放對象有關的接口包括Collection, List, Set和Map。在理想情況下,絕大多數代碼應該只同這些接口打交道,只是在創建容器的時候纔要精確地指明它的確切類型。
add(),就像它的名字告訴我們的,會把新的元素放進Collection。但是文檔裏面特別仔細地聲明,“add()會確保容器包含指定的元素”。這句話是說給Set的,因爲它只添加原先沒有的元素,對ArrayList或其他List,add()總是“把它放進去”,因爲List並不關心它是不是保存了相同的元素。
Collection都能用iterator()方法產生一個Iterator。這裏,我們用Iterator來遍歷整個Collection,然後把他們打印出來。
Collection的功能
下面這張表給出了Collection的所有功能,也就是你能用Set和List做什麼事(不包括從Object自動繼承過來的方法)。(List還有一些額外的功能。)Map不是繼承Collection的,所以我們會區別對待。
boolean add(Object):確保容器能持有你傳給它的那個參數。如果沒有把它加進去,就返回false。(這是個“可選”的方法,本章稍後會再作解釋。)
boolean addAll(Collection):加入參數Collection所含的所有元素。只要加了元素,就返回true。
void clear():清除容器所保存的所有元素。(“可選”)
boolean contains(Object):如果容器持有參數Object,就返回true。
boolean containsAll(Collection):如果容器持有參數Collection所含的全部元素,就返回true。
boolean isEmpty():如果容器裏面沒有保存任何元素,就返回true。
Iterator iterator():返回一個可以在容器的各元素之間移動的Iterator。
boolean removeAll(Collection):刪除容器裏面所有參數Collection所包含的元素。只要刪過東西,就返回true。(“可選”)
boolean retainAll(Collection):只保存參數Collection所包括的元素(集合論中“交集”的概念)。如果發生過變化,則返回true。(“可選”)
int size():返回容器所含元素的數量。
Object[] toArray():返回一個包含容器中所有元素的數組。
Object[] toArray(Object[] a):返回一個包含容器中所有元素的數組,且這個數組不是普通的Object數組,它的類型應該同參數數組a的類型相同(要做類型轉換)。
注意,這裏沒有能進行隨機訪問的get()方法。這是因爲Collection還包括Set。而Set有它自己的內部順序(因此隨即訪問事毫無意義的)。所以如果你要檢查Collection的元素,你就必須使用迭代器。
接下來講List, Set和Map的各種實現了,每講一種容器,我都會(用星號)告訴你默認情況下應該選用哪種實現。
List的功能
List的基本用法事相當將但的。雖然絕大多數時候,你只是用add()加對象,用get()取對象,用iterator()獲取這個序列的Iterator,但List還有一些別的很有用的方法。
實際上有兩種List:擅長對元素進行隨機訪問的,較常用的ArrayList,和更強大的LinkedList。LinkedList不是爲快速的隨機訪問而設計的,但是它卻有一組更加通用的方法。
Lisk(接口):List的最重要的特徵就是有序;它會確保以一定的順序保存元素。List在Collection的基礎上添加了大量方法,使之能在序列中間插入和刪除元素。(只對LinkedList推薦使用。)List可以製造ListIterator對象,你除了能用它在List的中間插入和刪除元素之外,還能用它沿兩個方法遍歷List。
ArrayList*:一個用數組實現的List。能進行快速的隨機訪問,但是往列表中間插入和刪除元素的時候比較慢。ListIterator只能用在反向遍歷ArrayList的場合,不要用它來插入和刪除元素,因爲相比LinkedList,在ArrayList裏面用ListIterator的系統開銷比較高。
LinkedList:對順序訪問進行了優化。在List中間插入和刪除元素的代價也不高。隨機訪問的速度相對較慢。(用ArrayList吧。)此外它還有addFirst(),addLast(),getFirst(),getLast(),removeFirst()和removeLast()等方法(這些方法,接口和基類均未定義),你能把它當成棧(stack),隊列(queue)或雙向隊列(deque)來用。
記住,容器只是一個存儲對象的盒子。如果這個笑盒子能幫你解決所有的問題,那你就用不着取管它事怎麼實現的(在絕大多數情況下,這是使用對象的基本概念)。如果開發環境裏面還有一些別的,會造成固定的性能開銷的因素存在,那麼ArrayList和LinkedList之間的性能差別就會變得不那麼重要了。你只需要它們中的一個,你甚至可以想象有這樣一種“完美”的抽象容器;它能根據用途,自動地切換其底層的實現。
用LinkedList做一個棧
“棧(stack)”有時也被稱爲“後進先出”(LIFO)的容器。就是說,最後一個被“壓”進棧中的東西,會第一個“彈”出來。同其他Java容器一樣,壓進去和彈出來的東西都是Object,所以除非你只用Object的功能,否則就必須對彈起來的東西進行類型轉換。
LinkedList的方法能直接實現棧的功能,所以你完全可以不寫Stack而直接使用LinkedList。
如果你只想要棧的功能,那麼繼承就不太合適了,因爲繼承出來的是一個擁有LinkedList的所有方法的類。
用LinkedList做一個隊列
隊列(queue)是一個“先進先出”(FIFO)容器。也就是,你把一端把東西放進去,從另一端把東西取出來。所以你放東西的順序也就是取東西的順序。LinkedList有支持隊列的功能的方法,所以它也能被當作Queue來用。
還能很輕易地用LinkedList做一個deque(雙向隊列)。它很像隊列,只是你可以從任意一端添加和刪除元素。
Set的功能
Set的接口就是Collection的,所以不像那兩個List,它沒有額外的功能。實際上Set確確實實就是一個Collection--只不過行爲方式不同罷了。(這是繼承和多態性的完美運用:表達不同地行爲。)Set會拒絕持有多個具有相同值的對象的實例(對象的“值”又是由什麼決定的呢?這個問題比較複雜,我們以後會講)。
Set(接口):加入Set的每個元素必須是唯一的;否則,Set是不會把它加進去的。要想加進Set,Object必須定義equals(),這樣才能標明對象的唯一性。Set的接口和Collection的一摸一樣。Set的接口不保證它會用哪種順序來存儲元素。
HashSet*:爲優化查詢速度而設計的Set。要放進HashSet裏面的Object還得定義hashCode()。
TreeSet:是一個有序的Set,其底層是一顆樹。這樣你就能從Set裏面提取一個有序序列了。
LinkedHashSet(JDK 1.4):一個在內部使用鏈表的Set,既有HashSet的查詢速度,又能保存元素被加進去的順序(插入順序)。用Iterator遍歷Set的時候,它是按插入順序進行訪問的。
HashSet保存對象的順序是和TreeSet和LinkedHashSet不一樣的。這是因爲它們是用不同的方法來存儲和查找元素的。(TreeSet用了一種叫紅黑樹的數據結構【red-black tree data structure】來爲元素排序,而HashSet則用了“專爲快速查找而設計”的散列函數。LinkedHashSet在內部用散列來提高查詢速度,但是它看上去像是用鏈表來保存元素的插入順序的。)你寫自己的類的時候,一定要記住,Set要有一個判斷以什麼順序來存儲元素的標準,也就是說你必須實現 Comparable接口,並且定義compareTo()方法。
SortedSet
SortedSet(只有TreeSet這一個實現可用)中的元素一定是有序的。這使得SortedSet接口多了一些方法:
Comparator comparator():返回Set鎖使用的Comparator對象,或者用null表示它使用Object自有的排序方法。
Object first():返回最小的元素。
Object last():返回最大的元素。
SortedSet subSet(fromElement, toElement):返回Set的子集,其中的元素從fromElement開始到toElement爲止(包括fromElement,不包括toElement)。
SortedSet headSet(toElement):返回Set的子集,其中的元素都應小於toElement。
SortedSet headSet(toElement):返回Set的子集,其中的元素都應大於fromElement。
注意,SortedSet意思是“根據對象的比較順序”,而不是“插入順序”進行排序。
Map的功能
ArrayList能讓你用數字在一愕嘎對象序列裏面進行選擇,所以從某種意義上講,它是將數字和對象關聯起來。但是,如果你想根據其他條件在一個對象序列裏面進行選擇的話,那又該怎麼做呢?棧就是一個例子。它的標準是“選取最後一個被壓入棧的對象”。我們常用的術語map,dictionary,或 associative array就是一種非常強大的,能在序列裏面進行挑選的工具。從概念上講,它看上去像是一個ArrayList,但它不用數字,而是用另一個對象來查找對象!這是一種至關重要的編程技巧。
這一概念在Java中表現爲Map。put(Object key, Object value)方法會往Map裏面加一個值,並且把這個值同鍵(你查找時所用的對象)聯繫起來。給出鍵之後,get(Object key)就會返回與之相關的值。你也可以用containsKey()和containsValue()測試Map是否包含有某個鍵或值。
Java標準類庫裏有好幾種Map:HashMap,TreeMap,LinkedHashMap,WeakHashMap,以及 IdentityHashMap。它們都實現了Map的基本接口,但是在行爲方式方面有着明顯的詫異。這些差異體現在,效率,持有和表示對象pair的順序,持有對象的時間長短,以及如何決定鍵的相等性。
性能時Map所要面對的一個大問題。如果你知道get()時怎麼工作的,你就會發覺(比方說)在ArrayList裏面找對象會是相當慢的。而這正是 HashMap的強項。它不是慢慢地一個個地找這個鍵,而是用了一種被稱爲hash code的特殊值來進行查找的。散列(hash)時一種算法,它會從目標對象當中提取一些信息,然後生成一個表示這個對象的“相對獨特”的int。 hashCode()是Object根類的方法,因此所有Java對象都能生成hash code。HashMap則利用對象的hashCode()來進行快速的查找。這樣性能就有了急劇的提高。
Map(接口):維持鍵--值的關係(既pairs),這樣就能用鍵來找值了。
HashMap*:基於hash表的實現。(用它來代替Hashtable。)提供時間恆定的插入與查詢。在構造函數種可以設置hash表的capacity和load factor。可以通過構造函數來調節其性能。
LinkedHashMap(JDK 1.4):很像HashMap,但是用Iterator進行遍歷的時候,它會按插入順序或最先使用的順序(least-recently-used (LRU)order)進行訪問。除了用Iterator外,其他情況下,只是比HashMap稍慢一點。用Iterator的情況下,由於是使用鏈表來保存內部順序,因此速度會更快。
TreeMap:基於紅黑樹數據結構的實現。當你查看鍵或pair時,會發現它們時按順序(根據Comparable或Comparator,我們過一會講)排列的。TreeMap的特點時,你鎖得到的時一個有序的Map。TreeMap是Map中唯一有subMap()方法的實現。這個方法能讓你獲取這個樹中的一部分。
WeakHashMap:一個weak key的Map,是爲某些特殊問題而設計的。它能讓Map釋放其所持有的對象。如果某個對象除了在Map當中充當鍵之外,在其他地方都沒有其reference的話,那它將被當作垃圾回收。
IdentityHashMap(JDK 1.4):一個用==,而不是equals()來比較鍵的hash map。不是爲我們平常使用而設計的,是用來解決特殊問題的。
散列是往Map裏存數據的常用算法。
SortedMap
SortedMap(只有TreeMap這一個實現)的鍵肯定是有序的,因此這個接口裏面就有一些附加功能的方法了。
Comparator comparator():返回Map所使用的comparator,如果是用Object內置的方法的話,則返回null。
Object firstKey():返回第一個鍵。
Object lastKey():返回最後一個鍵。
SortedMap subMap(fromKey, toKey):返回這個Map的一個子集,其鍵從fromKey開始到toKey爲止,包括前者,不包括後者。
SortedMap headMap(toKey):返回這個Map的一愕嘎子集,其鍵均小於toKey。
SortedMap tailMap(fromKey):返回這個Map的一個子集,其鍵均大於等於fromKey。
pair是按key的順序存儲的,由於TreeMap有順序的概念,因此“位置”是有意義的,所以你可以去獲取它的第一個和最後一個元素,以及它的子集。
LinkedHashMap
爲了提高速度,LinkedHashMap對所有東西都做了hash,而且遍歷的時候(println()會遍歷整個Map,所以你能看到這個過程)還會按插入順序返回pair。此外,你還可以在LinkedHashMap的構造函數裏面進行配置,讓它使用基於訪問的LRU(least-recently -used)算法,這樣還沒被訪問過的元素(同時也是要刪除的候選對象)就會出現在隊列的最前頭。這樣,爲節省資源而寫一個定時清理的程序就變得很簡單了。
散列算法與Hash數
一個合適的equals()必須做到一下五點:
1 反身性:對任何x, x.equals(x)必須是true的。
2 對稱性:對任何x和y,如果y.equals(x)是true的,那麼
x.equals(y)也必須是true的。
3 傳遞性:對任何x,y和z,如果x.equals(y)是true的,且
y.equals(z)也是true的,那麼x.equals(z)也必須是true的。
4 一致性:對任何x和y,如果對象裏面用來判斷相等姓的信息沒有修
改過,那麼無論調用多少次x.equals(y),它都必須一致地返回
true或false。
5 對於任何非空的x,x.equals(null)必須返回false。
默認的Object.equals()只是簡單地比較兩個對象的地址。
如果你想把子集寫的類當HashMap的鍵來用的話,你就必須把hashCode()和equals()都給覆寫了。
理解hashCode()
如果你不覆寫鍵的hashCode()和equals()的話,散列數據結構(HashSet,HashMap,LinkedHashSet,或LinkedHashMap)就沒法正確地處理鍵。
散列的價值就在於速度:散列算法能很快地找出東西。
數組是最快的數據結構。
持有reference
java.lang.ref類庫裏有一套能增進垃圾回收器工作的靈活性的類。一旦碰到了“對象達到要耗光內存”的時候,這些類就會顯得格外有用。有三個類是繼承抽象類Reference的:SoftReference,WeakReference和PhantomReference。如果待處理的對象只能通過這些Reference進行訪問的話,那麼這些Reference對象就會向垃圾回收器提供一些不同級別的暗示。
……
總結Java標準類庫的容器類:
1。數組把對象和數字形式的下標聯繫起來。它持有的是類型確定的對象,這樣提取對象的時候就不用再作類型傳遞了。它可以是多維的,也可以持有primitive。但是創建之後它的容量不能改了。
2。Collection持有單個元素,而Map持有相關聯的pair。
3。和數組一樣,List也把數字下標同對象聯繫起來,你可以把數組和List想成有序的容器。List會隨元素的增加自動調整容量。但是List只能持有Object reference,所以不能存放primitive,而且把Object提取出來之後,還要做類型傳遞。
4。如果要做很多隨機訪問,那麼請用ArrayList,但是如果要再List的中間做很多插入和刪除的話,就應該用LinkedList了。
5。LinkedList能提供隊列,雙向隊列和棧的功能。
6。Map提供的不是對象與數組的關聯,而是對象和對象的關聯。
HashMap看重的是訪問速度,而TreeMap各國那看重鍵的順序,因而它不如HashMap那麼快。而LinkedHashMap則保持對象插入的順序,但是也可以用LRU算法爲它重新排序。
7。Set只接受不重複的對象。HashSet提供了最快的查詢速度。而TreeSet則保持元素有序。LinkedHashSet保持元素的插入順序。
8。沒必要再在新代碼裏使用舊類庫留下來的Vector,Hashtable和Stack了。
容器類庫是你每天都會用到的工具,它能使程序更簡介,更強大並且更高效。