程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java 下一代: Groovy、Scala 和 Clojure 中的共同點(三)

Java 下一代: Groovy、Scala 和 Clojure 中的共同點(三)

編輯:關於JAVA

反思異常、表達式和空

在 上一期文章 中,我介紹了 Java 下一代語言用來消除 Java 語言中華而不實的東西和復雜性的創新方式。在這一期 文章中,我將展示這些語言如何消除 Java  的一些瑕疵:異常、語句與表達式,以及圍繞 null 的邊緣情況。

表達式

Java  語言從 C 語言那裡繼承的一項傳承是區分編程語言 和編程表達式。Java 語句的示例包 括使用 if 或 while 的代碼行,以及使用 void 來聲明不會返回任何值的方法的代碼行。表達式(比如 1 + 2 )用於求取 某一個值。

這種區分在最早的編程語言中就已經開始,比如在 Fortran 中,這種區分基於硬件條件以及對編程語言 設計的初步了解。在許多語言中,它被保留為操作(語句)與求值(表達式)的指示器。但語言設計人員逐漸意識到,該語 言可以完全由表達式組成,在對結果不感興趣的時候忽略結果。事實上,所有函數式語言完全可以消除這種區分,僅使用表 達式。

Groovy 的 if 和 ?:

在 Java 下一代語言中,傳統的命令式語言(Groovy)和函數式語言(Clojure 和 Scala)之間的分離展示了向表達式的進化。Groovy 仍然包含語句,這些語句基於 Java 語法,但添加了更多的表達式 。而 Scala 和 Clojure 則完全使用表達式。

語句和表達式中包含的內容都為語言增添了語法上的笨拙。可以考慮 Groovy 中的 if 語句,它繼承自 Java。它有兩個版本,清單 1 對它們進行了比較,這兩個版本是用於執行判斷的 if 語 句,以及神秘的三元運算符 ?::

清單 1. Groovy 的兩個 if 語句

def x = 5
def y = 0
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 語句 清單 1 中,我必須將 x 的值設置為一個副作用 (side effect),因為 if 語句沒 有返回任何值。要執行判斷並同時進行賦值,必須使用三元賦值,如 清單 1 中的第二個賦值語句所示。

Scala 的 基於表達式的 if 語句

Scala 消除了對三元運算符的需求,允許 if 表達式對兩種情況都進行處理。您可以使用它 ,就像在 Java 代碼中使用 if 語句那樣(忽略返回值),或者在賦值語句中使用它,如清單 2 中所示:

清單 2. Scala 的基於表達式的 if 語句

val x = 5
val y = if (x % 2 == 0) 
          x * 2
    else
      x - 1
println(y)

Scala 和其他兩種 Java 下一代語言一樣,不要求方法中包含明確的 return 語句。因此,方法的最 後一行是返回值,強調了這些語言中的方法的基於表達式的特性。

當您在 Java 和 Groovy 代碼中進行操作和設置 值時,可以將每個響應封裝為一個代碼塊,如清單 3 中所示,並包含任何所需的副作用:

清單 3. Scala if + 副 作用

val z = if (x % 2 == 0) {
              println("centerisible by 2")
          x * 2
        } else {
              println("not centerisible by 2; odd")
          x - 1
        }
println(z)

在 清單 3 中,除了返回新計算得出的值之外,我還為每種情況打印了一條狀態消息。代碼塊中的代 碼行的順序非常重要:代碼塊的最後一行表示符合條件的返回值。因此,當您使用基於表達式的 if 進行混合求值和具有副 作用時,必須非常小心。

Clojure 的表達式和副作用

Clojure 也完全由表達式組成,但它更進一步,從求值 代碼中區分出了副作用代碼。前兩個示例的 Clojure 版本是用一個 let 代碼塊表達的,在清單 4 中,這允許定義局部作 用域變量:

清單 4. Clojure 的基於表達式的 if 語句

(let [x 5
      y (if (= 0 (rem x 2)) (* x 2) (- x 1))]
  (println y))

在 清單 4 中,我為 x 分配了一個值 5,然後使用 if 建立了表達式來計算兩個條件:(rem x2 ) 調用了 remainder 函數,類似於 Java % 操作符,並將結果與零值進行比較,在除以 2 時檢查零剩余值(zero remainder)。在 Clojure 的 if 表達式中,第一個參數是 condition,第二個參數是 true 分支,第三個參數是可選的 else 分支。if 表達式的結果被分配給 y,然後被打印出來。

