追隨 Kotlin/Scala,看 Java 12-15 的現代語言特性
本文原發於我的個人部落格:https://hltj.me/java/2020/06/14/java-12-15-lang-features.html。本副本只用於圖靈社群,禁止第三方轉載。
Java 14 釋出已經過去了三個月,Java 15 目前也已經到了“Rampdown Phase One ”階段,其新特性均已敲定。 由於 12-15 都是短期版本,無需考慮也不應該將其用於生產環境。但可以提前瞭解新特性,以免在下一個 LTS(Java17)正式釋出時毫無心理準備。 Java 12-15 引入了一系列改進,本文只討論語言層面的新特性,它們看起來似曾相識——沒錯,這些特性讓人感覺 Java 在沿 Kotlin/Scala 走過的路線前行。
雖然不能說 Java 就是借鑑的它們(畢竟這些特性既非它們獨有也非它們首創),但可以說是 Java 官方對 Kotlin/Scala 這些特性的充分肯定。而這些新特性也讓 Java 向現代語言的方向又邁進了一些,我們逐個來看。
switch 表示式
相當於只支援值匹配的 Kotlin when
表示式/Scala match
表示式。
我們看一個不嚴謹的示例:判斷一個非空物件對應哪種 JSON 型別,使用傳統的 switch
語句實現如下:
switch (obj.getClass().getSimpleName()) {
case "Integer":
case "Long":
case "Float":
case "Double":
System.out.println("number");
break;
case "List":
case "Set":
System.out.println("array");
break;
case "String":
System.out.println("string");
break;
case "Boolean":
System.out.println("boolean");
break;
case "Map":
System.out.println("object");
break;
default:
System.out.println("object/unknown");
}
而使用 switch
表示式可以簡化為:
switch (obj.getClass().getSimpleName()) {
case "Integer", "Long", "Float", "Double" -> System.out.println("number");
case "List", "Set" -> System.out.println("array");
case "String" -> System.out.println("string");
case "Boolean" -> System.out.println("boolean");
case "Map" -> System.out.println("object");
default -> System.out.println("object/unknown");
}
是不是簡潔了很多?當然如果只能這樣用,那還稱不上 switch
表示式,既然是表示式那麼一定可以有一個返回值。 我們可以利用 switch
表示式的返回值來進一步重構上述程式碼:
String jsonType = switch (obj.getClass().getSimpleName()) {
case "Integer", "Long", "Float", "Double" -> "number";
case "List", "Set" -> "array";
case "String" -> "string";
case "Boolean" -> "boolean";
case "Map" -> "object";
default -> "object/unknown";
};
System.out.println(jsonType);
switch
表示式中箭頭的右側不僅可以是常規表示式,還可以是一個程式碼塊,在塊中通過 yield
來指定返回值。 例如在上述 default
分支打一條錯誤資訊,可以這樣改:
String javaType = obj.getClass().getSimpleName();
String jsonType = switch (javaType) {
case "Integer", "Long", "Float", "Double" -> "number";
case "List", "Set" -> "array";
case "String" -> "string";
case "Boolean" -> "boolean";
case "Map" -> "object";
default -> {
System.err.println("unknown type: " + javaType);
yield "object";
}
};
是不是有點類似 Kotlin 的 when
與 Scala 的 match
了?非常像,只是目前只支援簡單的值匹配,還不支援 Kotlin when
的 is
/in
以及 Scala match
的模式匹配。 當然如果綜合 Java 12-15 引入的所有語言特性來看,不難預見 switch
表示式未來會支援模式匹配的。
switch
表示式的優點不僅是簡潔且具有返回值,還避免了傳統 switch
語句的一些坑(如忘記寫 break
語句,再如各 case
/default
子句共享同一個區域性作用域)。 因此,在 Java 14 及以上版本中,應該儘量採用新語法、避免使用傳統的 switch
語句。IDEA 甚至會對傳統 switch
語句標記警告,並且提供了自動將傳統語法重構為新語法的 quick fix 功能。
文字塊
Java 的文字塊(多行字串)語法與 Kotlin 原始字串/Scala 多行字串類似,都是採用三重雙引號括起,不過具體語法、語義不盡相同。 Java 文字塊起始的三重雙引號後只能跟空白符和換行,因此不能像 Kotlin/Scala 那樣寫 """hello"""
,而必須這樣寫:
"""
hello"""
Java 會自動去掉第一個換行以及每行末尾的空白,因此上述字串等同於 "hello"
,但是結尾處三重引號前的換行並不會去掉,例如:
// 等同於 "hello"
var s1 = """
hello""";
// 等同於 "hello"
var s2 = """
hello """;
// 等同於 "hello\n"
var s3 = """
hello
""";
文字塊中的少於連續三個的雙引號都無需轉義,這也是文字塊的用途之一,例如:
System.out.println("""
This is a string literal in Java: "Hello".""");
這段程式碼會輸出 This is a string literal in Java: "Hello".
。 編譯過程中會自動去掉縮排用的空白符,如果存在多行,會以前導空白最少的一行作為基準。
文字塊的另一個用途是便於書寫預排版的文字,例如 ASCII Art 或者豎排文字:
String ci = "┆蝶┆觀┆月┆池┆遊┆ ┆獨┆錢┆古┆來┆端┆\n" +
"┆自┆音┆老┆畔┆人┆ ┆賞┆江┆塔┆客┆陽┆\n" +
"┆舞┆堂┆祠┆問┆醉┆ ┆亦┆西┆聽┆尚┆至┆\n" +
"┆翩┆外┆前┆奇┆ ┆ ┆悠┆子┆濤┆流┆ ┆\n" +
"┆躚┆雨┆人┆緣┆ ┆ ┆然┆匯┆於┆連┆ ┆\n" +
"┆ ┆連┆絡┆ ┆ ┆ ┆ ┆吳┆越┆ ┆ ┆\n" +
"┆ ┆綿┆繹┆ ┆ ┆ ┆ ┆山┆地┆ ┆ ┆\n";
這段豎排文字是我幾年前寫的《雙調憶江南·庚寅年端午遊杭州》,可以看到自動格式化後第一行沒有與後續幾行對齊,雖然還有變通辦法,但是這本身就已經比較複雜了。 而如果採用文字塊就會簡單很多:
String ci = """
┆蝶┆觀┆月┆池┆遊┆ ┆獨┆錢┆古┆來┆端┆
┆自┆音┆老┆畔┆人┆ ┆賞┆江┆塔┆客┆陽┆
┆舞┆堂┆祠┆問┆醉┆ ┆亦┆西┆聽┆尚┆至┆
┆翩┆外┆前┆奇┆ ┆ ┆悠┆子┆濤┆流┆ ┆
┆躚┆雨┆人┆緣┆ ┆ ┆然┆匯┆於┆連┆ ┆
┆ ┆連┆絡┆ ┆ ┆ ┆ ┆吳┆越┆ ┆ ┆
┆ ┆綿┆繹┆ ┆ ┆ ┆ ┆山┆地┆ ┆ ┆
""";
而既有雙引號又有預排版的多行文字就更適合使用文字塊了,例如 XML 、JSON 或其他配置/程式碼文字的字面值:
var languages = "[\n" +
" {\n" +
" \"name\": \"kotlin\",\n" +
" \"type\": \"static\"\n" +
" },\n" +
" {\n" +
" \"name\": \"julia\",\n" +
" \"type\": \"dynamic\"\n" +
" }\n" +
"]";
上述 JSON 字面值看起來很凌亂,而用文字塊就會清晰很多:
var languages = """
[
{
"name": "kotlin",
"type": "static"
},
{
"name": "julia",
"type": "dynamic"
}
]""";
instanceof
模式匹配
Java 14 預覽、Java 15 二次預覽,預計 Java 16 正式。
類似於 Kotlin 的智慧轉換,但語法不同,在 Scala 中沒有直接對應。
傳統的 instanceof
判斷成功之後仍然需要強制轉換才能按相應型別使用,例如:
if (obj instanceof String) {
System.out.println(((String) obj).length());
}
而使用模式匹配之後,可以在判斷成功時繫結為一個對應型別的變數,之後直接使用該變數即可:
if (obj instanceof String s) {
System.out.println(s.length());
}
需要注意的是,只有成功匹配的分支才能繫結該變數:
if (obj instanceof String s) {
System.out.println(s.length());
} else {
// 錯誤:此處 s 不可用
// System.out.println(s.length());
}
instanceof
模式匹配不僅能用於 if
語句中,還可以用於 while
語句、三目運算子以及 &&
、||
等能使用布林邏輯的地方,例如:
int i = obj instanceof String s ? s.length() : -1;
var isEmptyString = obj instanceof String s && s.isEmpty();
var isNotEmptyString = !(obj instanceof String s) || !s.isEmpty();
目前 Java 中只引入了這一種非常簡單的模式匹配形式,未來應該會引入更多模式匹配語法。
記錄型別
Java 14 預覽、Java 15 二次預覽,預計 Java 16 正式。
記錄型別(record)類似於 Kotlin 的資料類(data class)與 Scala 的樣例類(case class),只是更加嚴格。
在沒有記錄型別之前,建立一個具有各欄位對應 getter、為所有欄位初始化的建構函式、基於所有欄位的 equals()
/hashCode()
/toString()
的簡單類卻需要寫一大堆程式碼,其中大部分都是樣板程式碼。 例如:
class Font {
private final String name;
private final int size;
public Font(String name, int size) {
this.name = name;
this.size = size;
}
public String name() {
return name;
}
public int size() {
return size;
}
public boolean equals(Object o) {
if (!(o instanceof Font other)) return false;
return other.name.equals(name) && other.size == size;
}
public int hashCode() {
return Objects.hash(name, size);
}
public String toString() {
return String.format("Font[name=%s, size=%d]", name, size);
}
}
上述程式碼中,除了類名、欄位型別與欄位名之外,其他的全部都是樣板程式碼。而使用記錄只需非常簡單的一行程式碼即可:
record Font(String name, int size) { }
跟一般類相比,記錄有以下限制:
- 總是隱式繼承自
java.lang.Record
而無法顯式繼承任何任何類 - 記錄隱含了 final 並且不能宣告為抽象
- 不能顯式宣告欄位,也不能定義初始化塊
- 隱式宣告的所有欄位均為 final
- 如果顯式宣告任何會隱式生成的成員,其型別必須嚴格匹配
- 不能宣告 native method(通常譯為“本地方法”,按說應該叫“原生方法”)
除了這些限制之外,它與普通類一致:
- 用
new
例項化 - 可以在頂層宣告,也可以在類內部、區域性作用域中宣告
- 可以宣告靜態方法與例項方法
- 可以宣告靜態欄位與靜態初始化塊
- 可以實現介面
- 可以有其內部型別
- 可以標註註解
記錄型別還可以與接下來提到的密封類/密封介面很好協作,另外記錄還適用於未來版本的模式匹配。
密封類與密封介面
Java 15 預覽,預計 Java 16 二次預覽、Java 17 正式。
Java 15 引入的密封類(sealed class)類似於 Kotlin/Scala 的密封類、密封介面類似於 Scala 的密封特質(sealed trait)。 不妨將二者統稱為密封型別,與普通類/介面不同的是,密封型別限定了哪些類/介面作為其直接子型別。例如:
sealed interface JvmLanguage
permits DynamicTypedJvmLanguage, StaticTypedJvmLanguage {}
sealed interface StaticTypedJvmLanguage extends JvmLanguage {}
sealed class DynamicTypedJvmLanguage implements JvmLanguage
permits Clojure, JvmScriptLanguage {}
final class Java implements StaticTypedJvmLanguage {}
final class Scala implements StaticTypedJvmLanguage {}
final class Kotlin implements StaticTypedJvmLanguage {}
final class Clojure extends DynamicTypedJvmLanguage {}
non-sealed abstract class JvmScriptLanguage extends DynamicTypedJvmLanguage {}
class Groovy extends JvmScriptLanguage {}
在密封型別的宣告中可以通過 permits
顯式宣告其直接子型別列表,也可以省略——編譯器會根據當前檔案中的直接子型別的宣告推斷出來。 一個密封型別的直接子型別必須標註 sealed
、non-sealed
、final
三者之一。
密封型別與記錄是相互獨立的功能,但是二者能夠很好協作,例如:
sealed interface Json {}
record NullVal() implements Json {}
record BooleanVal(boolean val) implements Json {}
record NumberVal(double val) implements Json {}
record StringVal(String val) implements Json {}
record ArrayVal(Object...vals) implements Json {}
record ObjectVal(Map<String, Object> kvs) implements Json {}
此外,還可以用記錄與密封型別來實現代數資料型別(ADT):記錄為積型別、密封型別為和型別。 與記錄類似,密封型別也將適用於未來版本的模式匹配。
小結
Java 12-15 引入了 switch
表示式、文字塊、instanceof
模式匹配、記錄、密封型別這幾個語言新特性,這些特性在 Kotlin/Scala 中基本上都有對應,如同 Java 在追隨 Kotlin/Scala 的步伐。
另外,不知大家有沒有注意到這一點:除了文字塊外,其他幾個特性都直接或間接指向了同一個關鍵詞——模式匹配。 這些特性除了自身價值之外,也都在為未來版本的模式匹配做鋪墊。因此不妨做個大膽預測:在未來的幾個版本中,Java 會引入更完善的模式匹配機制。
些許遺憾
Java 12-15 中引入語言層面的新特性並不很多,很多令人期待新特性都沒有包含在內。 當然語言需要漸進式演化,這也是情理之中的事。唯有兩點我覺得有些遺憾:
空安全
安全表達可選值是現代語言的一個特徵,隔壁 C# 8 引入空安全的經驗告訴我們: 即便語言當初做了錯誤的設計,倘若迷途知返,仍然能夠回到正軌。 Java會不會也有這麼一天?也許會,不過 Java 12-15 顯然沒有,在接下來的幾個版本中這麼做可能性也很渺茫,也許還會在“迷途”中繼續前行很久。
當然關於空安全這塊也不是什麼長進都沒有,Java 14 就有一處:JEP 358: Helpful NullPointerExceptions 改善了 NullPointerException 所攜帶的資訊,以便定位是哪個變數出現空值導致的。
協程
協程即將成為現代工業級語言的標配,不僅 Python、JavaScript、Rust 等語言紛紛引入了協程支援(async
-await
尤為便利),就連隔壁 C++ 也都已將協程納入了今年的新標準(C++ 20)中。 再對比靈活、強大的 Kotlin 協程在非同步程式設計中所帶來的便利,還在堅守 Java 的開發者對協程的期待就不難理解了。 遺憾的是,Project Loom 未能合入 Java 15,那麼按照其他特性的演進週期來看,在下一個 LTS(即 Java 17)釋出時就算包含進來也不會是穩定特性。
當然,目前 JVM 平臺的 5 門主流工業級語言(Java、Kotlin、Scala、Groovy、Clojure)中,只有 Kotlin 具備上述兩個特性,歡迎來 Kotlin 中文站學習。
相關文章
- DPC++中的現代C++語言特性C++
- 有趣的 Scala 語言: 簡潔的 Scala 語法
- 從OOP和FP看蘋果Swift語言與Scala比較OOP蘋果Swift
- 現代程式語言用什麼語言寫成?
- 筆記:追隨雲原生的Java筆記Java
- Objective-C 的現代語法和新特性Object
- Java從8到21的語言新特性Java
- java環境中基於jvm的兩大語言:scala,groovyJavaJVM
- 那些年 我追過的語言
- Scala,基於JVM的併發語言JVM
- 雜談現代高階程式語言
- Kotlin 程式語言初探Kotlin
- 奇特的程式語言特性
- 現代語言Go、Rust、Swift和Dart的比較GoRustSwiftDart
- 現代編譯原理C語言描述pdf編譯原理C語言
- PureBasic 現代 BASIC 程式語言編輯器
- scala 語言值得去學習嗎
- Scala確實是門好語言
- 隨便聊聊 Java 8 的函數語言程式設計Java函數程式設計
- 淺談JavaScript的語言特性JavaScript
- Go語言實現的Java Stream APIGoJavaAPI
- C 語言隨機數生成器的實現分析隨機
- GO語言————6.4 defer 和追蹤Go
- Scala: 感覺像動態的靜態語言
- 有趣的 Scala 語言: 使用遞迴的方式去思考遞迴
- Kotlin Type? vs Scala OptionKotlin
- Kotlin語言極簡介紹Kotlin
- 理解Javascript的動態語言特性JavaScript
- C++ 語言特性的效能分析C++
- Java8 新特性 —— 函數語言程式設計Java函數程式設計
- C++11新特性(二):語言特性C++
- C++11新特性(一):語言特性C++
- C++11新特性(三):語言特性C++
- Scala是世界上最好的語言(一):Type Bound
- 數學語言看世界
- Kotlin成為正式的Android程式語言KotlinAndroid
- 沒學過C語言的代價C語言
- 使用 Kotlin 語言開發 NeoForge 模組Kotlin