淺談如何為Java創(chuàng)建Pair類
Java中對于多個返回參數(shù)的選項是有限制的。一種方法只能返回一個對象,數(shù)組或原始函數(shù),和其他語言不同的是它不會提供一種簡易方式來消耗方法調(diào)用中的參數(shù)。實際上我們的選擇是返回一個對象數(shù)組,一個集合,僅為返回的參數(shù)創(chuàng)建一個類,或者最終將其發(fā)送到你打算替換的對象中。所有這些方法都存在缺陷:
使用對象數(shù)組
如果我們能夠幸運地獲取一套同類的返回參數(shù),那么對象的數(shù)組就會是一個帶有例外的選項,當我們打開這些對象時需要能分辨出每個參數(shù)。從另一方面來說,如果我們正在返回多個參數(shù)的不同類型,我們需要使用所有超類對象的數(shù)組--最有可能的就是對象本身。然后我們要拋出每一個打開的參數(shù)。我們已經(jīng)丟失了類型安全性且返回參數(shù)命令出錯的機會也可能增加。
使用集合
與使用數(shù)組的方法類似,我們或許也能創(chuàng)造一個集合來實現(xiàn)返回。在集合之上使用數(shù)組主要是因為創(chuàng)建集合所需要代碼的數(shù)量要比這段代碼短:
List< Object> retVal = new ArrayList< Object>(); |
事實上在數(shù)組之上使用集合沒有什么真正的優(yōu)勢,除非我們要使用映射以便通過名稱或其他要素來返回值。
首次創(chuàng)建Java時,其簡單程度是對日趨復(fù)雜的c++的一種顛覆。指針和內(nèi)存管理被簡化了,包括去除參數(shù)間接,常量,函數(shù)指針以及其他功能強大但容易混淆的性能。在c++中,我們可以用值或參照傳遞參數(shù),這樣可以對方法中的參照進行重新分配,并為你提供參數(shù)或返回更多值的方式。
使用JavaBeans
C++也支持的structs允許簡單結(jié)構(gòu)化的數(shù)據(jù)包。當然,Java類可以簡單完成雙倍于structs的任務(wù),但通常習慣以大量模板來擴大源碼。
使用類和JavaBeans慣例的時候還存在一個問題,即對象本質(zhì)上是易變的。這些對象有可能被方法的調(diào)用者和方法調(diào)用的類共享,這樣就會導(dǎo)致易變狀態(tài)的共享,而這中情況無益于多線程系統(tǒng)。
在Java中,你能用值傳遞的只剩下方法參數(shù)了,而且還不能是outparams,同時方法只能返回一個參數(shù)??匆幌氯我獯a庫就會發(fā)現(xiàn)大量的實例,不過卻不是十分有效。
改進Java Beans方式
那么應(yīng)該怎樣做呢?Java類選項事實上才是解決方案的關(guān)鍵以及對其方式進行改進。這些類可以成為structs更好的替代物。
讓我們?yōu)榉祷氐念惔_定兩個參數(shù):名稱和出生日期:
public class PersonNameDOB { |
然這是一個人為的例子,我們有機會擁有一個已經(jīng)定義的Person類。大家肯定也會有類似的例子,需要從方法中返回兩個不同的對象,但是卻沒有已經(jīng)為其定義的類,或者是返回的類夾帶多余的信息,或許是比這更糟的情況。例如,如果有人調(diào)用了你的方法來使用或修改返回對象中的值。
上述情況所需代碼更多。因此我們可以做一些簡單的修改:
public class PersonNameDOB { |
其結(jié)果要短一些且更適合這項任務(wù)。值已經(jīng)返回了,因此不需要setters了,我們只要在返回對象建成后創(chuàng)建值就可以了。它們不需要更改,由于它們位于構(gòu)造器中,因此具有決定性作用?,F(xiàn)在任務(wù)已經(jīng)完成,將類的屬性設(shè)為公開也沒有風險了。同理,可以處理getters了,其結(jié)果更短,更易于使用。
PersonNameDOB personNameDOB = SSNLookup.lookupBySSN("123-45-6789"); |
如果這些太顯而易見,請耐心看下去。我喜歡用這個方法來簡化對象的返回。這種類型是安全的,因此在返回后不需要將對象拋出數(shù)組。而最后屬性的修改意味著這些返回的對象不會被濫用--它們只是為了傳輸數(shù)據(jù)。
為了安全起見,建議你復(fù)制對象或使用不可改變的對象以應(yīng)付值的意外修改。在我們的例子中,字符串是不可改變的,但是日期可以復(fù)制:
public PersonNameDOB lookupBySSN(String ssn) { |
這可以阻止調(diào)用者做接下來的操作:
PersonNameDOB personNameDOB = SSNLookup.lookupBySSN("123-45-6789"); |
成對的需求
以上的模式是筆者經(jīng)常在Java應(yīng)用程序接口調(diào)用中用來替代structs的方法,但是如果我們只是想返回兩個類型對象,這些就還不夠??瓷先ナ峭偈挚傻玫臇|西其實仍然從JavaSE標準分配中神秘失蹤,而這就是被原始化的Pair類??纯次覀?nèi)绾螐纳鲜瞿J絹斫air類。
首先,值要比名稱和出生日期都普遍。最普遍的是在將域名定為first和second:
public class Pair { |
現(xiàn)在擁有了一個可以返回Strings和Dates對的通用類,但還不包括其他類型,將其擴展為通用類型:
public class Pair< A, B> { |
這樣就好多了。沒必要擔心代表了一對返回類型快捷方式的通配符。這個類現(xiàn)在可以作為通用類型對來使用,如:
public static Pair< String, Date> lookupBySSN(String ssn) { |
開始使用:
Pair< String, Date> personNameDOB = SSNLookup.lookupBySSN("123-45-6789"); |
Pair類還未完成。我們還要考慮類型是否真具有普遍性:
1. 我們不想其他人擴展或更改Pair類,因為這可能破壞類的最初意圖。
2. 新的new Pair()是可以的,但是看上去有些不雅,我們可以改進一下。
3. Pair對于返回值是很有效的,但是如果要作為映射中的關(guān)鍵要素就有些勉為其難。
4. 擁有用于調(diào)試或其他toString()用途的Pair字符串代表形式是非常好的。
5. 最后,允許Pair和包含的對象可被序列化,其內(nèi)容也被假定為可以序列化了。
因此,讓我們看看它是如何改變Pair執(zhí)行的:
public final class Pair< A,B> implements Serializable {
private static final long serialVersionUID = 1L; // shouldn't// need to change
public final A first;
public final B second;
private Pair (A first, B second) {
this.first = first;
this.second = second;
}
public static < A,B> Pair< A,B> of (A first, B second) {
return new Pair< A,B>(first,second);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Pair other = (Pair) obj;
if (this.first != other.first &&
(this.first == null || !this.first.equals(other.first))) {
return false;
}
if (this.second != other.second &&
(this.second == null || !this.second.equals(other.second))) {
return false;
}
return true;
}
@Override
public int hashCode() {
int hash = 7;
hash = 37 * hash + (this.first != null ?
this.first.hashCode() : 0);
hash = 37 * hash + (this.second != null ? this.second.hashCode() : 0);
return hash;
}
@Override
public String toString () {
return String.format("Pair[%s,%s]", first,second);
}
}
這樣就已經(jīng)將構(gòu)造器私有化,但是提供了一個看起來更適合的靜態(tài)of()方法:
return Pair.of(person.getName(), new Date(person.getDOB().getTime())); |
Pair類已經(jīng)完成,因此沒有人可以取代它或改變類的原始意圖。它看上去很嚴格,但卻可以用于廣泛的使用。如果有人想讓Pair以不同方式運作,就應(yīng)該寫出適合自己的執(zhí)行代碼,并對其進行求證。
equals()和hash()方法意味著這個類不止可以用來返回值。它們還可以成為映射的關(guān)鍵。此處對使用該模式返回對象給出的建議是讓IDE為我們創(chuàng)建equals和hashCode方法。IDE往往都有可插入的模板,其中包含已定義的最佳方法,因此我們只需讓NetBeans創(chuàng)建就可以了。
toString()給出了pair的合適形式,如 "Pair[FredJones,Sun Mar 22 12:55:44 PDT 2009]"。這對于調(diào)試彈出很有用。
這一個類現(xiàn)在執(zhí)行Serializable。相信此處我們的選擇令人懷疑,但是集合是可序列化的,Pair應(yīng)該也可以。如果Pair中的類不能序列化那么就如果集合中的類不能序列化一樣糟糕,而Pair不應(yīng)該阻止類中間的序列化。
完成了嗎?
既是又不是。這個類現(xiàn)在在Navigenics中使用。毫無疑問在其他Java應(yīng)用程序中也存在類似的執(zhí)行。那我們完成了沒有呢?從"最具預(yù)見性"的角度來講,我們完成了。從"任何時候都需要"的角度來說恐怕還沒完成。
例如,這類Pairs不能做比較。增加一個compareTo()方法使之可比,但是要注意應(yīng)對復(fù)雜的通用類設(shè)計。通常,比較第一個值很容易,如果它們相等,就要比較第二個值。這可能是最合適的行為,但是Pair的每次使用都正確嗎?我們要檢查一下作比較的類是否具有可比性,如果不具備,該怎么辦?
結(jié)論
為Java創(chuàng)建一個Pair類是有些難的,而且在簡單通用的類庫執(zhí)行中,類庫要求在少量功能前提下不改變Java語言。通用性是很麻煩的一件事。不過,本人相信本文描述的Pair類至少是SE庫中標準化Pair執(zhí)行的良好開端。但愿在Java 7中會增加這一項。盡管它對于其他語言中的Tuple來說是顯得有些貧乏,但是加入標準的Pair有望為我們帶來大量實用性。這種可讀性的改進以及模板代碼的刪除都旨在減少Coin項目的語言變化。
【編輯推薦】