Java? 取消了操作符重載,但是新興的 Groovy 又使之浮出水面。在實戰 Groovy 定期連載的“Groovy 每日應用”的最後一期中,請隨著 Andrew Glover 介紹的三類可重載操作符,重新尋回自己多年來失去的東西。
許多以前使用 C++ 的開發人員會懷念操作符重載,例如 + 和 -。雖然它們很方便,但是被覆蓋的操作符的多態實質會造成混淆,所以操作符重載在 Java 語言中被取消了。這個限制的好處是清晰:Java 開發人員不必猜想兩個對象上的 + 是把它們加在一起還是把一個對象附加到另一個對象上。不好的地方則是喪失了一個有價值的簡寫形式。
把任何一個工具用於開發實踐的關鍵是,知道什麼時候使用它而什麼時候把它留在工具箱中。腳本語言(或動態語言)可以是工具箱中極為強大的工具,但是只有在恰當地應用到適當的場景中才是這樣。為了這個目標,實戰 Groovy 是一系列文章,專門介紹 Groovy 的實際應用,並教給您什麼時候、如何成功地應用它們。
現在,期望放任自由的 Groovy 把這個簡寫形式帶回來!在 實戰 Groovy 的這一期中,我將介紹 Groovy 對操作符即時多態(也稱為操作符重載)的支持。正如 C++ 開發人員會告訴您的,這個東西既方便又有趣,雖然必須小心謹慎才能接近。
三類可重載操作符
我把 Groovy 的可重載操作符分成三個邏輯組: 比較操作符、算術類操作符和數組類操作符。這幾組只涵蓋了普通 Java 編程中可用操作符的一個子集。例如,像 & 和 ^ 這樣的邏輯操作符目前在 Groovy 中不可用。
表 1 顯示了 Groovy 中可用的三組可重載操作符:
表 1. Groovy 的可重載操作符
1. 比較操作符對應著普通的 Java equals 和 compareTo 實現
2. Java 的算術類操作符,例如 +、- 和 *
3. 數組存取類操作符 []
比較操作符
比較操作符對應著 Java 語言中的 equals 和 compareTo 實現,通常用作集合中排序的快捷方式。表 2 顯示了 Groovy 的兩個比較操作符:
表 2. 比較操作符
操作符 方法
a == b a.equals(b)
a <=> b a.compareTo(b)
操作符 == 是 Java 語言中表示對象相等的簡寫形式,但是不代表對象引用的相等。換句話說,放在兩個對象之間的 Groovy 的 == 意味著它們相同,因為它們的屬性是相等的,雖然每個對象指向獨立的引用。
根據 Javadoc,Java 語言的 compareTo() 方法返回負整數、0 或正整數,代表對象小於、等於或大於指定對象。因為這個方法能夠返回三個值,所以 Groovy 用四個附加值擴充了 <=> 語法,如表 3 所示:
表 3. 四個附加的值
操作符 含義
a > b 如果 a.compareTo(b) 返回的值大於 0,那麼這個條件為 true
a >= b 如果 a.compareTo(b) 返回的值大於等於 0,那麼這個條件為 true
a < b 如果 a.compareTo(b) 小於 0,那麼這個條件為 true
a <= b 如果 a.compareTo(b) 小於等於 0,那麼這個條件為 true
比較購物
還記得我最初在“感受 Groovy”一文中定義的磁盤友好的 LavaLamp 類麼,它還在“實戰 Groovy: Groovy 的騰飛”一文中充當遷移到新的 JSR 語法的示例,我要再次使用這個類來演示使用比較操作符的一些漂亮的技巧。
在清單 1 中,我通過實現普通 Java 的 equals() 方法(以及它的可惡伙伴 hashCode)增強了 LavaLamp 類。另外,我讓 LavaLamp 實現了 Java 語言的 Comparable 接口並創建了 compareTo() 方法的實現:
清單 1. LavaLamp 歸來!
package com.vanward.groovy
import org.apache.commons.lang.builder.CompareToBuilder
import org.apache.commons.lang.builder.EqualsBuilder
import org.apache.commons.lang.builder.HashCodeBuilder
import org.apache.commons.lang.builder.ToStringBuilder
class LavaLamp implements Comparable{
@Property model
@Property baseColor
@Property liquidColor
@Property lavaColor
def String toString() {
return new ToStringBuilder(this).
append(this.model).
append(this.baseColor).
append(this.liquidColor).
append(this.lavaColor).
toString()
}
def boolean equals(obj) {
if (!(obj instanceof LavaLamp)) {
return false
}
LavaLamp rhs = (LavaLamp) obj
return new EqualsBuilder().
append(this.model, rhs.model).
append(this.baseColor, rhs.baseColor).
append(this.liquidColor, rhs.liquidColor).
append(this.lavaColor, rhs.lavaColor).
isEquals()
}
def int hashCode() {
return new HashCodeBuilder(17, 37).
append(this.model).
append(this.baseColor).
append(this.liquidColor).
append(this.lavaColor).
toHashCode()
}
def int compareTo(obj) {
LavaLamp lmp = (LavaLamp)obj
return new CompareToBuilder().
append(lmp.model, this.model).
append(lmp.lavaColor, this.lavaColor).
append(lmp.baseColor, this.baseColor).
append(lmp.liquidColor, this.liquidColor).
toComparison()
}
}
注:因為我是重用狂,所以我的這些實現嚴重依賴 Jakarta 的 Commons Lang 項目(甚至 toString() 方法也是如此!)如果您還在自己編寫 equals() 方法,那麼您可能想花一分鐘來簽出這個庫。(請參閱 參考資料。)
在清單 2 中,可以看到我在清單 1 中設置的操作符重載的效果。我創建了五個 LavaLamp 實例(沒錯,我們在開 party!)並用 Groovy 的比較操作符來區分它們:
清單 2. 比較操作符的效果
lamp1 = new LavaLamp(model:"1341", baseColor:"Black",
liquidColor:"Clear", lavaColor:"Red")
lamp2 = new LavaLamp(model:"1341", baseColor:"Blue",
liquidColor:"Clear", lavaColor:"Red")
lamp3 = new LavaLamp(model:"1341", baseColor:"Black",
liquidColor:"Clear", lavaColor:"Blue")
lamp4 = new LavaLamp(model:"1342", baseColor:"Blue",
liquidColor:"Clear", lavaColor:"DarkGreen")
lamp5 = new LavaLamp(model:"1342", baseColor:"Blue",
liquidColor:"Clear", lavaColor:"DarkGreen")
println lamp1 <=> lamp2 // 1
println lamp1 <=> lamp3 // -1
println lamp1 < lamp3 // true
println lamp4 <=> lamp5 // 0
assert lamp4 == lamp5
assert lamp3 != lamp4
注意 lamp4 和 lamp5 是如何相同的,以及其他對象之間具有什麼樣的細微差異。因為 lamp1 的 baseColor 是 Black 而lamp2 的 baseColor 是 Blue,所以 <=> 返回 1(black 中的 a 比 blue 中的 u 靠前)。類似地,lamp3 的 lavaColor 是 Blue,而 lamp1 的是 Red。因為條件 lamp1 <=> lamp3 返回 -1,所以語句 lamp1 < lamp3 返回 true。llamp4 和 llamp5 是相等的,所以 <=> 返回 0。
而且,可以看到 == 也適用於對象相等。lamp4 和 lamp5 是同一對象。當然,我應當用 assert lamp4.equals(lamp5) 證實這一點,但是 == 更快捷!
我想要我的引用相等
現在,如果真的想測試對象引用相等該怎麼辦?顯然,我不能用 ==,但是 Groovy 為這類事情提供了 is() 方法,如清單 3 所示:
清單 3. is() 的作用!
lamp6 = new LavaLamp(model:"1344", baseColor:"Black",
liquidColor:"Clear", lavaColor:"Purple")
lamp7 = lamp6
assert lamp7.is(lamp6)
在清單 3 中可以看出,lamp6 和 lamp7 是相同的引用,所以 is 返回 true。
順便說一句,如果感覺迷糊,不要驚訝:使用重載操作符差不多讓 Groovy 語言退步了。但是我認為是有趣的。
算術類操作符
Groovy 支持以下算術類操作符的重載:
表 3. Groovy 的算術類操作符
操作符 方法
a + b a.plus(b)
a - b a.minus(b)
a * b a.multiply(b)
a / b a.divide(b)
a++ or ++a a.next()
a-- or --a a.previous()
a << b a.leftShift(b)
您可能已經注意到 Groovy 中的 + 操作符已經在幾個不同的領域重載了,特別是在用於集合的時候。您是否想過,這怎麼可能?或者至少想過對自己的類能否這麼做?現在我們來看答案。
添加操作符
還記得“在 Java 應用程序中加一些 Groovy 進來”一文中的 Song 類麼?我們來看看當我創建一個 JukeBox 對象來播放 Song 時,發生了什麼。在清單 4 中,我將忽略實際播放 MP3 的細節,把重點放在從 JukeBox 添加和減去 Song 上:
清單 4. Jukebox
package com.vanward.groovy
import com.vanward.groovy.Song
class JukeBox {
def songs
JukeBox(){
songs = []
}
def plus(song){
this.songs << song
}
def minus(song){
def val = this.songs.lastIndexOf(song)
this.songs.remove(val)
}
def printPlayList(){
songs.each{ song -> println "${song.getTitle()}" }
}
}
通過實現 plus() 和 minus() 方法,我重載了 + 和 -,現在可以把它們用在腳本中了。清單 5 演示了它們從播放列表添加和減少歌曲的行為:
清單 5. 播放音樂
sng1 = new Song("SpanishEyes.mp3")
sng2 = new Song("RaceWithDevilSpanishHighway.mp3")
sng3 = new Song("Nena.mp3")
jbox = new JukeBox()
jbox + sng1
jbox + sng2
jbox + sng3
jbox.printPlayList() //prints Spanish Eyes, Race with the Devil.., Nena
jbox - sng2
jbox.printPlayList() //prints Spanish Eyes, Nena
重載,重載的,重載器
繼續進行這個重載的 主題,您可能注意到,在表 3 中有一個可以重載的算術類操作符 <<,它恰好也為 Groovy 的集合重載。在集合的情況下,<< 覆蓋後的作用像普通的 Java add() 方法一樣,把值添加到集合的尾部(這與 Ruby 也很相似)。在清單 6 中,可以看到當我模擬這個行為,允許 JukeBox 的用戶通過 << 操作符以及 + 操作符添加 Song 時發生的情況:
清單 6. 左移音樂
def leftShift(song){
this.plus(song)
}
在清單 6 中,我實現了 leftShift 方法,它調用 plus 方法添加 Song 到播放列表。清單 7 顯示了 << 操作符的效果:
清單 7. 比賽進行中
jbox << sng2 //re-adds Race with the Devil...
可以看出,Groovy 的算術類重載操作符不僅能負重,而且能做得很快!
數組類操作符
Groovy 支持重載標准的 Java 數組存取語法 [],如表 4 所示:
表 4. 數組操作符
操作符 方法
a[b] a.getAt(b)
a[b] = c a.putAt(b, c)
數組存取語法很好地映射到集合,所以我在清單 8 中更新了 JukeBox 類,把兩種情況都做了重載:
清單 8. Music 重載
def getAt(position){
return songs[position]
}
def putAt(position, song){
songs[position] = song
}
現在我實現了 getAt 和 putAt,我可以使用 [] 語法了,如清單 9 所示:
清單 9. 還能比這更快麼?
println jbox[0] //prints Spanish Eyes
jbox[0] = sng2 //placed Race w/the Devil in first slot
println jbox[0] //prints Race w/the Devil
更 Groovy 化的 JDK 方法
一旦掌握了操作符重載的概念和它在 Groovy 中的實現,就可以看到許多日常的 Java 對象已經 被 Groovy 的作者做了改進。
例如,Character 類支持 compareTo(),如清單 10 所示:
清單 10. 比較字符
def a = Character.valueOf('a' as char)
def b = Character.valueOf('b' as char)
def c = Character.valueOf('c' as char)
def g = Character.valueOf('g' as char)
println a < b //prints true
println g < c //prints false
同樣,StringBuffer 可以用 << 操作符進行添加,如清單 11 所示:
清單 11. 緩沖區中的字符串
def strbuf = new StringBuffer()
strbuf.append("Error message: ")
strbuf << "NullPointerException on line ..."
println strbuf.toString() //prints Error message: NullPointerException on line ...
最後,清單 12 表示 Date 可以通過 + 和 - 來操縱。
清單 12. 是哪一天?
def today = new Date()
println today //prints Tue Oct 11 21:15:21 EDT 2005
println "tomorrow: " + (today + 1) //Wed Oct 12 21:15:21 EDT 2005
println "yesterday: " + (today - 1) //Mon Oct 10 21:15:21 EDT 2005
結束語
可以看到,操作符的即時多態,或操作符重載,對於我們來說,如果小心使用和記錄,會非常強大。但是,要當心不要濫用這個特性。如果決定覆蓋一個操作符去做一些非常規的事情,請一定要清楚地記錄下您的工作。對 Groovy 類進行改進,支持重載非常簡單。小心應對並記錄所做的工作,對於由此而來的方便的簡寫形式來說,代價非常公道。