JDK 23 功能預覽:JEP 482 彈性建構式主體設計

JEP 482_Flexible Constructor Bodies

Java 語言的演進一直在追求更好的程式設計體驗,而 JDK 23 中的 JEP 482 提出的彈性建構式主體(Flexible Constructor Bodies)功能,為開發者帶來了建構式設計上的重大突破。這項功能允許在明確的建構式(不論是父類別建構式或本身的其他建構式)調用之前加入程式語句(除了讀取正在建構中的實例變數之外),讓程式設計更加靈活且直覺。

這項改進特別著重於提升類別在方法被覆寫時的可靠性。透過允許在調用父類別建構函式之前初始化衍生類別的成員,確保衍生類別的狀態在父類別建構式執行時已經準備就緒,有效避免了許多潛在的程式錯誤。

前言

類別的建構式負責建立該類別的可用實例,並且確保該類別中宣告的欄位皆以正確地完成初始化。在傳統的 Java 程式設計中,建構式的第一個語句必須是對另一個建構函式的明確調用,例如呼叫父類別的建構式 super(...),或是該類別自身的其他建構式 this(...)

類別的建構式還負責確保若有子類別存在的情況下仍然保有正確性。假設 EmployeePerson 的子類別,每個 Employee 建構式都會隱式或顯式地呼叫某個 Person 建構式。這些建構式必須協同工作以確保實例的有效性:Employee 建構式負責 Employee 類別中的欄位,而 Person 建構式負責 Person 類別中的欄位。由於 Employee 建構式中的程式碼可能會引用由 Person 建構式初始化的欄位,因此 Person 建構式必須首先運行。

一般來說,建構式必須從上到下運行。意即,父類別中的建構式必須先運行,以確保該類別中宣告的欄位其有效性,然後子類別中的建構式才能接著依序運行。為了保證建構式能從上到下運行,Java 語言要求在建構式主體中的第一個語句必須是對另一個建構式的顯式調用。如果建構式主體中沒有出現顯式建構式調用,則編譯器會在建構式主體的第一個語句前插入 super()

另外,Java 還要求對於任何顯式建構式的呼叫,其參數都不能以任何方式使用正在建構中的實例。

這些限制雖然確保了類別初始化的順序性,但同時也帶來了許多應用上的不便之處。它們保證了新實例在建構過程中的可預測性,但卻過於嚴格,因為它們禁止了某些常見的程式設計模式。

建構函式的困境

最常見的問題是驗證參數的時間點,也就是當我們需要在調用父類別建構式之前驗證參數時。目前能夠使用的方式不僅降低了程式碼的可讀性,還增加了維護的複雜度。

舉例來說,假設 Person 類別的實例有一個 age 欄位,其值必須始終小於 130。一個接受與年齡相關參數(例如:出生日期)的建構式必須驗證該參數並將其寫入 age 欄位,從而確保數值正確,否則需要拋出異常。

有時我們需要驗證傳遞給父類別建構式的參數,但只能在呼叫父類別建構式後才能驗證參數,而這意味著可能會做了不必要的工作:

public class Employee extends Person {
    public Employee(int age) {
        super(age);                 // 可能不必要的工作
        if (age > 130) throw new IllegalArgumentException();
    }
}

更好的做法是宣告一個快速失敗的建構式,使得我們在呼叫父類別建構式之前能夠驗證參數。目前,我們只能透過在呼叫 super(..) 時內嵌驗證方法來實現這一點:

public class Employee extends Person {
    private static long verify(int age) {
        if (age > 130) throw new IllegalArgumentException();
        return age;
    }

    public Employee(int age) {
        super(verify(age));  // 內嵌驗證方法,有效但不直觀
    }
}

我們想要允許建構式主體中能夠在呼叫其他建構式前出現其他語句,只要它們並未使用到目前正在建構中的實例。遺憾的是即使所有這些語句是安全的,現今的編譯器依然會拒絕它們。

如果能夠有更具備彈性的規則以保證自上而下的建構順序,那麼建構式主體將更容易編寫和維護。建構式主體可以更自然地進行參數驗證、參數準備和參數共享,而無需使用笨拙的輔助方法或建構函式。

JEP 482 概觀

本功能允許 Java 類別的建構式中,在顯式地呼叫其他建構式(即 super(...)this(...))之前可以出現特定的語句,而這些語句不能存取正在建構中的實例,僅能初始化它的欄位。因為它允許將判斷邏輯放在呼叫父類別的建構式之前,從而避免了將某些檢查和初始化邏輯分解到靜態方法和中間建構式中的必要。例如:如果建構式可以在呼叫父類別建構式之前先行驗證參數,那麼當參數無效時就能夠拋出異常並避免不必要的父類別實例化。

另外,如果我們在呼叫另一個建構式之前先完成欄位初始化,可以提高類別在方法被覆寫時的可靠性。例如,在呼叫父類別建構式之前先初始化子類別的成員,可以確保子類別的狀態在父類別建構式執行時已經準備就緒,可以避免潛在的錯誤。尤其在方法覆寫的情況下,能確保子類別的狀態在父類別方法執行前已經正確初始化。

