你所不知道的五件事情--Java集合框架API(第二部分)
-- 小心可變性
這是Ted Neward在IBM developerWorks中5 things系列文章中的一篇,仍然講述了關(guān)于Java集合框架的一些應(yīng)用竅門,值得大家學(xué)習(xí)。(2010.05.08最后更新)-- 小心可變性
概要:你可以在任何地方使用Java集合框架,但不要想當(dāng)然地使用它們。集合框架有神秘之處,如果你不能正確地對待它,它就會為你惹麻煩。Ted Neward探索了Java集合框架API中復(fù)雜且可變的部分,還給出了一些幫助你更好地利用Iterable,HashMap和SortedSet的竅門,這些竅門將會使你的代碼不會產(chǎn)生Bug。
設(shè)計java.util包中集合框架類的目的就是幫助,也即替代數(shù)組,這也就提高了Java的能力。如你在上一篇文章中所學(xué)習(xí)到的,它們?nèi)跃呖伤苄?,它們希望以不同的途徑,好的方式,整潔的代碼去進行定制和擴展。
集合框架仍然強大,但它是可變的:要小心使用之,若濫用之則會使你陷入危機中。
1. List不同于數(shù)組
Java開發(fā)者經(jīng)常錯誤地猜想ArrayList只是Java數(shù)組的替代品。集合框架的背后就是數(shù)組,這就使得在集合對象中隨機地查找元素時能有好的性能。另外,如同數(shù)組那樣,集合對象使用整數(shù)序數(shù)去獲取特定元素。即便如此,集合對象仍不是數(shù)組的簡易替代品。
將集合對象與數(shù)組區(qū)分開來的技巧就是要知道順序與位置之間的區(qū)別。例如,List是一個接口,它為置入集合中的元素維護了順序,如清單1所示:
清單1. Mutable keys
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);
}
}
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);
}
}
當(dāng)刪除上面List中的第三個元素時,該元素"下面"的其它元素會向上移動以填補空位。很清楚,集合對象的行為不同于數(shù)組。(事實上,從數(shù)組中刪除一個元素與從List中刪除一個元素大為不同--從數(shù)組中"刪除"一個元素就是用一個新的引用變量或null去覆蓋該元素所處的位置。)
2. 迭代器,令我大為吃驚!
毫無疑問,Java開發(fā)者喜歡Java集合框架中的Iterator,但你最后一次看到Iterator接口是在什么時候呢?可以這么說,多數(shù)時候,我們只是將Iterator置入for循環(huán)或改進的for循環(huán)中。
但對于那些善于挖掘的人,Iterator內(nèi)藏兩大驚人之處:
第一,通過調(diào)用Iterator本身的remove()方法,Iterator擁有了從來源集合對象中安全地刪除元素的能力。此處的關(guān)鍵點在于避免了 ConcurrentModifiedException,顧名思意:當(dāng)?shù)髡诒闅v集合對象時,又正在修改該集合。一些集合對象不會讓你向正在被遍歷的集合中刪除或添加元素,但調(diào)用Iterator的remove()方法是一個安全的實踐方式。
第二,Iterator支持派生出的(且功能更強大的)兄弟。ListIterator,它只存在于List實例中,支持在遍歷過程中向List中添加和刪除元素,并且能雙向滾動(bidirectional scrolling)List對象。
雙向滾動(bidirectional scrolling)在某些場景下有特別強大的功能,例如無處不在的"結(jié)果集滑動",即,從數(shù)據(jù)庫或其它集合對象的眾多結(jié)果中展示其中的10個。它還可以被用于"向后遍歷"一個集合或列表,而不用試圖從前向后地訪問每個元素。使用ListIterator要比利用向下計數(shù)的整數(shù)參數(shù)的List.get() 方法去"向后遍歷"一個List容易得多。
3. 并不是所有的Iterable實例都來自于集合對象
Ruby和Groovy開發(fā)者喜歡炫耀他們怎樣使用一行代碼就遍歷了整篇文本,并將其中的內(nèi)容打印到控制臺上。多數(shù)時候,他們會說,使用Java來做同樣的事情需要編寫許多代碼:打開一個FileReader,再創(chuàng)建一個BufferedReader,然后創(chuàng)建一個while()循環(huán)去調(diào)用 getLine()方法,直到返回null為止。當(dāng)然,你還必須得在一個try/catch/finally語句塊中做上述事情,這個語句塊用于處理異常且在結(jié)束時關(guān)閉文件句柄。
看起來這像是一個微不足道,學(xué)究式的爭論,但它還是有些意義的。
他們(包括一些Java開發(fā)者)不知道并不是所有Iterable實例都要來自于集合對象。相反地,一個Iterable實例可以創(chuàng)建一個 Iterator實例,這個Iterator知道如何去憑空地造出下一個元素,而不是在一個預(yù)先已存在集合對象的內(nèi)部默默地進行處理。
清單2 Iterating a file
// 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);
}
}
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);
}
}
該方法的優(yōu)點在于不需要在內(nèi)存中處理整個文件的內(nèi)容,但有一個告誡,如上面所編寫的代碼,它不能關(guān)閉下層的文件句柄。(當(dāng)readLing()方法返回 null時就關(guān)閉文件句柄,通過該方法可以修正這一問題,但當(dāng)Iterator未能遍歷完整個文件時,該方法也解決不了這個問題。)
4. 意識到可變的hashCode()方法
Map是很好的集合對象,它帶給我們只有在其它編程語言,如Perl,中才能體會到的鍵-值對集合的樂趣。并且JDK為我們提供了一個很棒的Map實現(xiàn),HashMap,該實現(xiàn)在內(nèi)部使用散列表,這使得快速地通過鍵來查找對應(yīng)的值。但在那兒就會出現(xiàn)一個細(xì)微的問題:支持散列碼的鍵會依賴內(nèi)容可變的字段,這很容易就產(chǎn)生Bug。即使對那些最有耐心的Java開發(fā)者,這樣的Bug也會使他們發(fā)瘋。
想像清單3中的Person對象,它有一個典型的hashCode()方法(該方法使用firstName,lastName和age字段--所有的字段都不是final的--去計算散列碼),調(diào)用Map的get()方法將可能失敗并返回null。
清單3 可變的hashCode()使人犯錯
// 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));
}
}
import java.util.*;
public class Person
implements Iterable<Person>
{
public Person(String fn, String ln, int a, Person

{
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的鍵永遠(yuǎn)不要使用可變對象。
5. equals() vs Comparable
瀏覽Javadoc時,Java開發(fā)者們常會遇到SortedSet類型(在JDK中,它的唯一實現(xiàn)是TreeSet)。因為SortedSet是 java.util包中唯一提供了某種指定排序行為的集合類,所以開發(fā)者們在一開始使用它時并沒有仔細(xì)地考究其中的細(xì)節(jié)。清單4證明了這一點:
清單4 SortedSet,很高興發(fā)現(xiàn)你
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);
}
}
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);
}
}
在用了上述代碼一段時間之后,你可能會發(fā)現(xiàn)Set的核心特性之一:它不允許重復(fù)。這一特性在Set的Javadoc中有明確的描述。Set是"不包含重復(fù)元素的集合"。更準(zhǔn)確地說,對于元素e1和e2,如果有e1.eqauls(e2),那么Set就不能同時包含它們,并且最多只能包含一個null元素。
但這似乎不是實際情況--雖然清單4沒有Person對象是相等的(根據(jù)Person所實現(xiàn)的equals()方法),但當(dāng)打印該TreeSet時,只展示了三個Person對象。
與Set的天然狀態(tài)相反,TreeSet要求對象要么實現(xiàn)Comparable接口,要么向構(gòu)造器中直接傳入一個Comparator實現(xiàn),不用 equals()方法相比較對象;而是使用Comparator/Comparable中的compare/comparaTo方法。
存儲在Set中的對象有兩種潛在的方法來判定相等性:期望中的equals()方法;Comparable/Comparator方法,這依賴于調(diào)用這些方法的上下文。
更糟的是,如此簡單的描述還不足以表明這二者是不同的,因為以排序為目的的比較不同于以等價性為目的的比較:當(dāng)按姓氏進行排序時,某兩個Person對象是相等的,但它們的內(nèi)容卻是不等的。
總是要明確equals()與Comparable.compareTo()方法的區(qū)別--當(dāng)實現(xiàn)Set時,返回零必須是清晰的。甚至于,應(yīng)該在你的文檔中清晰地描述這一區(qū)別。
結(jié)論
Java集合框架遍布有用之物,只要知道它們,就能使你的生活更簡單也更富有成效。然而,挖掘出的這些有用之物經(jīng)常伴隨著一定的復(fù)雜度,例如,你會發(fā)現(xiàn)只要不在鍵中使用可變對象,就可以按你自己的方式去使用HashMap。
到目前為止,我們已經(jīng)對集合框架進行了深入挖掘,但我們還未觸及這其中的"金礦":由Java 5引入的并發(fā)集合。本系列的后5個竅門將關(guān)注包java.util.concurrent。
請關(guān)注你所不知道的五件事情--Java集合框架API(第一部分)