Categories
程式開發

剖析Java15新語法特性


前言

9月15日,Java社區正式發布了Java15的GA版本,這意味著大家欠Oracle的技術債開始變得越來越多。在之前的新時代背景下的Java語法特性“一文中,我為大家詳細介紹了Java9~14的語法全貌;而本文,我會緊接上一篇博文的內容,繼續為大家剖析Java15帶給我們的改變。

進一步強化的instanceof運算符

早期,如果我們需要在程序中對某個引用類型進行強制類型轉換,通常情況下,為了避免在轉換過程中出現類型不匹配的轉換錯誤,我們往往會使用instanceof運算符驗證目標對像是否是特定類型的一個實例,只有當表達式條件成立時,才允許進行強制類型轉換,示例1-1:

if (obj instanceof String) {
String temp = (String) obj;
} else {
//...
}

上述程序示例中,整個轉換過程總共需要經歷2個步驟,首先需要使用instanceof關鍵字來匹配目標對象,當條件成立後,再使用一個新的局部變量來接收強轉後的值。平心而論,這樣的寫法著實有點冗餘,因此,從Java14開始,Java的設計者們開始對instanceof運算符進行了進一步的升級強化,為開發人員提供了模式匹配功能,以便於簡化樣板代碼的編寫。示例1-2:

if (obj instanceof String str) {
//TODO 变量str的作用域仅限if代码块
} else {
//...
}

很明顯,使用instanceof的模式匹配功能後,弱化了原本代碼樣板化嚴重的問題。 instanceof關鍵字的右邊允許開發人員直接聲明變量,當滿足條件後,編譯器會隱式強轉並為其賦值。在上一篇博文中,我之所以沒有為大家分享這項新特性,是因為它當時還僅僅只是個預覽功能;儘管Java15它仍未轉正,但從JEP提案中可以明確得知,這項語法特性在未來發生改動的可能性較小。

擴展限制支持

在談擴展限制之前,我們首先回顧下Java訪問修飾符是如何控制資源的訪問權限的。當資源被聲明為public時,意味著資源對所有類可見;當資源被聲明為protected時,意味著僅同包類,或派生類可見;當資源被聲明為default時,意味著僅同包類可見;而當資源被聲明為private時,僅同類可見。

在某些情況下,如果我們希望所定義的超類具備擴展限制時,通常會採用如下2種方式:

將超類定義為final;將超類的構造函數聲明為default。

如果我們將超類定義為final後,那麼任何類都不允許對其進行繼承,也就是說,超類將不提供任何擴展支持,很明顯,這樣的做法顯然無法有效支撐某些特定場景(比如:我們希望超類僅允許被固定的幾個子類擴展)。而如果將超類的構造函數聲明為default時,儘管在一定範圍內可以限制超類的擴展權限(同包類可見),但如果其它包中的子類也需要對其進行擴展時則顯得無能為力。因此,在Java15誕生之前,僅憑現有的技術手段均無法有效的為開發人員提供一種聲明式的方式來合理限制超類對子類的擴展能力。

值得慶幸的是,Java15的到來很好的滿足了上述需求,為開發人員在語法層面上提供了sealed關鍵字來支持這項特性,示例1-3:

public sealed interface Animal permits Tiger, Lion {}

final class Tiger implements Animal {}

final class Lion implements Animal {}

上述程序示例中,我們通過sealed關鍵字定義了一個具備擴展限制的超類,並在classname之後使用permits關鍵字來限制其擴展範圍;也就是說,只有在限制範圍內的固定子類才允許對超類進行擴展,否則編譯器將會出現編譯錯誤。當然,permits關鍵字並非是強制的,當我們所定義的子類為嵌套內部類時,編譯器會在編譯時進行自動類型推斷,示例1-4:

public sealed interface Animal{}

final class Tiger implements Animal {}

final class Lion implements Animal {}

相信大家也看見了,示例1-3和1-4中,子類均使用了final關鍵字來進行修飾,這是為何?實際上,這與sealed類的約束有關。我們使用sealed的初衷是希望對超類的擴展做出限制,在開發過程中不允許任何類都對其進行繼承或實現,因此對於sealed類的子類而言,就需要追加3個約束。首先是超類和子類必須被在同包內,如果要解除這個限制,就必須被包含在同一模塊中,示例1-5:

module name {}

其次,permits關鍵字包含的子類都必須顯示extends或者implements。最後,子類都必須定義一個特定的修飾符來描述是否延續超類的擴展限制;也就是說,子類也可以使用sealed、permits關鍵字來定義它的下級派生,並且也可以使用final關鍵字來禁止所有子類繼承於它,甚至還可以使用non-sealed關鍵字來解除所有限制(如果這麼做,擴展限制將失去意義)。具體怎麼定義,還需要結合具體的業務場景而定,示例1-6:

