前兩期探討了Java下一代語言擴展既有類別的多種途徑,本文深入剖析Groovy元編程在多重情境下的實戰應用,展現其解決常見問題的簡潔威力。
介面是Java語義復用的核心機制。Groovy除支援傳統implements外,更允許以閉包或映射直接強制轉換為介面實例,大幅減少樣板碼。
以FilenameFilter為例,Java需建立匿名內部類覆寫accept();Groovy僅需一閉包配合as運算子:
new File('.').list({ File dir, String name -> new File(name).isDirectory() } as FilenameFilter).each { println it }
as FilenameFilter將閉包具體化為實作介面的類別,對單一方法介面尤為適配。
需實作多方法時,可用「方法名稱/閉包」鍵值對構建映射:
h = [hasNext:{ h.i > 0 }, next:{h.i--}]
h.i = 10
def iterator = h as Iterator
while (iterator.hasNext()) print iterator.next() + ", " // 10, 9, 8...1
映射鍵自動視為字串,點號語法(h.i)存取映射屬性,展現Groovy語法糖的優雅。
ExpandoMetaClass不僅能全域擴展核心類別(Object、String),更支援實例級擴展與異常處理增強。
全域擴展雖強大(源自Grails需求),但有時需限制範圍。Groovy允許針對單一實例操作:
def list = new ArrayList()
list.metaClass.randomize = { -> Collections.shuffle(delegate); delegate }
list << 1 << 2 << 3 << 4
println list.randomize() // [2, 1, 4, 3](原列表被改變)
改進版避免副作用:
def list2 = new ArrayList()
list2.metaClass.getRandom = { -> def l = new ArrayList(delegate); Collections.shuffle(l); l }
list2 << 1 << 2 << 3 << 4
println list2.random // [4, 1, 3, 2](原列表不變)
屬性慣例(getRandom→random)消除括號干擾,配合Groovy 1.8引入的命令鏈,構建流暢DSL更為自然。
混入(Mixin)讓行為橫向注入既有層級,無需繼承:
class ListUtils { static randomize(List list) { def l = new ArrayList(delegate); Collections.shuffle(l); l } }
List.metaClass.mixin ListUtils
亦可用於實例級,利於除錯追蹤,是附加行為的最佳實踐。
結合methodMissing鉤子與ExpandoMetaClass,可讓Logger支援任意創意方法名:
Logger.metaClass.methodMissing = { String name, args ->
println "inside methodMissing with $name"
int val = Level.WARNING.intValue() + (Level.SEVERE.intValue() - Level.WARNING.intValue()) * Math.random()
def level = new CustomLevel(name.toUpperCase(), val)
def impl = { Object... varArgs -> delegate.log(level, varArgs[0]) }
Logger.metaClass."$name" = impl
impl args
}
Logger log = Logger.getLogger(this.class.name)
log.neal "really messed this up"
log.minor_mistake "can fix later"
首次調用動態生成方法並快取,後續直呼實作,極具動態語言風範。
Groovy以元編程模擬AOP,無需位元碼織入:
def oldTransfer = Bank.metaClass.getMetaMethod("transfer", [Account, Account, BigDecimal] as Object[])
Bank.metaClass.transfer = { Account to, Account from, BigDecimal amount ->
println "Logging transfer: to:${to}, from:${from}, amount:${amount}"
oldTransfer.invoke(delegate, [to, from, amount] as Object[])
}
此前置切點(Before Advice)在原方法執行前注入日誌,Ruby社區稱之為「Monkey Patching」。
註解驅動的AST轉換修改抽象語法樹,超越運行時擴展極限。Groovy預置實用轉換:
import groovy.transform.*;
@Immutable @TupleConstructor @EqualsAndHashCode @ToString(includeNames=true, includeFields=true)
final class Point { int x; int y }
編譯期自動產生:元組建構子、equals/hashCode、不可變性、toString。優於IDE產生(需手動維護)與反射(運行時開銷大)。
更可透過完整API自訂AST轉換,掌控代碼生成的最細粒度。
Groovy元編程提供令人眼花繚亂的擴展選擇:介面強制轉換、ExpandoMetaClass、混入、methodMissing、AOP模擬、AST轉換。下一期將探索Scala的特徵與混入機制。