S-JIS[2024-03-22/2025-03-19] 変更履歴

super()の前に文を書く(Java22〜24)

Java22〜24のsuper()・this()の前に文を書けるようにする緩和(プレビュー版)について。


概要

2024/3/19にリリースされたJava22で、プレビュー版としてsuper()・this()の前に文が書けるようになった。

親コンストラクター呼び出し(super())や代替コンストラクター呼び出し(this())の前に文を書くことが出来る。

ただし、そこでは、初期化されていない領域にはアクセスできない。(コンパイルエラーになる)
すなわち、親クラス・自クラスのインスタンスフィールドにはアクセスできない。
thisそのものを使用することも出来ない。


Java23では、親クラスのコンストラクター呼び出し(super())の前に自分のクラス(子クラス)のフィールドを先に初期化することが出来るという仕様も加わったそうだ。[2024-09-22]
(YUICHI SAKURABAさんのJEPでは語れないJava 23

親クラスのコンストラクターからprotectedなメソッド(子クラスでオーバーライドされているメソッド)を呼び出すと、
今まではフィールドの初期化順は親クラスが完了してからだったので、子クラスのフィールドを使用することは出来なかった。
これが出来るようになるということだ。(良し悪しは別として、確かにやりたいと思ったことはある^^;)


super()・this()の前に文を書くのはJava22〜24ではプレビュー版の機能なので、この機能を使いたい場合はコンパイル時にjavacコマンドに--enable-previewを付ける必要があり、
また、実行時にjavaコマンドに--enable-previewを付ける必要がある。
JShellで試す場合もjshellコマンドに--enable-previewを付ける。

> javac --enable-preview --release 22 Example.java
> java --enable-preview Example
> java --enable-preview --source 22 Example.java

super()・this()の呼び出し前に値をチェックする例。

public class PositiveBigInteger extends BigInteger {

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

super()・this()の呼び出し前に計算を行う例。

public class Example {

    public Example(String s) {
        String key, value;
        int n = s.indexOf("=");
        if (n >= 0) {
            key = s.substring(0, n);
            value = s.substring(n + 1);
        } else {
            key = s;
            value = null;
        }
        this(key, value);
    }

    public Example(String key, String value) {
        〜
    }
}

super()・this()の呼び出しの前にthisを使うとコンパイルエラーになる。

public class Example {

    public Example(int n) {
        System.out.println(this); // NG
        this();
    }

    public Example() {
    }
}

親コンストラクター呼び出し前にフィールドを初期化する例

Java23のプレビュー版では、親クラスのコンストラクターを呼び出す前に自分のクラスのフィールドを初期化することが出来るようになった。[2024-09-22]
具体的には、「super();」の前に「this.フィールド = 値;」を書くことが出来るようになった。

従来は、親インスタンスの初期化が終わってから自分のインスタンスの初期化が開始される、という順序だった。
すなわち、親コンストラクターが呼ばれた時点では自クラス(子クラス)のフィールドはまだ初期化されていなかった。
親コンストラクターの中から子クラスのフィールドを取得すること自体は出来たが、初期化前なので、取得された値は常にnullや0だった。

親コンストラクター呼び出しより前にフィールドを初期化する場合、自クラスのフィールドの初期化が、親インスタンスの初期化より前になる。
すなわち、親クラスのコンストラクターから子クラス(自クラス)のフィールドを使用できる。

  従来の初期化順序 親コンストラクター呼び出し前にフィールドを初期化した場合の初期化順序
1 親クラスのstaticフィールドの初期化および静的初期化ブロックの実行 親クラスのstaticフィールドの初期化および静的初期化ブロックの実行
2 自クラスのstaticフィールドの初期化および静的初期化ブロックの実行 自クラスのstaticフィールドの初期化および静的初期化ブロックの実行
3   自クラスのフィールドの初期化およびインスタンス初期化ブロックの実行
4 親クラスのフィールドの初期化およびインスタンス初期化ブロックの実行 親クラスのフィールドの初期化およびインスタンス初期化ブロックの実行
5 親クラスのコンストラクターを実行 親クラスのコンストラクターを実行
6 自クラスのフィールドの初期化およびインスタンス初期化ブロックの実行  
7 自クラスのコンストラクター(super()呼び出しの後続)を実行 自クラスのコンストラクター(super()呼び出しの後続)を実行

自クラスのフィールドが親インスタンスより先に初期化されるようになるのは、「super();」の前に「this.フィールド = 値;」を書いた場合のみ
そうでない場合は、Java23プレビュー版でコンパイル(javac --enable-preview --release 23)しても、従来通りの順序で初期化される。

abstract class Parent {

    static { // 親クラスの静的初期化ブロック
        System.out.println("Parent.static-initializer");
    }
    { // 親クラスのインスタンス初期化ブロック
        System.out.println("Parent.instance-initializer");
    }

    // 親クラスのフィールド
    private String parentField = "parent-value";

    // 親クラスのコンストラクター
    public Parent() {
        System.out.println("Parent constructor parentField=" + parentField);
        System.out.println("Parent constructor childField0=" + getChildField0());
        System.out.println("Parent constructor childField1=" + getChildField1());
    }

    protected abstract String getChildField0();

    protected abstract String getChildField1();
}
public class Example extends Parent {

    static { // 自クラスの静的初期化ブロック
        System.out.println("Example.static-initializer");
    }
    { // 自クラスのインスタンス初期化ブロック
        System.out.println("Example.instance-initializer");
    }

    // 自クラスのフィールド
    private String field0 = "field0-default-value";
//  private String field1 = "field1-default-value";
    private String field1;		// コンストラクター内でsuper()の前にフィールドに値を代入する場合は、フィールド定義では初期値を書けない

    // 自クラスのコンストラクター
    public Example() {
        this.field1 = "field1-by-constructor";
        super();
        System.out.println("Example constructer field0=" + field0);
        System.out.println("Example constructer field1=" + field1);
    }

    @Override
    protected String getChildField0() {
        return this.field0;
    }

    @Override
    protected String getChildField1() {
        return this.field1;
    }

    public static void main(String... args) {
        new Example();
    }
}
実行結果
  従来の初期化順序 親コンストラクター呼び出し前にフィールドを初期化した場合の初期化順序
field1の初期化方法
    private String field1 = "field1-default-value";
    publc Example() {
        super();
        〜
    }
    private String field1;
    publc Example() {
        this.field1 = "field1-by-constructor";
        super();
        〜
    }

コンストラクター内でsuper()の前にフィールドに値を代入する場合は、フィールド定義で初期値を書くとコンパイルエラーになる。

実行結果
Parent.static-initializer
Example.static-initializer
Parent.instance-initializer
Parent constructor parentField=parent-value
Parent constructor childField0=null
Parent constructor childField1=null
Example.instance-initializer
Example constructer field0=field0-default-value
Example constructer field1=field1-default-value
Parent.static-initializer
Example.static-initializer
Example.instance-initializer
Parent.instance-initializer
Parent constructor parentField=parent-value
Parent constructor childField0=field0-default-value
Parent constructor childField1=field1-by-constructor
Example constructer field0=field0-default-value
Example constructer field1=field1-by-constructor

コンストラクター内で代入したのはfield1のみだが、フィールド初期化全体が先に実行されており、親コンストラクターで取得したfield0にも値が入っている。


プレビュー機能へ戻る / Java目次へ戻る / 新機能へ戻る / 技術メモへ戻る
メールの送信先:ひしだま