標籤:
注意可變對象
java.util 中的 Collections 類旨在通過取代數組提高 Java 效能。如您在 第 1 部分 中瞭解到的,它們也是多變的,能夠以各種方 式定製和擴充,協助實現優質、簡潔的代碼。
Collections 非常強大,但是很多變:使用它們要小心,濫用它們會帶來風險。
1. List 不同於數組
Java 開發人員常常錯誤地認為 ArrayList 就是 Java 數組的替代品。Collections 由數組支援,在集合內隨機尋找內容時效能較好。 與數組一樣,集合使用整序數擷取特定項。但集合不是數組的簡單替代。
要明白數組與集合的區別需要弄清楚順序 和位置 的不同。例如,List 是一個介面,它儲存各個項被放入集合中的順序,如清單 1 所 示:
清單 1. 可變索引值
import java.util.*;public class OrderAndPosition{ public static void dumpArray(T[] array) { System.out.println("============="); for (int i=0; i System.out.println("Position " + i + ": " + array[i]); } public static void dumpList(List list) { System.out.println("============="); for (int i=0; i System.out.println("Ordinal " + i + ": " + list.get(i)); } public static void main(String[] args) { List argList = new ArrayList(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.javaimport java.io.*;import java.util.*;public class FileUtils{ public static Iterable readlines(String filename) throws IOException { final FileReader fr = new FileReader(filename); final BufferedReader br = new BufferedReader(fr); return new Iterable() { public Iterator iterator() { return new Iterator() { 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.javaimport 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.javaimport java.util.*;public class Person implements Iterable{ 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 children = new ArrayList();}
// MissingHash.javaimport 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 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() { 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。
關於Java Collections API您不知道的5件事,第2部分