Java語言從1995年發表至今2022年已經27個年頭,有些舊專案可能還停留在1.6版,比較近代一點的也可能停留在8或11這兩個LTS版。這些年間光是switch關鍵字的功能就進化許多次,像是加入的了Switch陳述句(Switch Expressions)、箭頭回傳值、多重數值的case(Multi-Constant Case)、新的關鍵字yield等,本文將介紹switch多了哪些方便好用的新功能。

Java 7

Switch with String case

在Java 1.7之前switch只能帶入整數或字元,如果帶入String會出現類似這樣錯誤訊息:

test/App.java:27: incompatible types
found   : java.lang.String
required: int
        switch (url) {
                ^
1 error

從Java 1.7開始才能使用String,例如以下範例:

String str = "klab.tw";
switch(str) {
    case "klab.tw": return "Blog Home";
    case "google.com": return "Search Engine";
    default: return "UNKNOW";
}

原理上Java會使用呼叫String.hashCode()將switch參數轉換成int型態,因此switch與case判斷的依然是整數,但是Hash Code有可能會重複,因此編譯器會自動在case內部使用if判斷式搭配String.equals(Object)再判斷一次是否一致,詳細可以參考這篇問答:How switch case with string(come in java 1.7) work internally?

順道一提enum關鍵字產生的列舉型態帶入switch的時候也會呼叫Enum.ordinal()轉換成整數型態做比較,因此switch語句一直都是接受整數(char也可以看作是一種整數型態)。也因此傳給switch的String或Enum都不可以是null,Java無法對null呼叫hashCode()ordinal()等方法,用default:來處理也是沒用的,因為程式還來不及執行到default就會先拋出NullPointerException。switch會有這些限制是因為要在編譯期間進行優化,執行的時候switch會比if-else的效率來得高。

Java 12到14

從Java 12開始推出許多Switch的預覽功能,到Java 14才從預覽版變成正式版。預覽版代表這些功能以後可能還會修改,甚至移除。正式版代表這些功能之後會穩定存在,可以放心使用。

為了方便後續使用程式碼舉例,我們先假設有以下這個enum。

public enum EUrl {
    HOME, GOOGLE, BING, OTHER;
}

Statements跟Expressions

先簡單講一下Statements跟Expressions的差別,Statements常見的翻譯為「語句」,是一段用來定義、控制、改變程式的狀態的程式碼。而Expressions常見的翻譯是「陳述句」,是一段有回傳值的程式碼。例如int x = 1 + 2 * 3;是一個Statement,它宣告有一個int變數在記憶體中,並且指定了裡面的值,而且沒有任何回傳值。而等號右邊的1 + 2 * 3是Expression,可以是四則運算、邏輯運算、有回傳值的函數呼叫,最後變成一個值回傳到最左邊。可以參考:https://andyyou.github.io/2016/03/06/expressions-vs-statements-in-js/,雖然是以JavaScript為例講解,但是Statements跟Expressions的基本概念是通用的。

Switch Expressions

原本switch是一段Statement,switch會接受一段Expression,例如switch(num)switch(1 + 1)switch(getNumber())等,然後經過case判斷後進行一些控制與操作,不會有回傳值。儘管在case內使用了return關鍵字可以產生類似回傳值的效果,但其實那依然是一個控制行為,代表退出目前所在的method,沒辦法留在method內。

從Java 12開始發展了Switch Expressions,然後在Java 14以正式版推出此功能,讓switch可以成為Expression有返回值,例如以下範例。

String url = switch(eurl) {
    case HOME -> "klab.tw";
    case GOOGLE -> "google.com";
    case BING -> "bing.com";
    default -> "UNKNOW";
};
// 注意,最後要加上分號

從範例中可以看見使用case 條件 -> 返回值;,直接將值返回給switch語句前面的變數。以前想要類似功能會使用case 條件: return 返回值;,但是這樣會直接離開Method,只能將switch另外包裝在一個Method內呼叫,在不需要重複使用這段程式碼的情況下多此一舉。

在Visual Studio Code使用低於Java 14的版本使用以上Code會出現「Switch Expressions are supported from Java 14 onwards only Java(1073743545) 」的錯誤提示。

新的關鍵字yield與Case Block

Switch Expression讓程式碼更加簡潔,但有時候我們還是需要一些Statement在case裡面,因此可以加上大括號包起來,要回傳的數值就使用yield關鍵字。例如我們修改上一則範例,在default裡面加上一個System.err的輸出,然後才回傳UNKNOW。

String url = switch(eurl) {
    case HOME -> "klab.tw";
    case GOOGLE -> "google.com";
    case BING -> "bing.com";
    default -> {
        System.err.println("Unknow Type");
        if(defaultUrl != null) {
            yield defaultUrl;
        } else {
            yield "UNKNOW";
        }
    }
};

在Java 12時基於JEP 325出現一個預覽版的寫法是break VALUE;,但是Java 13基於JEP 354移除掉此寫法,改成上面範例中的新關鍵字yield VALUE;了,原因是break後面還有可能接上label名稱,容易造成混淆。可以參考這篇問答:What does the new keyword “yield” mean in Java 13?。關鍵字yield在Java 14已經變成正式功能了,可以放心使用。

Multi-Constant Case

這個功能是讓一個case後面接好幾個值,例如case 1, 2, 3:或是case 1, 2, 3 ->,從Java 12開始加入預覽版,在Java 14成為正式功能。例如以下範例。

String type = switch(eurl) {
    case HOME -> "My Blog https://klab.tw/";
    case GOOGLE, BING -> "Search Engine";
    default -> {
        System.err.println("Unknow Type");
        yield "UNKNOW";
    }
};

在Visual Studio Code使用低於Java 14的版本使用以上Code會出現「Multi-constant case labels supported from Java 14 onwards only Java(1073743543) 」的錯誤提示。

有些文章稱呼為Multi-values case,但這樣聽起來好像可以是變數,可是switch的case不接受變數型態,這會讓編譯器無法進行編譯時的優化,因此Multi-constant的稱呼比較符合。

在Java這類預先編譯過的語言中switch的效率比if-eise來得高,限制是switch的值必須在編譯階段就確定,因此判斷條件是不確定的變數的情況下只能使用if-eise的方式來達成。另外Java編譯switch的時候會依照每個case的數值的稀疏程度編譯出兩種不同類型的switch,其中case的數值接近產生的緊湊型效率會更高。也因此switch的case不但是常數,還必須是整數,有機會來討論這點。

Java 17到20

Java 17是一個LTS版,從這一代開始switch關鍵字開始出現模式匹配功能,但在這一版的新功能是預覽階段,還不能正式使用。而且17、18、19、20連續四代都有一個switch JEP預覽,所以以下篇幅介紹的都是從Java 17開始的預覽功能,希望在Java 21能正式使用囉!

接受null case(預覽)

前面有提到switch關鍵字是會在編譯期間優化的,而且全部都會轉為整數,因此沒辦法接受null值,這次在case中可以加入null的判斷了!我猜這個新功能是一個語法糖,本質上只是預先幫你判斷如果是null要做什麼,讓開發者可以當成眾多case之一直接寫進switch區塊內。

Eurl eurl = null;
String type = switch(eurl) {
    case null -> {
        System.err.println("Is Null!!");
        yield "UNKNOW";
    }
    case HOME -> "My Blog https://klab.tw/";
    case GOOGLE, BING -> "Search Engine";
    default -> {
        System.err.println("Unknow Type");
        yield "UNKNOW";
    }
}

Pattern Matching 模式匹配(預覽)

這個功能要先提到Java 16的正式新功能,JEP 394: Pattern Matching for instanceof,是讓開發者減少程式碼的語法糖,先直接看以下範例。

// Old code
if(obj instanceof Integer) {
    Integer num = (Integer) obj;
    System.out.printf("%d", num);
} else if(obj instanceof Double) {
    Double num = (Double) obj;
    System.out.printf("%f", num);
}

// Java 16 Pattern Matching
if(obj instanceof Integer num) {
    System.out.printf("%d", num);
} else if(obj instanceof Double num) {
    System.out.printf("%f", num);
}

可以看出新版的code可以簡化很多,所以switch關鍵字也要新增這樣的功能,也就是預覽了四代的Pattern Matching for switch,目前來說會長這樣。

String str = switch(obj) {
    case Integer n -> String.format("int %d", n);
    case Double n  -> String.format("double %f", n);
    default        -> o.toString();
}

我有一些專案也要因應不同類型的變數做不同的回應,能早點有這些Pattern Matching寫起來就會輕鬆多了呢。

新的關鍵字when(預覽)

這也是語法糖,如果在switch case比對到又要加上if關鍵字做更多判斷,會讓程式碼變成好多層,新的when關鍵字可以讓程式碼更加優雅。

// No `when` keyword
String str = switch(obj) {
    case Integer n -> {
        if(n >= 100){
            yield String.format("int %06d", n);
        } else {
            yield String.format("int %03d", n);
        }
    }
}

// Use `when` keyword
String str = switch(obj) {
    case Integer n when n >= 100 -> String.format("int %06d", n);
    case Integer n               -> String.format("int %03d", n);
}

但目前看起來如果有多個when的話,似乎也要寫多個Integer n,希望之後可以更加優雅。

更多預覽功能

可以上OpenJDK看JEP頁面介紹,這邊整理了四篇JEP,從Java 17到Java 20每一代都有一個預覽功能說明。

發佈留言