LoginSignup
8
4

【Java22】super()をコンストラクタの先頭以外に書けるようになる【JEP 447】

Posted at
Java21
public class SuperClass{
	public SuperClass(long value) {
		/* なんか重い処理 */
	}
}

public class SubClass extends SuperClass{
	public SubClass(long value) {
		super(value);
		
		// super呼ぶ前に検証したいんじゃが
		if(value <= 0){
			throw new IllegalArgumentException("non-positive value");
		}
	}
}

これまでsuper()は必ずコンストラクタの一番最初に書かなければなりませんでした。
2024/03/19にリリースされたJava22において、ようやく次の書式が合法化されました。

Java22
public class SubClass extends SuperClass{
	public SubClass(long value) {
		// 先に検証できる
		if(value <= 0){
			throw new IllegalArgumentException("non-positive value");
		}
		super(value);
	}
}

ということで以下は該当のJEP、Statements before super(...)の紹介です。

JEP 447: Statements before super(...)

Summary

Javaのコンストラクタにおいて、インスタンスを参照しないステートメントは親クラスのコンストラクタを呼ぶ前に書けるようにします。

Goals

現在は仕方なくstaticイニシャライザなどに書いているロジックをより自然に配置できるようになり、開発者がコンストラクタをよりよく表現できるようになります。

コンストラクタ内の処理順は維持し、サブクラスのコンストラクタ内のコードがスーパークラスのコンストラクタ内の処理に干渉しないようにします。

Java仮想マシンには変更ありません。
JVMのコードの検証を行っている部分のみ影響します。

Motivation

あるクラスが親クラスを拡張する際、サブクラスはスーパークラスの機能を継承しつつ、個別のフィールドやメソッドを配置して機能を追加することができます。
サブクラスで宣言されたフィールドの初期値はスーパークラスで宣言された初期値に依存する可能性があるため、先にスーパークラスのフィールドを初期化することが推奨されます。
たとえばclass B exnteds Aがある場合、まずは明示されていないObjectのフィールドを最初に初期化し、次にAのフィールド、最後にBのフィールドを初期化します。

フィールドをこの順番で初期化するということはつまり、コンストラクタをトップダウンで実行する必要があるということです。
スーパークラスのコンストラクタは、サブクラスのコンストラクタが実行される前に、そのクラスで宣言されているフィールドの初期化を完了する必要があります。

クラスのフィールドが初期化される前にアクセスされないようにすることが重要であり、これはすなわちコンストラクタを制限する必要があることを意味します。
スーパークラスのコンストラクタが終了するまで、サブクラスのコンストラクタ内ではスーパークラスのフィールドにアクセスしてはなりません。
この動作を保証するために、Javaではサブクラスのコンストラクタ内ではスーパークラスのコンストラクタ呼び出しを必ず最初に記載しなければなりません。
コンストラクタがない場合は、コンパイラによって自動挿入されます。

これらの要件によってコンストラクタのトップダウン動作と初期化前のアクセス禁止を保証していますが、かわりに他のメソッドで使われるイディオムのいくつかが使用できなくなっています。
その例を次に示します。

Example: Validating superclass constructor arguments

スーパークラスのコンストラクタ引数の検証

スーパークラスのコンストラクタ引数を追加で検証したい場合があります。
事後に検証することもできますが、本質的に不要な処理が発生することを意味します。

public class PositiveBigInteger extends BigInteger {

    public PositiveBigInteger(long value) {
        super(value);  // 引数によっては実行しなくていい
        if (value <= 0)
            throw new IllegalArgumentException("non-positive value");
    }

}

現在は静的メソッドを用いてインライン実行するしかありません。

public class PositiveBigInteger extends BigInteger {

    public PositiveBigInteger(long value) {
        super(verifyPositive(value));
    }

    private static long verifyPositive(long value) {
        if (value <= 0)
            throw new IllegalArgumentException("non-positive value");
        return value;
    }

}

コンストラクタに直接書くことができれば、見通しがよくなります。

Java22
public class PositiveBigInteger extends BigInteger {

    public PositiveBigInteger(long value) {
        if (value <= 0)
            throw new IllegalArgumentException("non-positive value");
        super(value);
    }

}

Example: Preparing superclass constructor arguments

スーパークラスのコンストラクタ引数の準備

