了解Java 下一代語言如何減少樣板代碼和降低復雜性
Java 編程語言誕生時所面臨的限制與如今的開發人員所面臨的條件有所不同。具體來講,由於上世紀 90 年代中期的硬 件的性能和內存限制,Java 語言中存在原語類型。從那時起,Java 語言不斷在演化,通過自動裝箱(autobox)消除了許 多麻煩操作,而下一代語言(Groovy、Scala 和 Clojure)更進一步,消除了每種語言中的不一致性和沖突。
在這 一期的文章中,我將展示下一代語言如何消除一些常見的 Java 限制,無論是語法上還是默認行為上。第一個限制是原語數 據類型的存在。
原語的消亡
Java 語言最開始有 8 對原語和相應的類型包裝器類(最初用於解決性能和內存 限制),並通過自動裝箱逐步地淡化了它們之間的區別。Java 下一代語言更進一步,讓開發人員覺得好像根本不存在差別 。
Groovy 完全隱藏了原語類型。例如,int 始終表示 Integer,Groovy 自動處理數字類型的上變換,防止出現數 值溢出錯誤。例如,請查看清單 1 中的 Groovy shell 交互:
清單 1. Groovy 對原語的自動處理
groovy:000> 1.class ===> class java.lang.Integer groovy:000> 1e12.class ===> class java.math.BigDecimal
在 清單 1 中,Groovy shell 顯示,即使是常量也是通過底層的類來表示 的。因為所有數字(和其他偽裝的原語)都是真正的類,所以可以使用元編程技術。這些技術包括將方法添加到數字中(這 通常用於構建特定領域的語言,即 DSL),支持 3.cm 這樣的表達式。在後面介紹可擴展性的那期文章中,我會更全面地介 紹此功能。
與 Groovy 中一樣,Clojure 自動屏蔽原語與包裝器之間的區別,允許對所有類型執行方法調用,自動 處理容量的類型轉換。Clojure 封裝了大量底層優化,這已在語言文檔中詳細說明。在許多情況下,可提供類型 hints,使 編譯器能夠生成更快的代碼。例如,無需使用 (defn sum[x] ... ) 定義方法,可以添加一個類型提示,比如 (defn sum [^float x] ... ),它會為臨界區 (critical section) 生成更高效的代碼。
Scala 也屏蔽了原語之間的區別,通 常對代碼的時效性部件使用底層原語。它還允許在常量上調用方法,就像 2.toString 中一樣。借助其混搭原語和包裝器的 能力,比如 Integer,Scala 比 Java 自動裝箱更加透明。例如,Scala 中的 == 運算符可在原語和對象引用上正確運行( 比較值,而不是引用),而不同於相同運算符的 Java 版本。Scala 還包含一個 eq 方法(以及一個對稱的 ne 方法),它 始終比較底層引用類型是否等效。基本而言,Scala 會智能地切換默認行為。在 Java 語言中,== 會對引用數據進行比較 ,您幾乎不需要這麼做,可以使用不太直觀的 equals() 比較值。在 Scala 中,== 能正確運行(比較值),無論底層實現 是什麼,它還提供了一個方法來執行不太常見的引用相等性檢查 (reference equality check)。
Scala 的這一特性 表明,Java 下一代語言的一個重要優勢在於:將低級細節卸載到語言和運行時,開發人員能夠有更多的時間考慮更高級的 問題。
簡化默認行為
人們的看法高度一致,大部分 Java 開發人員都認為,在 Java 語言中常見的操作需要 太多的語法。例如,屬性定義和其他樣板代碼使類定義變得很雜亂,掩蓋了重要的方法。所有 Java 下一代語言都提供了簡 化創建和訪問過程的途徑。
Scala 中的類和 case 類
Scala 已簡化了類定義,可為您自動創建存取函數、賦 值函數和構造函數。例如,請查看清單 2 中的 Java 類:
清單 2. Java 中簡單的 Person 類
class Person { private String name; private int age; Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return name + " is " + age + " years old."; } }
清單 2 中惟一的非樣板代碼是改寫的 toString() 方法。構造函數和所有方法都由 IDE 生成。相比快速生成代 碼,在以後輕松理解它更為重要。無用的語法增加了您在理解底層含義之前必須使用的代碼量。
Scala Person 類
令人震驚的是,清單 3 中用 Scala 編寫的簡單 3 行定義就創建了一個等效的類:
清單 3. Scala 中的等 效類
class Person(val name: String, var age: Int) { override def toString = name + " is " + age + " years old." }
清單 3 中的 Person 類濃縮成了一個可變的 age 屬性、一個不可變的 name 屬性,以及一個包含兩個參數的構 造函數,還有我改寫的 toString() 方法。很容易看到這個類的獨特之處,因為有趣的部分沒有埋藏在語法中。
Scala 的設計強調了以最少的語法創建代碼的能力,它使許多語法成為可選語法。清單 4 中的簡單類演示了一個將 字符串更改為大寫字母的 Verbose 類:
清單 4. Verbose 類
class UpperVerbose { def upper(strings: String*) : Seq[String] = { strings.map((s:String) => s.toUpperCase()) } }
清單 4 中的許多代碼都是可選的。清單 5 給出了相同的代碼,現在使用了一個 object 而不是 class:
清單 5. 一個轉換為大寫的更簡單的對象
object Up { def upper(strings: String*) = strings.map(_.toUpperCase()) }
對於等效於 Java 靜態方法的 Scala 代碼,可創建一個 object(與獨體實例等效的 Scala 內置實體)而不是 一個類。方法的返回類型、用於將單行方法主體分開的括號,以及 清單 4 中無用的 s 參數都從 清單 5 中消失了。Scala 中的這種 “可折疊語法” 有利有弊。使用可折疊語法,能夠以非常符合語言習慣的方式編寫代碼,但這讓不熟悉的人難以 理解您的代碼。
case 類
用作數據持有者的簡單類在面向對象的系統中很常見,尤其是必須與不同系統通信 的系統。這種類型的類的流行使得 Scala 項目向前推進了一步,創造了 case 類。case 類自動提供了多種便捷的語法:
可根據該類的名稱創建一個工廠方法。例如,可以在不使用 new 關鍵字的情況下構造一個新實例:val bob = Person("Bob", 42)。
該類的參數列表中的所有參數都自動 val,也就是說,它們是作為不可變的內部字 段來維護的。
編譯器為您的類生成合理的默認 equals()、hashCode() 和 toString() 方法。
編譯器將一個 copy() 方法添加到類中,以便您可返回某個副本來執行變體式更改。
Java 下一代語言不僅修復了語法瑕疵,還促 進了對現代軟件工作原理的更准確的理解,朝這個方向塑造它們的工具。
Groovy 的自動生成屬性
在 Java 下一代語言中,Groovy 與 Java 語法最接近,為常見情形提供了稱為 “語法糖 (syntactic-sugar)” 的代碼生成方法。 參見清單 6 中簡單的 Groovy Person 類:
清單 6. Groovy Person 類
class Person { private name def age def getName() { name } @Override String toString() { "${name} is ${age} years old." } } def bob = new Person(name: "Bob", age:42) println(bob.name)
在 清單 6 的 Groovy 代碼中,定義一個字段 def 會得到一個存取函數和賦值函數。如果僅 喜歡其中一個函數,可自行定義它,就像我對 name 屬性所做的那樣。盡管該方法名為 getName(),但我仍然可以通過更直 觀的 bob.name 語法訪問它。
如果希望 Groovy 自動為您生成 equals() 和 hashCode() 方法對,可以向類中添加 @EqualsAndHashCode 注釋。該注釋使用 Groovy 的抽象語法樹 (Abstract Syntax Tree, AST) 轉換 生成基於您的屬性的 方法。在默認情況下,此注釋僅考慮屬性(而不考慮字段);如果添加了 includeFields=true 修飾符,它也會考慮字段。
Clojure 的映射式記錄
可在 Clojure 中像其他語言中一樣創建相同 Person 類,但這並不符合語言習慣。 傳統上,Clojure 等語言依靠映射(名稱-值對)數據結構來持有這種類型的信息,並使用了一些處理該結構的函數。盡管 仍然可以在映射中建模結構化的數據,但目前更常見的情形是使用記錄。記錄是 Clojure 對具有屬性(常常是嵌套的)的 類型名的更加正式的封裝,每個實例具有相同的語義含義。(Clojure 中的記錄就像類 C 語言中的 struct。)
例 如,請考慮以下人員定義:
(def mario {:fname "Mario" :age "18"})
鑒於此結構,可以通過 (get mario :age) 訪問 age。簡單的訪問 是映射上的一個常見操作。借助 Clojure,可以利用使用鍵充當著映射上的存取函數 的語法糖,以便使用更有效的 (:agemario) 速記法。Clojure 期望對映射進行操作,所以它提供了大量語法糖來簡化此操作。
Clojure 還擁有訪 問嵌套的映射元素的語法糖,如清單 7 所示:
清單 7. Clojure 的速記式訪問
(def hal {:fname "hal" :age "17" :address {:street "Enfield Tennis Academy" :city "Boston" :state "MA"}}) (println (:fname hal)) (println (:city (:address hal))) (println (-> hal :address :city))
在 清單 7 中,我定義了一個名為 hal 的嵌套數據結構。對外部元素的 訪問按預期進行 ((:fname hal))。如 清單 7 中倒數第二行所示,Lisp 語法執行 “內外” 評估。首先,必須從 hal 獲 取 address 記錄,然後訪問 city 字段。因為 “內外” 評估是一種常見用法,所以 Clojure 提供了一個特殊運算符(- > thread 運算符)來反轉表達式,使它們更加自然、更具可讀性:(-> hal :address :city)。
可使用記錄 創建等效的結構,如清單 8 所示:
清單 8. 使用記錄創建結構
(defrecord Person [fname lname address]) (defrecord Address [street city state]) (def don (Person. "Don" "Gately" (Address. "Ennet House" "Boston", "MA"))) (println (:fname don)) (println (-> don :address :city))
在 清單 8 中,我使用 defrecord 創建了相同的結構,得到了一種更加 傳統的類結構。借助 Clojure,可以通過熟悉的映射操作和方言在記錄結構中實現同樣便捷的訪問。
Clojure 1.2 圍繞常見操作的記錄定義通過兩個工廠函數添加了語法糖:
->類型名稱, 接收字段的位置參數
->映射 ->類型名稱, 字段值的關鍵字映射
使用符合語言習慣的函數,代碼由 清單 8 轉換成版本 清單 9.
清單 9. Clojure 的漂亮的語法糖
(def don (->Person "Don" "Gately" (->Address "Ennet House" "Boston", "MA")))
在許多情況下,記錄比映射和扁平結構更受歡迎。首先, defrecord 創建了一個 Java 類,使它更容易在多方法定義中使用。然後,defrecord 指定更多任務,在您定義記錄時啟用 字段驗證和其他細微處理。第三,記錄速度快得多,尤其在您擁有一組固定的已知鍵的時候。
Clojure 結合使用記 錄和協議來構造代碼。未來的一期文章將介紹它們的關系。
結束語
與 Java 語言相比,所有 3 種 Java 下 一代語言都提供了更便捷的語法。Groovy 和 Scala 使構建類和常見情形更加輕松,而 Clojure 使映射、記錄和類能夠無 縫地互操作。所有 Java 下一代語言的一個共同主旨是消除不必要的樣板代碼。在下一期文章中,我將繼續探討這個主題並 討論一些異常。