public sealed interface Animal{}

/**
* 子类也可以定义sealed来延续超类的扩展限制
*/
sealed class Tiger implements Animal permits NortheastTiger {}
final class NortheastTiger extends Tiger{}

/**
* non-sealed解除所有限制
*/
non-sealed class Lion implements Animal {}
class PumaConcolor extends Lion{}

進一步強化的String類型

早在Java13的時候,Java就已經開始支持多行字符串的文本塊語法定義,儘管在當時這項語法特性還僅僅只是個預覽功能,但也足以讓大家期待;而隨著Java15的正式來臨,多行文本塊已經被JDK正式支持。關於多行文本塊的具體使用,大家可以參考我的上一篇“弘文。

數據載體類支持

實際上,這項語法特性也是為了給大家“減負”使用的,目的就是為了減少樣板代碼的編寫。在實際開發過程中,我們往往會在業務代碼中定義非常多的POJO類,比如:Controller和Service之間的DTO對象、持久層的PO對像等。但是這類POJO類型本身並不會處理複雜的業務邏輯,也不會包含任何行為,其作用純粹就是作為一個數據載體,以便於數據的傳輸或訪問處理;但我們仍然每天都需要不勝其煩的編寫一大堆樣板代碼,比如:setter、getter、hashCode,以及equals等。在此大家需要注意,如果一個POJO類中存在較多的字段,會嚴重影響其維護性和可讀性,示例1-7:

public class UserEntity {
private int id;
private String account, pwd;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof UserEntity)) return false;
UserEntity that = (UserEntity) o;
return getId() == that.getId() &&
Objects.equals(getAccount(), that.getAccount()) &&
Objects.equals(getPwd(), that.getPwd());
}

@Override
public int hashCode() {
return Objects.hash(getId(), getAccount(), getPwd());
}
//省略setter/getter方法
}

為了減少樣板代碼的編寫,Java的設計者們為開發人員提供了record關鍵字,專用於定義不可變的數據載體類,示例1-8:

public record UserEntity(int id, String account, String pwd) {}

上述程序示例中,我們僅通過一行代碼即可完成一個POJO類的定義,大幅減少了樣板代碼的編寫。通過record關鍵字定義的POJO類在對其進行反編譯後我們不難發現,編譯器在編譯時,仍然會將其編譯為一個標準的Java類型,相當於隱式幫我們實現了Lombok的功能,示例1-9:

public final class UserEntity extends java.lang.Record {
private final int id;
private final java.lang.String account;
private final java.lang.String pwd;

public UserEntity(int id, java.lang.String account, java.lang.String pwd) { /* compiled code */ }
public final java.lang.String toString() { /* compiled code */ }
public final int hashCode() { /* compiled code */ }
public final boolean equals(java.lang.Object o) { /* compiled code */ }
public int id() { /* compiled code */ }
public java.lang.String account() { /* compiled code */ }
public java.lang.String pwd() { /* compiled code */ }
}

上述程序示例中,所有的字段都被聲明為了final,這也說明,record類是專用於定義不可變數據。在此大家需要注意,在使用record類時,有幾點是必須注意的,如下所示:

record類中不允許定義實例字段,但允許定義靜態字段;record類中允許定義靜態方法和實例方法,以便於實現一些特定的數據加工任務;record類中允許定義構造函數。

或許大家存在疑問,早期我們在定義POJO類時,如果遇到有很多可選參數時,往往會採用重載構造函數的方式來解決,但如果使用record類後,我們應該如何解決這個問題?實際上,既然record類允許我們定義構造函數,那這就意味著同樣可以通過相似的技術手段來解決共性問題,但實際開發過程中,我更建議大家使用Builder模式,示例1-10:

public record UserEntity(int id, String account, String pwd) {
private UserEntity(Builder builder) {
this(builder.id, builder.account, builder.pwd);
}

static class Builder {
int id;
String account, pwd;

Builder(String account, String pwd) {
this.account = account;
this.pwd = pwd;
}
Builder id(int id) {
this.id = id;
return this;
}
public UserEntity build() {
return new UserEntity(this);
}
}

public static void main(String[] args) {
var userEntity = new Builder("gxl", "123456").id(100).build();
System.out.println(userEntity.toString());
}
}

至此,本文內容全部結束。如果在閱讀過程中有疑問,歡迎加入微信群聊和小伙伴們一起參與討論。

剖析Java15新語法特性 1

碼字不易,歡迎轉發