Java 單繼承+接口設計限制了類擴展靈活性。新一代 JVM 語言各自實現了解決「表達式問題」的方案:在不修改源碼、不使用繼承前提下,向既有類添加方法。本文對比三語言核心機制。
如何在不重編譯現有代碼、保持類型安全前提下,為數據類型添加新案例與新函數?三語言給出不同答案。
class IntegerConv {
static Double getAsMeters(Integer self) { self * 0.30480 }
static Double getAsFeet(Integer self) { self * 3.2808 }
}
// 使用:詞法作用域內生效
use(IntegerConv) {
assert 1.asFeet == 3.2808
assert 1.asMeters == 0.30480
}
靜態方法首參數為目標類型,use 塊限制作用域,屬性語法糖 (getAsMeters → .asMeters) 提升 DSL 表達力。
Integer.metaClass.getAsM = { delegate * 0.30480 }
Integer.metaClass.getAsFt = { delegate * 3.2808 }
assert 1.asM == 0.30480
assert 1.asFt == 3.2808
全局生效,可覆蓋現有方法,Grails 等框架核心機制。
class UnitWrapper(i: Int) {
def asFt = i * 3.2808
def asM = i * 0.30480
}
implicit def unitWrapper(i: Int) = new UnitWrapper(i)
println(1.asM) // 自動轉換調用
編譯期類型安全,隱式搜索機制自動注入包裝器。無法修改/刪除現有方法,但語義清晰、無運行時驚喜。
(defprotocol UnitConversions
(asF [this]) (asM [this]))
(extend Number UnitConversions
{:asF (fn [this] (* this 3.2808))
:asM #(* % 0.30480)})
(println (asM 1)) ; 0.3048
協議類似 Java 接口,extend 將函數映射綁定到類型。函數式風格,支持 extend-protocol 批量擴展多類型(如 Compojure 渲染協議)。
| 語言 | 機制 | 作用域 | 可修改現有方法 | 類型安全 | 風格 |
|---|---|---|---|---|---|
| Groovy | Category / ExpandoMetaClass | 詞法 / 全局 | 是 | 動態 | 面向對象+元編程 |
| Scala | 隱式轉換 | 隱式作用域 | 否 | 編譯期 | 函數式+面向對象 |
| Clojure | 協議 + extend | 全局 | 否 (函數映射) | 動態契約 | 函數式+數據導向 |
三種方案各有千秋:Groovy 最靈活適合 DSL,Scala 最安全適合大型系統,Clojure 最簡潔適合數據驅動架構。選擇取決於團隊技術棧與架構哲學。