要不要升級(jí)?Java 21強(qiáng)大的新特性,代碼量減半
1. record模式
Record模式由 JEP 405 作為預(yù)覽功能提出,并在 JDK 19 中發(fā)布,JEP 432 進(jìn)行了第二次預(yù)覽,并在 JDK 20 中發(fā)布。該功能與用于switch的模式匹配(JEP 441)共同發(fā)展,兩者之間有相當(dāng)多的交互
1.1 instanceof類型模式
Object obj = "Pack" ;
// Java 16之前
if (obj instanceof String) {
String s = (String) obj ;
System.out.println("強(qiáng)轉(zhuǎn)為String") ;
}
// 自Java 16起
if (obj instanceof String s) {
System.out.println("簡(jiǎn)便多了") ;
}
在上面的代碼中從java16開始,運(yùn)行時(shí)obj的值是String的實(shí)例,則obj與類型模式String s匹配。如果模式匹配,則表達(dá)式的實(shí)例為true,并且模式變量s初始化為obj轉(zhuǎn)換為String的值,然后可以在包含的代碼塊中使用該值。
1.2 模式匹配與Records
Records (JEP 395)是數(shù)據(jù)的透明載體。接收record類實(shí)例的代碼通常將使用內(nèi)置的組件訪問器方法提取數(shù)據(jù),稱為組件。例如,我們可以使用類型模式來測(cè)試值是否是record類Point的實(shí)例,如果是,則從值中提取x和y組件:
// 自Java 16起
public record Point(int x, int y) {
}
public static void main(String[] args) {
Object obj = new Point(10, 20);
if (obj instanceof Point p) {
int x = p.x();
int y = p.y();
System.out.println(x + y);
}
}
上面的代碼看著與1.1中介紹的沒撒區(qū)別就是類型模式,在上面的代碼中我們僅僅是訪問了record類x與y的方法,如果是這樣我們還可以像下面這樣操作:
Object obj = new Point(10, 20) ;
// 自java 21起
if (obj instanceof Point(int x, int y)) {
System.out.println(x + y) ;
}
這里的Point(int x, int y) 是一個(gè)record模式。它將提取組件的局部變量聲明移至模式本身,并在值與模式匹配時(shí)通過調(diào)用訪問器方法初始化這些變量。
1.3 嵌套record模式
有如下定義
public record Point(int x, int y) {}
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) {}
record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}
如果要提取左上角點(diǎn)的顏色,我們可以這樣寫:
Object r = new Rectangle(
new ColoredPoint(new Point(0, 0), Color.RED),
new ColoredPoint(new Point(100, 100), Color.BLUE)
) ;
// 從java 21起
if (r instanceof Rectangle(ColoredPoint ul, ColoredPoint lr)) {
System.out.printf("%s, %s%n", ul, lr) ;
}
輸出結(jié)果
ColoredPoint[p=Point[x=0, y=0], c=RED], ColoredPoint[p=Point[x=100, y=100], c=BLUE]
如果你希望訪問具體的顏色值,record模式還支持嵌套,如下示例:
// 從java 21起
if (r instanceof Rectangle(
ColoredPoint(Point(int x, int y), Color c1),
ColoredPoint lr
)
) {
System.out.printf("x = %d, y = %d%n", x, y) ;
}
1.4 嵌套模式無法匹配情況
在下面這情況下是無法進(jìn)行匹配的
public record Pair(Object x, Object y) {}
Pair p = new Pair(42, 42);
if (p instanceof Pair(String s, String t)) {
System.out.println(s + ", " + t);
} else {
System.out.println("Not a pair of strings") ;
}
以上是關(guān)于record 模式的所有內(nèi)容。
2. switch模式匹配
該功能最初由 JEP 406(JDK 17)提出,后經(jīng) JEP 420(JDK 18)、427(JDK 19)和 433(JDK 20)改進(jìn)。它與 "1. record模式 "功能(JEP 440)共同發(fā)展。
先來看下如下這段代碼
Object obj = 100L ;
if (obj instanceof Integer) {
Integer i = (Integer) obj ;
obj = String.format("int %d", i);
} else if (obj instanceof Long) {
Long l = (Long) obj ;
obj = String.format("long %d", l);
} else if (obj instanceof String) {
String s = (String) obj ;
obj = String.format("String %s", s);
}
有個(gè)instanceof 模式以后就可以簡(jiǎn)化這樣了
Object obj = 100L ;
if (obj instanceof Integer i) {
obj = String.format("int %d", i);
} else if (obj instanceof Long l) {
obj = String.format("long %d", l);
} else if (obj instanceof String s) {
obj = String.format("String %s", s);
}
System.out.printf("result obj = %s%n", obj) ;
注意:上面的代碼有2個(gè)問題
- 上面的代碼有如果沒有編譯器的作用,那么它的時(shí)間復(fù)雜度將是O(n)
- 隱藏了一個(gè)BUG,當(dāng)if,else沒有判斷到某個(gè)類型時(shí)可能會(huì)出現(xiàn)問題上面的代碼并沒有else,因?yàn)椴粡?qiáng)制所以當(dāng)判斷遺漏了某種類型時(shí)可能會(huì)給程序帶來潛在的問題。
從Java 21開始,我們可以如下處理上面的if.. else
var ret = switch (obj) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case String s -> String.format("String %s", s);
default -> obj.toString() ;
};
System.out.printf("result ret = %s%n", ret) ;
在過去我們知道如果switch的每個(gè)case沒有break或者return,那么它會(huì)穿透到下一個(gè)case直到遇到break或return。并且在傳統(tǒng)的switch中沒有default也是可以的。但是在上面的代碼中必須要有default子句。
2.1 switch與null值
傳統(tǒng)上,如果switch表達(dá)式值為空,switch 語句和表達(dá)式會(huì)拋出 NullPointerException,因此必須在 switch 之外進(jìn)行空判斷:
String s = null ;
switch (s) {
// 如果不清楚這里的語法,你應(yīng)該先看看java14對(duì)switch新語法的介紹
case "a", "b" -> System.out.println("a or b") ;
default -> System.out.println("defualt value") ;
}
控制臺(tái)輸出
圖片
在上面的代碼中在過去,我們要先對(duì)s進(jìn)行null的判斷,再進(jìn)行switch,否則有可能就會(huì)出現(xiàn)上面的錯(cuò)誤。修改如下:
if (s == null) {
return ;
}
switch (s) {
// TODO
}
以上代碼是Java 21之前,從Java 21起,我們可以如下:
switch (s) {
case null -> System.out.println("oops") ;
case "a", "b" -> System.out.println("a or b") ;
default -> System.out.println("defualt value") ;
}
無需單獨(dú)的if判斷是否為null情況。
2.2 switch條件判斷
在case中還可以添加if...else判斷
static void fn1(String resp) {
switch (resp) {
case String s -> {
if (s.equalsIgnoreCase("success"))
System.out.println("處理成功");
else if (s.equalsIgnoreCase("failure"))
System.err.println("處理失敗");
else
System.out.println("未知結(jié)果") ;
}
}
}
在case中是使用when子句
static void fn2(String resp) {
switch (resp) {
case null -> {}
case String s
when s.equalsIgnoreCase("success") -> {
System.out.println("處理成功");
}
case String s
when s.equalsIgnoreCase("failure") -> {
System.err.println("處理失敗");
}
case String s -> {
System.out.println("未知結(jié)果") ;
}
}
}
這樣,switch的可讀性就更強(qiáng)了。
2.3 switch與enum常量
在Java 21之前,switch的case表達(dá)式必須是枚舉類型,標(biāo)簽必須是枚舉常量的簡(jiǎn)單名稱,如下示例:
public enum Color { RED, BLUE, GREEN }
public static void fn1(Color c) {
switch (c) {
case RED, BLUE -> System.out.println("我喜歡的顏色") ;
case GREEN -> {
// TODO
}
default -> System.out.println("我討厭的顏色") ;
}
}
上面說的標(biāo)簽必須是枚舉常量的簡(jiǎn)單名稱什么意思呢?就是說在java21之前使用枚舉時(shí)的標(biāo)簽不能是下面這種寫法:
case Color.GREEN -> {}
而從Java 21起可以使用這種語法。
3. 虛擬線程
關(guān)于虛擬線程請(qǐng)查看這篇文章:
【技術(shù)革命】JDK21虛擬線程來襲,讓系統(tǒng)的吞吐量翻倍!
4. 字符串模版
注:這是一個(gè)預(yù)覽功能
編譯:javac --enable-preview --source 21 -Xlint:preview Xxx.java
運(yùn)行:java --enable-preview Xxx
在開發(fā)中字符串相關(guān)的操作是非常非常多的,雖然Java 提供了多種字符串組成機(jī)制,但遺憾的是,所有機(jī)制都有缺點(diǎn)。
- 使用+操作符拼接字符串,看著都不好理解
String result = x + " + " + y + " = " + (x + y) ;
- 冗余的StringBuilder
String s = new StringBuilder().append(x).append(" + ")
.append(y).append(" = ").append(x + y).toString() ;
- String#format 與 String#formatted將格式字符串與參數(shù)分離,避免了類型錯(cuò)配:
int x = 10, y = 20 ;
String s = String.format("%2$d + %1$d = %3$d", x, y, x + y);
String t = "%2$d + %1$d = %3$d".formatted(x, y, x + y) ;
- java.text.MessageFormat要求太多,而且格式字符串中使用了不熟悉的語法:
String ret = MessageFormat.format("{0} + {1} = {2}", x, y, x + y) ;
4.1 STR 模板處理器
STR 是 Java 平臺(tái)定義的模板處理器。它通過用表達(dá)式的(字符串化)值替換模板中的每個(gè)嵌入表達(dá)式來執(zhí)行字符串插值。
String firstName = "Bill" ;
String lastName = "Duck" ;
String fullName = STR."\{firstName} \{lastName}" ;
System.out.println(fullName) ;
輸出結(jié)果
Bill Duck
注:STR 是一個(gè)公共靜態(tài)最終字段,會(huì)自動(dòng)導(dǎo)入到每個(gè) Java 源文件中。
表達(dá)式還可以執(zhí)行相應(yīng)的操作,如下:
int x = 10, y = 20 ;
String result = STR."\{x} + \{y} = \{x + y}" ;
System.out.println(result) ;
// 10 + 20 = 30
表達(dá)式中還可以調(diào)用方法
static String getName() {
return "張三" ;
}
static record Req(String date, String time) {}
static void fn5() {
String s = STR."我的名字是 \{getName()} ";
System.out.println(s) ;
Req req = new Req("2000-01-01", "23:59:59") ;
String t = STR."Access at \{req.date} \{req.time}";
System.out.println(t) ;
}
輸出結(jié)果
我的名字是 張三
Access at 2000-01-01 23:59:59
多行模版字符串
static void fn6() {
String name = "張三";
String phone = "1899999999";
String address = "xxxooo";
String json = STR."""
{
"name": "\{name}",
"phone": "\{phone}",
"address": "\{address}"
}
""";
System.out.println(json);
}
輸出結(jié)果
{
"name": "張三",
"phone": "1899999999",
"address": "xxxooo"
}
以上是基于STR模版處理器的內(nèi)容,接下來介紹另外一個(gè)。
4.2 FMT 模板處理器
FMT 是 Java 平臺(tái)定義的另一種模板處理器。FMT 與 STR 類似,它執(zhí)行插值,但也解釋嵌入式表達(dá)式左側(cè)的格式規(guī)范。格式說明符與 java.util.Formatter 中定義的格式說明符相同。
record Rectangle(String name, double width, double height) {
double area() {
return width * height;
}
}
public static void main(String[] args) {
Rectangle[] zone = new Rectangle[] {
new Rectangle("Alfa", 17.8, 31.4),
new Rectangle("Bravo", 9.6, 12.4),
};
String s = FMT."""
Description Width Height Area
%-12s\{zone[0].name} %7.2f\{zone[0].width} %7.2f\{zone[0].height} %7.2f\{zone[0].area()}
%-12s\{zone[1].name} %7.2f\{zone[1].width} %7.2f\{zone[1].height} %7.2f\{zone[1].area()}
\{" ".repeat(28)} Total %7.2f\{zone[0].area() + zone[1].area() + zone[2].area()}
""";
System.out.println(s) ;
}
5. 序列集合
在Java21 之前的集合類中要獲取第一個(gè)和最后一個(gè)元素,不同的集合操作方式不同或者壓根就沒有對(duì)應(yīng)的方法。如下示例:
圖片
在說遍歷集合,正向時(shí)(從第一個(gè)到最后一個(gè))操作方法基本一致。但是反向時(shí)遍歷時(shí)每個(gè)集合就又不相同了。
在JDK21中提供了如下3個(gè)序列接口:
- SequencedCollection
public interface SequencedCollection<E> extends Collection<E> {
SequencedCollection<E> reversed() ;
default void addFirst(E e) ;
default void addLast(E e) ;
default E getFirst() ;
default E getLast() ;
default E removeFirst() ;
default E removeLast() ;
}
- SequencedSet
public interface SequencedSet<E> extends SequencedCollection<E>, Set<E> {
SequencedSet<E> reversed();
}
- SequencedMap
public interface SequencedMap<K, V> extends Map<K, V> {
SequencedMap<K, V> reversed() ;
default Map.Entry<K,V> firstEntry() ;
default Map.Entry<K,V> lastEntry() ;
default Map.Entry<K,V> pollFirstEntry() ;
default Map.Entry<K,V> pollLastEntry() ;
default V putFirst(K k, V v) ;
default V putLast(K k, V v) ;
// other
}
以上3個(gè)集合都提供了對(duì)應(yīng)的獲取第一個(gè)和最后一個(gè)元素的方法及集合反轉(zhuǎn)方法。上面定義的三個(gè)新接口與現(xiàn)有的集合類型層次結(jié)構(gòu)非常吻合,如下圖:
圖片
對(duì)現(xiàn)有的類和接口進(jìn)行了如下調(diào)整:
- List 現(xiàn)在將 SequencedCollection 作為其直接超接口、
- Deque 現(xiàn)在將 SequencedCollection 作為其直接超接口、
- LinkedHashSet 進(jìn)一步實(shí)現(xiàn)了 SequencedSet、
- SortedSet 現(xiàn)在將 SequencedSet 作為其直接超接口、
- LinkedHashMap 進(jìn)一步實(shí)現(xiàn)了 SequencedMap,而
- SortedMap 現(xiàn)在將 SequencedMap 作為其直接超接口。
6. 未命名模式&變量
注:這是一個(gè)預(yù)覽功能
先看下面這個(gè)示例
public record Point(int x, int y) {}
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) {}
record Rectangle(ColoredPoint cp) {}
Object obj = new Rectangle(
new ColoredPoint(new Point(10, 10), Color.RED)
) ;
if (obj instanceof Rectangle(ColoredPoint(Point(int x, int y), Color c))) {
System.out.printf("x = %d, y = %d%n", x, y) ;
}
在上面的if判斷中,對(duì)于Color c變量并沒有使用,從Java 21開始我們可以像下面這樣改寫:
if (obj instanceof Rectangle(ColoredPoint(Point(int x, int y), _))) {
System.out.printf("x = %d, y = %d%n", x, y) ;
}
使用一個(gè) "_" 下劃線代替即可。
未使用的變量
int[] arr = {1, 2, 3, 4, 5} ;
int total = 0 ;
for (var a : arr) {
total++ ;
}
在這個(gè)示例中,變量a并沒有使用,所以從Java 21開始可以改寫如下:
for (var _ : arr) {
total++ ;
}
對(duì)于這樣沒有使用的變量,我們可以用一個(gè) "_" 下劃線代替。其它示例:
try {
int a = 1 / 0 ;
} catch (Exception _) { // 這里沒有用到異常通過可以使用 _
}
注:我用的Eclipse沒法直接使用,我這里是通過記事本編寫,通過命令行編譯&運(yùn)行。
7. 未命名的類&Main方法
注:這是一個(gè)預(yù)覽功能
下面這個(gè)代碼是學(xué)習(xí)java的入門代碼
public class UnnamedClassAndMain {
public static void main(String[] args) {
System.out.println("Hello World!!!") ;
}
}
從Java 21開始,我們可以簡(jiǎn)化成如下形式了
public class UnnamedClassAndMain {
void main() {
System.out.println("Hello World!!!") ;
}
}
未命名的類
還是拿上面的程序演示,我們還可以繼續(xù)簡(jiǎn)化如下形式:
void main() {
System.out.println("Hello World!!!") ;
}
對(duì),文件中只有一個(gè)極簡(jiǎn)的方法,連類的聲明都沒有了。你甚至還可以如下,定義方法,方法調(diào)用
String name = "Pack" ;
String getName() {
return name ;
}
void main() {
System.out.println(getName()) ;
}
類文件直接定義方法,聲明變量。