スーパークラスのコンストラクタ引数を計算で求めたい場合があります。

public class Sub extends Super {

    public Sub(Certificate certificate) {
        super(prepareByteArray(certificate));
    }

    // 補助メソッド
    private static byte[] prepareByteArray(Certificate certificate) { 
        var publicKey = certificate.getPublicKey();
        if (publicKey == null) 
            throw new IllegalArgumentException("null certificate");
        return switch (publicKey) {
            case RSAKey rsaKey -> ...
            case DSAPublicKey dsaKey -> ...
            ...
            default -> ...
        };
    }

}

スーパークラスのコンストラクタはバイト列を受け取りますが、サブクラスのコンストラクタは証明書を受け取ります。
スーパークラスのコンストラクタ呼び出しはコンストラクタの先頭に書かなければならないため、補助メソッドprepareByteArrayを使って引数を変換しています。

これを直接コンストラクタに書くことができれば、コードはさらに読みやすくなるでしょう。

Java22
public Sub(Certificate certificate) {
        var publicKey = certificate.getPublicKey();
        if (publicKey == null) 
            throw new IllegalArgumentException("null certificate");
        final byte[] byteArray = switch (publicKey) {
            case RSAKey rsaKey -> ...
            case DSAPublicKey dsaKey -> ...
            ...
            default -> ...
        };
        super(byteArray);
    }

Example: Sharing superclass constructor arguments

スーパークラスのコンストラクタ引数の共有

スーパークラスのコンストラクタ引数を共有したい場合、これを実現する唯一の方法は補助コンストラクタを介することです。

public class Super {

    public Super(F f1, F f2) {
        ...
    }

}

public class Sub extends Super {

    // 補助コンストラクタ
    private Sub(int i, F f) { 
        super(f, f);                // f共有
        ... i ...
    }

    public Sub(int i) {
        this(i, new F());
    }

}

コンストラクタ内で直接コピーすれば、補助コンストラクタの必要がなくなります。

Java22
public Sub(int i) {
        var f = new F();
        super(f, f);
        ... i ...
    }

Summary

これらの例では、明示的な親コンストラクタ呼び出しの前にコードが記載されています
これらはスーパークラスのフィールドにはアクセスしていないので安全であるにもかかわらず、現在はコンパイラで拒否されます。

より柔軟な方法で初期化前フィールドアクセス禁止を保証することができれば、コードの記述や保守がより容易になることでしょう。
いちいち不自然な補助メソッドや補助コンストラクタを介することなく、自然な書き方ができるようになります。
このためには、Java1.0から続いている「super(..) や this(..) は最初に書かなければならない。」「thisを使用しない」縛りを断ち切る必要があります。

Description