它讓 Java 建構式更加靈活,允許在調用其他建構式之前先執行初始化操作,從而提高程式碼的可靠性和可維護性。然而,開發人員在使用此功能時需要注意其潛在的複雜性和錯誤風險,並確保相容現有的程式碼。

優點

  • 提供更直覺的參數驗證方式,無需依賴輔助方法
  • 能夠提高可靠性,尤其是能確保子類別的欄位在父類別建構式執行前已完成初始化
  • 將欄位初始化邏輯集中在建構函式開頭,使程式碼結構更清晰,易於閱讀和維護

缺點

  • 在建構式中引入額外的語句可能會增加複雜度,尤其對於大型或複雜的類別
  • 可能暫時影響現有的程式碼分析工具和 IDE 支援
  • 可能會對現有程式碼產生一些影響,尤其是在處理複雜的繼承關係時,需要更謹慎地設計

深入解析靈活建構函式主體

老喬直接以上面提到的 Employee 程式碼為例,以本功能語句的方式來重新改寫。我們可以看到整個程式碼變得簡潔易維護:

public class Employee extends Person {
    public Employee(int age) {
        if (age > 130) throw new IllegalArgumentException();
        super(age);
    }
}

出現在顯式建構式呼叫之前的語句構成建構式主體的「序言」;出現在顯式建構式呼叫之後的語句構成建構式主體的「結語」。

public class Employee extends Person {
    public Employee(int age) {  // 建構式主體
        if (age > 130) throw new IllegalArgumentException();  // 序言
        super(age);  // 顯式建構式呼叫
        System.out.println("Age: " + getAge());  // 結語
    }
}

建構式主體中的顯式建構式呼叫可以省略。在這種情況下,序言為空,建構函式主體中的所有語句構成結語。

public class Employee extends Person {
    public Employee() {  // 建構式主體
        // 序言:無
        // 顯式建構式呼叫:省略。預設呼叫 super()
        System.out.println("DONE.");  // 結語:建構式主體中的所有語句皆為結語
    }
}

如果 return 語句不包含表示式,則允許在建構式主體的結語中使用。也就是說,允許 return;,但不允許 return e;return 語句出現在建構式主體的序言時會發生編譯時期錯誤。

允許在建構式主體的序言或結語中拋出異常,尤其是為了快速失敗的情況而在序言中拋出異常是很常見的例子。

早期建構式上下文

在 Java 語言中,呼叫顯式建構式時的參數程式碼被認為是靜態上下文。也就是說,顯式建構式的傳入參數被視作如同靜態方法中的程式碼,此時並沒有實例可以拿來使用。這種對於靜態上下文所附加的技術限制過於強烈,使得它們阻止了有用且安全的程式碼成為建構式參數。

本功能並未修改原本的靜態上下文概念,而是引入早期建構上下文(early construction context)的概念。它同時涵蓋了原先顯式建構式呼叫的參數清單,以及在建構式主體中出現在它之前的任何語句(也就是序言)。早期建構上下文中的程式碼不得使用正在建構中的實例,除非是為了初始化沒有自己初始化器的欄位。

這意味著在早期建構上下文中不允許任何顯式或隱式使用 this 來引用當前實例,或存取當前實例的欄位或呼叫當前實例的方法:

class A {
    int i;

    A() {
        System.out.print(this);  // 錯誤 - 引用當前實例

        var x = this.i;          // 錯誤 - 顯式引用當前實例的欄位
        this.hashCode();         // 錯誤 - 顯式引用當前實例的方法

        var x = i;               // 錯誤 - 隱式引用當前實例的欄位
        hashCode();              // 錯誤 - 隱式引用當前實例的方法

        super();
    }
}

同樣地,在早期建構上下文中不允許任何由 super 限定的欄位存取、方法呼叫或方法引用:

class B {
    int i;
    void m() { ... }
}

class C extends B {
    C() {
        var x = super.i;         // 錯誤
        super.m();               // Error
        super();
    }
}

新的語法允許在呼叫其他建構式之前執行程式碼,但這些程式碼必須遵守「早期建構上下文」的規則:不能引用正在建構中的實例,除非是為了初始化沒有自己初始化器的欄位。

其他更多早期建構上下文的介紹,會等此功能正式上線後一併說明。

總結

JEP 482 讓 Java 建構式更加靈活,允許在呼叫其他建構式之前執行一些初始化操作,從而提高程式碼的可靠性和可維護性。它不僅簡化了常見的程式設計模式,還提供了更好的錯誤處理機制。這項改進特別有助於處理繼承關係中的初始化順序問題,讓開發者能夠寫出更可靠、更容易維護的程式碼。

雖然這項變更需要工具生態系統一定時間的適應,但它代表了 Java 語言在改善開發者體驗方面的重要一步。隨著虛擬執行緒等新特性的引入,這種靈活性將在未來的 Java 應用程式開發中發揮更大的價值。開發者在使用此功能時需要注意其潛在的複雜性和錯誤風險,並確保與現有程式碼的相容性。

本篇文章的內容為老喬原創、二創或翻譯而來。雖已善盡校對、順稿與查核義務,但人非聖賢,多少仍會有疏漏之處難以避免。如果大家有任何問題、建議或指教,都歡迎在底下留言與老喬討論!

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

two × 5 =

返回頂端