Clojure 也允許對每個條件使用代碼塊(可以包含副 作用),但需要一個包裝器,比如 (do ...)。包裝器通過使用最後一行作為代碼塊的返回值,對代碼塊中的每個表達式 進行求值。清單 5 說明了如何對某個條件或副作用進行求值:

清單 5. Clojure 中的顯式副作用

(let 

[x 5
      a (if (= 0 (rem x 2))
          (do
            (println "centerisible by 2")
            (* x 2))
          (do
            (println "not centerisible by 2; odd")
            (- x 1)))]
  (println a))

在 清單 5,我為 if 表達式的返回值分配了 a。對於每個條件,都創建了一個 (do ...) 包裝 器,並允許使用任意數量的語句。代碼塊的最後一行是 (do...) 代碼塊的返回值,這類似於 清單 3 中的 Scala   示例。請確保目標返回值是最後進行求值的。以這種方式使用 (do...) 代碼塊是如此之常見,以致於 Clojure 中的許多 結構(比如 (let []))已經包含隱式 (do ...) 代碼塊,這消除了許多情況下對它們的需求。

Java/Groovy 代 碼和 Scala/Clojure 代碼中的表達式的比較指示了編程語言中的總趨勢,即消除不必要的語句/表達式分歧。

異常

對於我而言,Java 編程中 “似乎最不錯的特性” 是已檢查出的異常以及廣播和(實施)異常意識(exception awareness) 的能力。事實上,這帶來了一場噩夢,它強迫用戶進行斷章取義的、不必要的異常處理(和誤操作)。

所有 Java 下一代語言都使用了已經內置於 JVM 中的異常機制,以及基於 Java 語法的語法,並對這些語法進行了 修改,以獲得它們自己的獨一無二的語法。這些語言都消除了已檢查出的異常,在執行 Java 交互操作期間遇到這些異常時 ,會將它們轉換成為 RuntimeExceptions。

Scala 對 Java  異常處理機制的轉換表現出了一些興趣,想在它 自己的基於表達式的世界中采用該機制。首先,應考慮到的事實是,異常可能是表達式的返回值,如清單 6 中所示:

清單 6. 異常是返回值

val quarter = 
  if (n % 4 == 0)
    n / 4
  else
    throw new RuntimeException("n must be quarterable")

在 清單 6 中,我分配了 n 值的 1/4 或一個異常 。如果觸發了異常,那麼返回值將沒有任何意義,因為在求取返回值之前,異常已經傳播開來。這種墨守成規的賦值看著似 乎有些奇怪,可以將 Scala 視為一種類型化的語言。Scala  異常類型不是一種數字類型,開發人員不熟悉這種類型 ,可將它視為 throw 表達式的返回值。Scala  以獨創的方式解決了這個問題,它使用特殊的 Nothing 類型作為 throw 的返回類型。Any 在 Scala 中位於繼承層次結構的頂部(類似於 Java 中的 Object),這意味著所有類都可以擴展 它。相反,Nothing 位於底部,它是其他所有類的自動子類。因此,編譯 清單 6 中的代碼是合法的:它要麼返回一個數字 ,要麼在設置返回值之前觸發異常。編譯器沒有報告錯誤,這是因為 Nothing 是 Int 的一個子類。    

其次,finally 代碼塊在基於表達式的世界中有一些有趣的行為。Scala 的 finally 代碼塊的作用類似於其 他功能,但有一些與返回值有關的微妙行為。請考慮清單 7 中的代碼:

清單 7. Scala 的 finally 返回值

def testReturn(): Int = 
  try {                               
    return 1                      
  } finally {                         
    return -1                     
  }

在 清單 7 中,總體返回值是 -1。finally 代碼塊的返回值“覆蓋”了從 try 語句的主體返回的值。這個令 人吃驚的結果僅出現在 finally 代碼塊包含顯式 return 語句時,隱式返回值被忽略,如清單 8 中所示:

清單 8. Scala 的隱式返回值

def testImplicitReturn(): Int = 
  try {
    1
  } finally {
   -1
  }

在 清單 8 中,函數的返回值是 1,這說明打算使用 finally 代碼塊作為清理副作用的地方,而不是將它用 作對表達式進行求解的地方。