コンストラクタの構文(JLS §8.8.7を、以下のように改定します。

ConstructorBody:
    { [BlockStatements] }
    { [BlockStatements] ExplicitConstructorInvocation [BlockStatements] }

Pre-construction contexts

Javaの言語仕様では、コンストラクタ本体での明示的なコンストラクタ呼び出しの引数に現れるコードを、静的コンテキストにあるものだと分類しています。(JLS §8.1.3)
つすなわち、コンストラクタ呼び出しの引数は静的メソッド内にあるかのように扱われるということであり、言い換えると利用可能なインスタンスがないということです。
しかしながら静的コンテキストへの制限は必要以上に厳格であり、コンストラクタ引数に便利で安全なコードを渡すことも邪魔してしまいます。

ここでは静的コンテキストの仕様自体を見直すのではなく、明示的なコンストラクタ呼び出しの引数と、その前に記述するロジックをまとめた、構築前コンテキストという新しい概念を定義します。
構築前コンテキストは、構築中のインスタンスにアクセスできないこと以外は、通常のインスタンスメソッドと概ね同じです。

何が構築中のインスタンスへのアクセスに当てはまるのかについては、考えていたよりずっと難しいことがわかりました。
幾つかの例で考えてみましょう。

まず簡単な例として、構築前コンテキストはthis.にアクセスできません。

class A {

    int i;

    A() {
        this.i++;                   // Error
        this.hashCode();            // Error
        System.out.print(this);     // Error
        super();
    }

}

同じ理由で、スーパークラスのフィールド、メソッドを参照することもできません。

class D {
    int i;
}

class E extends D {

    E() {
        super.i++;                  // Error
        super();
    }

}

もちろんthis.super.を書かなくても同じです。

class A {

    int i;

    A() {
        i++;                        // Error
        hashCode();                 // Error
        super();
    }

}

厄介なことに、現在のインスタンスではなく、外側のインスタンスを参照している場合があります。

class B {

    int b;

    class C {

        int c;

        C() {
            B.this.b++;             // 許される
            C.this.c++;             // Error - same instance
            super();
        }

    }

}

Innerクラスから呼ばれているhello()は、Innerのインスタンスではなく外側のOuterのインスタンスであるため、呼び出しが許可されます。

class Outer {

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

    class Inner {

        Inner() {
            hello();                // 許される
            super();
        }

    }

}

上の例では、Outerは既に構築なのでアクセス可能ですが、Innerは構築中なのでアクセス不能です。
逆の状況も考えられます。

class Outer {

    class Inner {
    }

    Outer() {
        new Inner();                // Error - 'this' is enclosing instance
        super();
    }

}

Inner()のコンストラクタにOuter()のインスタンスを提供する必要がありますが、そちらはまだ構築中であるためアクセスできません。

returnは、super()の後かつ値が含まれない場合のみ許可されます。
すなわち、return;は許可されますがreturn e;は禁止です。
super()の前にreturnするとコンパイルエラーになります。

super()の前で例外をthrowすることは許可されます。
むしろ一般的なシナリオです。

構築前コンテキストは、インスタンス自体にはアクセスできませんが、インスタンスの型は参照可能です。

class A<T> extends B {

    A() {
        super(this);                // Error - refers to 'this'
    }

    A(List<?> list) {
        super((T)list.get(0));      // Allowed - refers to 'T' but not 'this'
    }

Records

レコードクラスのコンストラクタには、通常クラスより多くの制限が課されています。

・標準コンストラクタには、コンストラクタ呼び出しを書くことはできない。
・非標準コンストラクタは、冒頭で標準コンストラクタを呼び出す必要があり、スーパークラスのコンストラクタ呼び出しはできない。

恩恵を受ける点としては、非標準コンストラクタの冒頭で標準コンストラクタ呼び出しの前にステートメントを記載することができるようになります。
それ以外の制限は残ったままです。

Enums

Enumクラスのコンストラクタには、明示的な代替コンストラクタ呼び出しを記載することはできますが、スーパークラスのコンストラクタ呼び出しはできません。
恩恵を受ける点としては、明示的な代替コンストラクタ呼び出しの前にステートメントを記載することができるようになりま

Testing

該当部分以外の既存の単体テストについては変更されません。
該当部分についてテストケースを追加し、コンパイラの動作検証を行います。

以前のバージョンと新しいバージョンのコンパイラを用いて全てのJDKクラスをコンパイルし、生成されたバイトコードが同じであることを確認します。

プラットフォーム固有のテストは不要です。

Risks and Assumptions

この変更は、既存の全てのJavaプログラムの互換性を完全に維持しつつ、Javaプログラムの文法を厳密に拡張します。

変更点自体はささやかなものですが、コンストラクタ呼び出しはコンストラクタの最初に書かなければならないという長年続いてきた要件が変更されます。
この要件は、コードアナライザ、スタイルチェッカー、シンタックスハイライター、開発環境、その他Javaエコシステムの奥底に深く刻み込まれています。
他の言語仕様変更と同等、ツールの更新が終わるまで痛みが伴う期間が発生するでしょう。

感想

Javaを書かなくなって久しいので正直「今までできなかったのか…」って感想だったりしますが、まあともかく、これでよりいいかんじに初期化できるようになりますね。

ぱっと思いついたところでは車の排気量とかみたいな、親クラスでは特に大きな制限がないけど、子クラスではそれぞれに対応した厳格な制限が必要なものにとかでしょうか。
まあなんか、いいかんじの使い方を専門家が考えてくれるはず。

Java22のリリースニュース記事では、本機能PEP447にかぎらず、各種新機能がほんの1行かそれ以下の微量しか書かれていないのですが、それぞれを見てみると非常に深掘りされていて1行で表しきれない面白さがあるので、覗いてみるといいとおもいます。

8
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
4