JEP 513 : Flexible Constructor Bodies

blog1.mammb.com


はじめに

JDK 25 では、コンストラクタ本体は明示的にせよ暗黙的にせよ、super(...) または this(...) で始まらなければならない、という構文ルールが削除されます。

以前は以下のようなコンストラクタ呼び出しは構文エラーでした。

class Employee extends Person {

    Employee(String name, int age) {
        if (age < 18 || age > 67)
            throw new IllegalArgumentException();
        super(name, age);
    }

}

JDK 25 からは、明示的なコンストラクタ呼び出しの前に、前処理を実行したりフィールドを初期化することができるようになりました。

このJEPは、以下のプレビュー公開を経て、JDK 25で正式公開となりました。

  • JDK 22 - JEP 447: Statements before super(...) (Preview)
  • JDK 23 - JEP 482: Flexible Constructor Bodies (Second Preview)
  • JDK 24 - JEP 492: Flexible Constructor Bodies (Third Preview)


JDK 25 前のコンストラクタ

Java言語では、コンストラクタをトップダウンで実行することで、有効なインスタンスの構築を保証しています。

例えば、以下のクラスがあった場合

class Person { }
class Employee extends Person { }

Employee コンストラクタのコードは Person クラスで宣言されたフィールドを参照できるため、Employee コンストラクタがこれらのフィールドにアクセスできるのは、Person コンストラクタがフィールドへの値の割り当てを完了した後でなければなりません。 そのため、コンストラクタの最初のステートメントがコンストラクタ呼び出し、つまりsuper(...)またはthis(...)であることが要求されます。

例えば、年齢制限のある Employee のコンストラクタでは、引数を検証はPerson クラスのコンストラクタ呼び出しの後に行わなければなりません。

class Person {
    String name;
    int age;
    Person(String name, int age) {
        if (age < 0)
            throw new IllegalArgumentException();
        this.name = name;
        this.age = age;
    }

}

class Employee extends Person {

    Employee(String name, int age) {
        super(name, age);
        if (age < 18 || age > 67)
            throw new IllegalArgumentException();
    }
}

Person コンストラクタを呼び出す前に引数を検証するには、コンストラクタ呼び出しの一部として補助メソッドをインラインで呼び出すしかありません。

class Employee extends Person {

    private static int verifyAge(int value) {
        if (age < 18 || age > 67)
            throw new IllegalArgumentException();
        return value;
    }

    Employee(String name, int age) {
        super(name, verifyAge(age));
    }
}

スーパークラスのコンストラクタが常にサブクラスのコンストラクタの前に実行されるようにするトップダウン・ルールは、新しいインスタンス全体の完全性を保証するには不十分です。

スーパークラスのコンストラクタは、サブクラスのコンストラクタがフィールドを初期化する前に、間接的にサブクラスのフィールドにアクセスすることができます。

例えば、Employee クラスに officeID フィールドがあり、Person のコンストラクタが Employee でオーバーライドされるメソッドを呼び出すとします。

class Person {
    int age;
    void show() {
        System.out.println("Age: " + this.age);
    }

    Person(int age) {
        if (age < 0)
            throw new IllegalArgumentException(...);
        this.age = age;
        show();
    }

}

class Employee extends Person {

    String officeID;

    @Override
    void show() {
        System.out.println("Age: " + this.age);
        System.out.println("Office: " + this.officeID);
    }

    Employee(int age, String officeID) {
        super(age);
        if (age < 18 || age > 67)
            throw new IllegalArgumentException();
        this.officeID = officeID;
    }

}

Person コンストラクタが show() メソッドを呼び出した段階では、officeID フィールドが初期化されていないため、officeID フィールドの既定値である null が出力されてしまいます。 この例のように、スーパークラスのコンストラクタが、サブクラスのコンストラクタによって値が割り当てられる前に、サブクラスのフィールドにアクセスする別のメソッドに現在のインスタンスを渡すことを止めるものは何もありません。


Flexible Constructor Bodies

JDK 25 からは、コンストラクタ本体で明示的コンストラクタ呼び出しの前にステートメントを書くことができます。

class Employee extends Person {

    String officeID;

    Employee(int age, String officeID) {
        if (age < 18  || age > 67)
            throw new IllegalArgumentException();
        this.officeID = officeID;
        super(..., age);
    }
}

明示的コンストラクタ呼び出しの前には、初期構築コンテキスト(Early construction contexts)コードを書くことができます。

初期構築コンテキストのコードは、明示的または暗黙的に現在のインスタンスを参照したり、フィールドにアクセスしたり、現在のインスタンスのメソッドを呼び出したりすることはできません。 このルールの唯一の例外は、同じクラスで宣言されたフィールドに単純な代入文を使用してもよいということです。

class X {

    int i;
    String s = "hello";

    X() {

        System.out.print(this); // Error - explicitly refers to the current instance

        var x = this.i;         // Error - explicitly refers to field of the current instance
        this.hashCode();        // Error - explicitly refers to method of the current instance

        var y = i;              // Error - implicitly refers to field of the current instance
        hashCode();             // Error - implicitly refers to method of the current instance

        i = 42;                 // OK - assignment to an uninitialized declared field

        s = "goodbye";          // Error - assignment to an initialized declared field

        super();

    }

}

さらなる制限は、初期構築コンテキストのコードは、スーパークラスのフィールドにアクセスしたりメソッドを呼び出したりするために super を使用することができません。

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

class Z extends Y {

    Z() {
        var x = super.i;         // Error
        super.m();               // Error
        super();
    }

}

ネストされたクラスの初期構築コンテキストでは、内包インスタンスに対する操作が許可されます。

以下のコードでは、Inner の宣言は Outer の宣言の中に入れ子になっているので、Inner のすべてのインスタンスは Outer のインスタンスを囲んでいます。 Inner のコンストラクタでは、初期構築コンテキストのコードは、単純な名前またはOuter.thisを介して、包含インスタンスとそのメンバを参照できます。

class Outer {

    int i;

    void hello() { System.out.println("Hello"); }

    class Inner {

        int j;

        Inner() {
            var x = i;            // OK - implicitly refers to field of enclosing instance
            var y = Outer.this.i; // OK - explicitly refers to field of enclosing instance
            hello();              // OK - implicitly refers to method of enclosing instance
            Outer.this.hello();   // OK - explicitly refers to method of enclosing instance
            super();
        }
    }
}

対照的に、以下に示す Outer のコンストラクタでは、初期構築コンテキストのコードは new Inner()Inner クラスをインスタンス化できません。 この式は実際には this.new Inner() であり、Inner インスタンスを囲むインスタンスとして Outer の現在のインスタンスを使用することを意味します。 先の規則に従って、早期構築コンテキストのコードでは、明示的にも暗黙的にも、現在のインスタンスを参照するために this を使用することはできません。

class Outer {

    class Inner {}

    Outer() {
        var x = new Inner();      // Error - implicitly refers to the current instance of Outer
        var y = this.new Inner(); // Error - explicitly refers to the current instance of Outer
        super();
    }

}