第 1 部分
http://www.ibm.com/developerworks/cn/java/j-5things2.html
對於很多 Java 開發人員來說,Java Collections API 是標准 Java 數組及其所有缺點的一個非常需要的替代品。將 Collections 主要與 ArrayList
聯系到一起本身沒有錯,但是對於那些有探索精神的人來說,這只是 Collections 的冰山一角。
雖然 Map
(以及它的常用實現 HashMap
)非常適合名-值對或鍵-值對,但是沒有理由讓自己局限於這些熟悉的工具。可以使用適當的 API,甚至適當的 Collection 來修正很多易錯的代碼。
本文是 5 件事 系列 中的第二篇文章,也是專門討論 Collections 的 7 篇文章中的第一篇文章,之所以花這么大的篇幅討論 Collections,是因為這些集合在 Java 編程中是如此重要。首先我將討論做每件事的最快(但也許不是最常見)的方式,例如將 Array
中的內容轉移到List
。然后我們深入探討一些較少人知道的東西,例如編寫定制的 Collections 類和擴展 Java Collections API。
1. Collections 比數組好
剛接觸 Java 技術的開發人員可能不知道,Java 語言最初包括數組,是為了應對上世紀 90 年代初期 C++ 開發人員對於性能方面的批評。從那時到現在,我們已經走過一段很長的路,如今,與 Java Collections 庫相比,數組不再有性能優勢。
例如,若要將數組的內容轉儲到一個字符串,需要迭代整個數組,然后將內容連接成一個 String
;而 Collections 的實現都有一個可用的toString()
實現。
除少數情況外,好的做法是盡快將遇到的任何數組轉換成集合。於是問題來了,完成這種轉換的最容易的方式是什么?事實證明,Java Collections API 使這種轉換變得容易,如清單 1 所示:
清單 1. ArrayToList
import java.util.*; public class ArrayToList { public static void main(String[] args) { // This gives us nothing good System.out.println(args); // Convert args to a List of String List<String> argList = Arrays.asList(args); // Print them out System.out.println(argList); } }
注意,返回的 List
是不可修改的,所以如果嘗試向其中添加新元素將拋出一個 UnsupportedOperationException
。
而且,由於 Arrays.asList()
使用 varargs 參數表示添加到 List
的元素,所以還可以使用它輕松地用以 new
新建的對象創建 List
。
2. 迭代的效率較低
將一個集合(特別是由數組轉化而成的集合)的內容轉移到另一個集合,或者從一個較大對象集合中移除一個較小對象集合,這些事情並不鮮見。
您也許很想對集合進行迭代,然后添加元素或移除找到的元素,但是不要這樣做。
在此情況下,迭代有很大的缺點:
- 每次添加或移除元素后重新調整集合將非常低效。
- 每次在獲取鎖、執行操作和釋放鎖的過程中,都存在潛在的並發困境。
- 當添加或移除元素時,存取集合的其他線程會引起競爭條件。
可以通過使用 addAll
或 removeAll
,傳入包含要對其添加或移除元素的集合作為參數,來避免所有這些問題。
3. 用 for 循環遍歷任何 Iterable
Java 5 中加入 Java 語言的最大的便利功能之一,增強的 for 循環,消除了使用 Java 集合的最后一道障礙。
以前,開發人員必須手動獲得一個 Iterator
,使用 next()
獲得 Iterator
指向的對象,並通過 hasNext()
檢查是否還有更多可用對象。從 Java 5 開始,我們可以隨意使用 for 循環的變種,它可以在幕后處理上述所有工作。
實際上,這個增強適用於實現 Iterable
接口的任何對象,而不僅僅是 Collections
。
清單 2 顯示通過 Iterator
提供 Person
對象的孩子列表的一種方法。 這里不是提供內部 List
的一個引用 (這使 Person
外的調用者可以為家庭增加孩子 — 而大多數父母並不希望如此),Person
類型實現 Iterable
。這種方法還使得 for 循環可以遍歷所有孩子。
清單 2. 增強的 for 循環:顯示孩子
// Person.java import java.util.*; public class Person implements Iterable<Person> { public Person(String fn, String ln, int a, Person... kids) { this.firstName = fn; this.lastName = ln; this.age = a; for (Person child : kids) children.add(child); } public String getFirstName() { return this.firstName; } public String getLastName() { return this.lastName; } public int getAge() { return this.age; } public Iterator<Person> iterator() { return children.iterator(); } public void setFirstName(String value) { this.firstName = value; } public void setLastName(String value) { this.lastName = value; } public void setAge(int value) { this.age = value; } public String toString() { return "[Person: " + "firstName=" + firstName + " " + "lastName=" + lastName + " " + "age=" + age + "]"; } private String firstName; private String lastName; private int age; private List<Person> children = new ArrayList<Person>(); } // App.java public class App { public static void main(String[] args) { Person ted = new Person("Ted", "Neward", 39, new Person("Michael", "Neward", 16), new Person("Matthew", "Neward", 10)); // Iterate over the kids for (Person kid : ted) { System.out.println(kid.getFirstName()); } } }
在域建模的時候,使用 Iterable
有一些明顯的缺陷,因為通過 iterator()
方法只能那么 “隱晦” 地支持一個那樣的對象集合。但是,如果孩子集合比較明顯,Iterable
可以使針對域類型的編程更容易,更直觀。
4. 經典算法和定制算法
您是否曾想過以倒序遍歷一個 Collection
?對於這種情況,使用經典的 Java Collections 算法非常方便。
在上面的 清單 2 中,Person
的孩子是按照傳入的順序排列的;但是,現在要以相反的順序列出他們。雖然可以編寫另一個 for 循環,按相反順序將每個對象插入到一個新的 ArrayList
中,但是 3、4 次重復這樣做之后,就會覺得很麻煩。
在此情況下,清單 3 中的算法就有了用武之地:
清單 3. ReverseIterator
public class ReverseIterator { public static void main(String[] args) { Person ted = new Person("Ted", "Neward", 39, new Person("Michael", "Neward", 16), new Person("Matthew", "Neward", 10)); // Make a copy of the List List<Person> kids = new ArrayList<Person>(ted.getChildren()); // Reverse it Collections.reverse(kids); // Display it System.out.println(kids); } }
Collections
類有很多這樣的 “算法”,它們被實現為靜態方法,以 Collections
作為參數,提供獨立於實現的針對整個集合的行為。
而且,由於很棒的 API 設計,我們不必完全受限於 Collections
類中提供的算法 — 例如,我喜歡不直接修改(傳入的 Collection 的)內容的方法。所以,可以編寫定制算法是一件很棒的事情,例如清單 4 就是一個這樣的例子:
清單 4. ReverseIterator 使事情更簡單
class MyCollections { public static <T> List<T> reverse(List<T> src) { List<T> results = new ArrayList<T>(src); Collections.reverse(results); return results; } }
5. 擴展 Collections API
以上定制算法闡釋了關於 Java Collections API 的一個最終觀點:它總是適合加以擴展和修改,以滿足開發人員的特定目的。
例如,假設您需要 Person
類中的孩子總是按年齡排序。雖然可以編寫代碼一遍又一遍地對孩子排序(也許是使用 Collections.sort
方法),但是通過一個 Collection
類來自動排序要好得多。
實際上,您甚至可能不關心是否每次按固定的順序將對象插入到 Collection
中(這正是 List
的基本原理)。您可能只是想讓它們按一定的順序排列。
java.util
中沒有 Collection
類能滿足這些需求,但是編寫一個這樣的類很簡單。只需創建一個接口,用它描述 Collection
應該提供的抽象行為。對於 SortedCollection
,它的作用完全是行為方面的。
清單 5. SortedCollection
public interface SortedCollection<E> extends Collection<E> { public Comparator<E> getComparator(); public void setComparator(Comparator<E> comp); }
編寫這個新接口的實現簡直不值一提:
清單 6. ArraySortedCollection
import java.util.*; public class ArraySortedCollection<E> implements SortedCollection<E>, Iterable<E> { private Comparator<E> comparator; private ArrayList<E> list; public ArraySortedCollection(Comparator<E> c) { this.list = new ArrayList<E>(); this.comparator = c; } public ArraySortedCollection(Collection<? extends E> src, Comparator<E> c) { this.list = new ArrayList<E>(src); this.comparator = c; sortThis(); } public Comparator<E> getComparator() { return comparator; } public void setComparator(Comparator<E> cmp) { comparator = cmp; sortThis(); } public boolean add(E e) { boolean r = list.add(e); sortThis(); return r; } public boolean addAll(Collection<? extends E> ec) { boolean r = list.addAll(ec); sortThis(); return r; } public boolean remove(Object o) { boolean r = list.remove(o); sortThis(); return r; } public boolean removeAll(Collection<?> c) { boolean r = list.removeAll(c); sortThis(); return r; } public boolean retainAll(Collection<?> ec) { boolean r = list.retainAll(ec); sortThis(); return r; } public void clear() { list.clear(); } public boolean contains(Object o) { return list.contains(o); } public boolean containsAll(Collection <?> c) { return list.containsAll(c); } public boolean isEmpty() { return list.isEmpty(); } public Iterator<E> iterator() { return list.iterator(); } public int size() { return list.size(); } public Object[] toArray() { return list.toArray(); } public <T> T[] toArray(T[] a) { return list.toArray(a); } public boolean equals(Object o) { if (o == this) return true; if (o instanceof ArraySortedCollection) { ArraySortedCollection<E> rhs = (ArraySortedCollection<E>)o; return this.list.equals(rhs.list); } return false; } public int hashCode() { return list.hashCode(); } public String toString() { return list.toString(); } private void sortThis() { Collections.sort(list, comparator); } }
這個實現非常簡陋,編寫時並沒有考慮優化,顯然還需要進行重構。但關鍵是 Java Collections API 從來無意將與集合相關的任何東西定死。它總是需要擴展,同時也鼓勵擴展。
當然,有些擴展比較復雜,例如 java.util.concurrent
中引入的擴展。但是另一些則非常簡單,只需編寫一個定制算法,或者已有Collection
類的簡單的擴展。
擴展 Java Collections API 看上去很難,但是一旦開始着手,您會發現遠不如想象的那樣難。
結束語
和 Java Serialization 一樣,Java Collections API 還有很多角落等待有人去探索 —正因為如此,我們還不准備結束這個話題。在 5 件事 系列 的下一篇文章中,將可以看到用 Java Collections API 做更多事情的 5 種新的方式。
第 2 部分
http://www.ibm.com/developerworks/cn/java/j-5things3.html
java.util
中的 Collections 類旨在通過取代數組提高 Java 性能。如您在 第 1 部分 中了解到的,它們也是多變的,能夠以各種方式定制和擴展,幫助實現優質、簡潔的代碼。
Collections 非常強大,但是很多變:使用它們要小心,濫用它們會帶來風險。
1. List 不同於數組
Java 開發人員常常錯誤地認為 ArrayList
就是 Java 數組的替代品。Collections 由數組支持,在集合內隨機查找內容時性能較好。與數組一樣,集合使用整序數獲取特定項。但集合不是數組的簡單替代。
要明白數組與集合的區別需要弄清楚順序 和位置 的不同。例如,List
是一個接口,它保存各個項被放入集合中的順序,如清單 1 所示:
清單 1. 可變鍵值
import java.util.*; public class OrderAndPosition { public static <T> void dumpArray(T[] array) { System.out.println("============="); for (int i=0; i<array.length; i++) System.out.println("Position " + i + ": " + array[i]); } public static <T> void dumpList(List<T> list) { System.out.println("============="); for (int i=0; i<list.size(); i++) System.out.println("Ordinal " + i + ": " + list.get(i)); } public static void main(String[] args) { List<String> argList = new ArrayList<String>(Arrays.asList(args)); dumpArray(args); args[1] = null; dumpArray(args); dumpList(argList); argList.remove(1); dumpList(argList); } }
當第三個元素從上面的 List
中被移除時,其 “后面” 的各項會上升填補空位。很顯然,此集合行為與數組的行為不同(事實上,從數組中移除項與從 List
中移除它也不完全是一回事兒 — 從數組中 “移除” 項意味着要用新引用或 null 覆蓋其索引槽)。
2. 令人驚訝的 Iterator
!
無疑 Java 開發人員很喜愛 Java 集合 Iterator
,但是您最后一次使用 Iterator
接口是什么時候的事情了?可以這么說,大部分時間我們只是將 Iterator
隨意放到 for()
循環或加強 for()
循環中,然后就繼續其他操作了。
但是進行深入研究后,您會發現 Iterator
實際上有兩個十分有用的功能。
第一,Iterator
支持從源集合中安全地刪除對象,只需在 Iterator
上調用 remove()
即可。這樣做的好處是可以避免ConcurrentModifiedException
,這個異常顧名思意:當打開 Iterator
迭代集合時,同時又在對集合進行修改。有些集合不允許在迭代時刪除或添加元素,但是調用 Iterator
的 remove()
方法是個安全的做法。
第二,Iterator
支持派生的(並且可能是更強大的)兄弟成員。ListIterator
,只存在於 List
中,支持在迭代期間向 List
中添加或刪除元素,並且可以在 List
中雙向滾動。
雙向滾動特別有用,尤其是在無處不在的 “滑動結果集” 操作中,因為結果集中只能顯示從數據庫或其他集合中獲取的眾多結果中的 10 個。它還可以用於 “反向遍歷” 集合或列表,而無需每次都從前向后遍歷。插入 ListIterator
比使用向下計數整數參數 List.get()
“反向” 遍歷 List
容易得多。
3. 並非所有 Iterable
都來自集合
Ruby 和 Groovy 開發人員喜歡炫耀他們如何能迭代整個文本文件並通過一行代碼將其內容輸出到控制台。通常,他們會說在 Java 編程中完成同樣的操作需要很多行代碼:打開 FileReader
,然后打開 BufferedReader
,接着創建 while()
循環來調用 getLine()
,直到它返回 null。當然,在 try/catch/finally
塊中必須要完成這些操作,它要處理異常並在結束時關閉文件句柄。
這看起來像是一個沒有意義的學術上的爭論,但是它也有其自身的價值。
他們(包括相當一部分 Java 開發人員)不知道並不是所有 Iterable
都來自集合。Iterable
可以創建 Iterator
,該迭代器知道如何憑空制造下一個元素,而不是從預先存在的 Collection
中盲目地處理:
清單 2. 迭代文件
// FileUtils.java import java.io.*; import java.util.*; public class FileUtils { public static Iterable<String> readlines(String filename) throws IOException { final FileReader fr = new FileReader(filename); final BufferedReader br = new BufferedReader(fr); return new Iterable<String>() { public <code>Iterator</code><String> iterator() { return new <code>Iterator</code><String>() { public boolean hasNext() { return line != null; } public String next() { String retval = line; line = getLine(); return retval; } public void remove() { throw new UnsupportedOperationException(); } String getLine() { String line = null; try { line = br.readLine(); } catch (IOException ioEx) { line = null; } return line; } String line = getLine(); }; } }; } } //DumpApp.java import java.util.*; public class DumpApp { public static void main(String[] args) throws Exception { for (String line : FileUtils.readlines(args[0])) System.out.println(line); } }
此方法的優勢是不會在內存中保留整個內容,但是有一個警告就是,它不能 close()
底層文件句柄(每當 readLine()
返回 null 時就關閉文件句柄,可以修正這一問題,但是在 Iterator
沒有結束時不能解決這個問題)。
4. 注意可變的 hashCode()
Map
是很好的集合,為我們帶來了在其他語言(比如 Perl)中經常可見的好用的鍵/值對集合。JDK 以 HashMap
的形式為我們提供了方便的 Map
實現,它在內部使用哈希表實現了對鍵的對應值的快速查找。但是這里也有一個小問題:支持哈希碼的鍵依賴於可變字段的內容,這樣容易產生 bug,即使最耐心的 Java 開發人員也會被這些 bug 逼瘋。
假設清單 3 中的 Person
對象有一個常見的 hashCode()
(它使用 firstName
、lastName
和 age
字段 — 所有字段都不是 final 字段 — 計算hashCode()
),對 Map
的 get()
調用會失敗並返回 null
:
清單 3. 可變 hashCode()
容易出現 bug
// Person.java import java.util.*; public class Person implements Iterable<Person> { public Person(String fn, String ln, int a, Person... kids) { this.firstName = fn; this.lastName = ln; this.age = a; for (Person kid : kids) children.add(kid); } // ... public void setFirstName(String value) { this.firstName = value; } public void setLastName(String value) { this.lastName = value; } public void setAge(int value) { this.age = value; } public int hashCode() { return firstName.hashCode() & lastName.hashCode() & age; } // ... private String firstName; private String lastName; private int age; private List<Person> children = new ArrayList<Person>(); } // MissingHash.java import java.util.*; public class MissingHash { public static void main(String[] args) { Person p1 = new Person("Ted", "Neward", 39); Person p2 = new Person("Charlotte", "Neward", 38); System.out.println(p1.hashCode()); Map<Person, Person> map = new HashMap<Person, Person>(); map.put(p1, p2); p1.setLastName("Finkelstein"); System.out.println(p1.hashCode()); System.out.println(map.get(p1)); } }
很顯然,這種方法很糟糕,但是解決方法也很簡單:永遠不要將可變對象類型用作 HashMap
中的鍵。
5. equals()
與 Comparable
在瀏覽 Javadoc 時,Java 開發人員常常會遇到 SortedSet
類型(它在 JDK 中唯一的實現是 TreeSet
)。因為 SortedSet
是 java.util
包中唯一提供某種排序行為的 Collection
,所以開發人員通常直接使用它而不會仔細地研究它。清單 4 展示了:
清單 4. SortedSet
,我很高興找到了它!
import java.util.*; public class UsingSortedSet { public static void main(String[] args) { List<Person> persons = Arrays.asList( new Person("Ted", "Neward", 39), new Person("Ron", "Reynolds", 39), new Person("Charlotte", "Neward", 38), new Person("Matthew", "McCullough", 18) ); SortedSet ss = new TreeSet(new Comparator<Person>() { public int compare(Person lhs, Person rhs) { return lhs.getLastName().compareTo(rhs.getLastName()); } }); ss.addAll(perons); System.out.println(ss); } }
使用上述代碼一段時間后,可能會發現這個 Set
的核心特性之一:它不允許重復。該特性在 Set
Javadoc 中進行了介紹。Set
是不包含重復元素的集合。更准確地說,set 不包含成對的 e1 和 e2 元素,因此如果 e1.equals(e2),那么最多包含一個 null 元素。
但實際上似乎並非如此 — 盡管 清單 4 中沒有相等的 Person
對象(根據 Person
的 equals()
實現),但在輸出時只有三個對象出現在TreeSet
中。
與 set 的有狀態本質相反,TreeSet
要求對象直接實現 Comparable
或者在構造時傳入 Comparator
,它不使用 equals()
比較對象;它使用Comparator/Comparable
的 compare
或 compareTo
方法。
因此存儲在 Set
中的對象有兩種方式確定相等性:大家常用的 equals()
方法和 Comparable/Comparator
方法,采用哪種方法取決於上下文。
更糟的是,簡單的聲明兩者相等還不夠,因為以排序為目的的比較不同於以相等性為目的的比較:可以想象一下按姓排序時兩個 Person
相等,但是其內容卻並不相同。
一定要明白 equals()
和 Comparable.compareTo()
兩者之間的不同 — 實現 Set
時會返回 0。甚至在文檔中也要明確兩者的區別。
結束語
Java Collections 庫中有很多有用之物,如果您能加以利用,它們可以讓您的工作更輕松、更高效。但是發掘這些有用之物可能有點復雜,比如只要您不將可變對象類型作為鍵,您就可以用自己的方式使用 HashMap
。
至此我們挖掘了 Collections 的一些有用特性,但我們還沒有挖到金礦:Concurrent Collections,它在 Java 5 中引入。本 系列 的后 5 個竅門將關注 java.util.concurrent
。