在上一篇文章中,我們探討了新世代 JVM 語言如何移除 Java 中冗餘的樣板代碼與不必要的複雜度。本文將深入剖析這三門語言——Groovy、Scala、Clojure——是如何解決 Java 核心設計中的三大痛點:陳述式與表達式的二元對立、受檢異常的處理機制,以及臭名昭著的 null 邊界情況。
Java 繼承自 C 語言,強行區分「陳述式」與「表達式」。if、while、void 方法皆為陳述式,不產生值;1 + 2 才是表達式。這種區分在 Fortran 時代源於硬體限制,現代語言設計早已證明:純表達式導向的語言更簡潔、更具組合性。函數式語言徹底消除了這種界限。
Groovy 保留 Java 風格的陳述式,但大幅擴充表達式能力。觀察其 if 的兩種寫法:
def x = 5
def y = 0
// 傳統 if 陳述式:必須透過副作用賦值
if (x % 2 == 0) y = x * 2 else y = x - 1
println y // 4
// 三元運算子 ?: 才能在表達式中完成判斷賦值
y = x % 2 == 0 ? (x *= 2) : (x -= 1)
println y // 4
傳統 if 無返回值,強迫開發者製造副作用;三元運算子雖能內嵌賦值,卻犧牲了可讀性與擴展性(難以加入副作用邏輯)。
if 即表達式Scala 徹底消除三元運算子,if-else 直接返回值:
val x = 5
val y = if (x % 2 == 0) x * 2 else x - 1
println(y)
更強大的是,if 代碼塊支援多行邏輯與副作用,最後一行自動成為返回值:
val z = if (x % 2 == 0) {
println("divisible by 2")
x * 2
} else {
println("not divisible by 2; odd")
x - 1
}
println(z)
注意:代碼塊順序至關重要,返回值必須置於最後求值。
Clojure 同為純表達式語言,但引入 do 包裝器顯式隔離副作用:
(let [x 5
y (if (= 0 (rem x 2)) (* x 2) (- x 1))]
(println y))
若需在分支中執行副作用(如日誌),必須用 do 封裝:
(let [x 5
a (if (= 0 (rem x 2))
(do (println "divisible by 2") (* x 2))
(do (println "not divisible by 2; odd") (- x 1)))]
(println a))
do 依序求值所有表達式,以最後一個為整體返回值。許多 Clojure 結構(如 let)內建隱式 do,減少樣板代碼。
從 Groovy 的妥協、Scala 的統一、到 Clojure 的嚴格分離,三門語言共同指向同一趨勢:消除不必要的陳述式/表達式二元對立,讓代碼更具組合性與推理能力。
Java 的受檢異常曾被視為「看似不錯的特性」,實則導致大量樣板 try-catch、被迫吞噬異常或強制拋出。三門新世代語言全部移除受檢異常,遇 Java 互操作時自動轉為 RuntimeException,並將異常機制融入表達式體系。
Nothing 底型與 finally 的返回值陷阱Scala 允許 throw 作為表達式返回值:
val quarter = if (n % 4 == 0) n / 4 else throw new RuntimeException("n must be quarterable")
類型系統如何兼容 Int 與異常?Scala 引入 Nothing 底型——所有類型的子類型。編譯器視 throw 返回 Nothing,自然兼容 Int。
警示:finally 會覆蓋返回值
def testReturn(): Int = try { return 1 } finally { return -1 } // 返回 -1
def testImplicitReturn(): Int = try { 1 } finally { -1 } // 返回 1
顯式 return 會被 finally 覆蓋;隱式返回值(最後一行)則被忽略。finally 應專注清理副作用,而非參與值計算。
try 即表達式Clojure 的 (try ...) 返回值為:
try 塊最後一行catch 塊最後一行(try
(do-work)
(do-more-work)
(catch SomeException e
(println "Caught" (.getMessage e))
"exception message")
(finally
(do-clean-up)))
主路徑返回值來自 (do-more-work),清晰且符合表達式語義。
Tony Hoare 稱發明 null 為「十億美元錯誤」。Java 開發者深受 NullPointerException 之苦,新世代語言給出不同答案。
?. 與 Elvis 操作符 ?:將防空判斷內建語法:
obj?.someMethod() // obj 為 null 時直接返回 null,不拋異常
def streetName = user?.address?.street // 鏈式安全導航
// Elvis 操作符:左側為真返回左側,否則返回右側默認值
def name = user.name ?: "Unknown" // 等同於 user.name ? user.name : "Unknown"
Option 類型化缺失值Scala 將 null 降級為 scala.Null(僅為引用類型底型),引入 Option[T] 封裝「可能存在的值」:
val designers = Map("Java" -> "Gosling", "C" -> "K&R")
designers get "C" // Some("K&R")
designers get "Scala" // None
配合模式匹配安全解包:
println(designers get "Pascal" match {
case Some(s) => s
case None => "?"
})
Option 強制調用者處理缺失情況,從類型層面消除 NPE 風險。
本文深入剖析了表達式統一、異常機制改良、空值處理三大主題。每門語言雖路徑不同,但共同目標直指 Java 核心痛點。值得注意的是,語言特性高度耦合:表達式導向重塑了異常處理慣用法,類型系統演進重新定義了空值語義。
下一篇,我們將探討新世代語言如何超越「繼承即擴展」的單一範式,提供更強大的行為組合機制。