Clojure 也是完全基於表達式的。(try ...) 的返回值總是以下兩者之一:

沒有異常的 try 代碼塊的最後一行

捕獲了異常的 catch 代碼塊的最後一行

清單 9 顯示了 Clojure 中用於 異常的語法:

清單 9. Clojure 的 (try...catch...finally) 代碼塊

(try
  (do-work)
  (do-more-work)
  (catch SomeException e  
    (println "Caught" (.getMessage e)) "exception message")
  (finally
    (do-clean-up)))

在 清單 9 中,主路徑的返回值是來自 (do-more-work) 的返回值。

Java 下一代 語言汲取了 Java 異常機制的長處,擯棄了該機制的缺點。此外,盡管有些實現有所不同,但它們設法將這些異常整合到基 於表達式的透視圖中。

在 2009 年於 QCon London 召開的報告會議中,Tony Hoare 將他發明的“null” 概念稱為 ALGOL W(1965 年引入的一種實驗性的面向對象的語言),“十億美元的錯誤” 是由於編程語言中的 null 引用 所導致的所有問題帶來的。Java 語言自身也遇到了一些與 null 有關的邊緣情況,Java 下一代語言解決了這些問題。

例如,Java 編程中一個習慣用語用於防止在您試圖調用方法之前出現 NullPointerException:

if (obj != null) {

   obj.someMethod();

}

Groovy 已經將這種模式封裝在安全導航 操作符 ?. 中。它自動 進行左側的 null 檢查,並嘗試僅在返回值為非 null 時進行方法調用;否則返回 null。

obj?.someMethod ();

def streetName = user?.address?.street

也可以采用嵌套方式調用安全導航操作符。

Groovy 的密 切相關的 Elvis 操作符 ?: 縮短了默認值中的 Java 三元運算符。例如,以下這些代碼行是等效的:

def name = user.name ? user.name : "Unknown" //traditional ternary operator usage

def name = user.name ?: "Unknown"  // more-compact Elvis operator

當左側有一個值(通常是默認值)時,Elvis 操 作符會保護它,否則設置一個新值。Elvis 操作符是三元運算符的一個較短的、傾向於操作符的版本。

Scala 增強 了 null 的概念,並使它成為一個類(scala.Null),並附帶一個相關的類 scala.Nothing。Null 和 Nothing 都位於 Scala 類分層結構的底部。Null 是每個引用類的子類,而 Nothing 則是其他每種類型的子類。

Scala 提供了 null 和表達式的替代物,以指示是否缺少值。許多關於收集的 Scala 操作(比如 Map 上的 get)會返回一個 Option 實例,該 示例包含以下兩個部件之一(但絕不會兩個都包括):Some 或 None。清單 10 中的 REPL 交互顯示了一個示例:

清單 10. Scala 的 Option 返回值

scala> val designers=Map("Java" -> "Gosling", "c" -> 

"K&R", "Pascal" -> "Wirth")
designers: scala.collection.immutable.Map[java.lang.String,java.lang.String] = 
       Map(Java -> Gosling, c -> K&R, Pascal -> Wirth)
    
scala> designers get "c"
res0: Option[java.lang.String] = Some(K&R)
    
scala> designers get "Scala"
res1: Option[java.lang.String] = None

請注意,在 清單 10 中,來自成功的 get 的返回值是一個 Option [java.lang.String] = Some(value),反之則是 None 中的一個失敗的查找結果。從收集值中獲得展開值 (unwrapping value) 的一項技術使用了模式匹配,模式匹配本身是一個表達式,它允許在一個簡潔的表達式中訪問和展開某個值:

println(designers get "Pascal" match { case Some(s) => s; case None => "? "})

Option 允許使用比單獨使用 null 更好的空表達式,尤其在為該表達式的使用提供語法支持時。

結束語

在這一期的文章中,我深入探討了 Java 語言的三個問題領域:表達式、異常和 null。每種 Java 下一代語 言都可以消除 Java 的瑕疵,但每種語言都有自己的慣用方式。表達式的出現改變了用於看似不相關的概念(比如異常)的 一些習慣用語和選項——進一步闡述了語言特性彼此之間是高度耦合的。

Java 開發人員有時會習慣性地認為繼承是 擴展行為的惟一方式。在下一期文章中,我將展示 Java 下一代語言如何提供許多更強大的替代。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved