前回は、2020年9月15日にリリースされたJava 15について、新たに追加された14個のJEP(JDK Enhancement Proposals)を紹介した。今回はそれらの中から、言語仕様に関わる新機能である「Records」「instanceofのパターンマッチング」「Sealed Classes」のについてもう少し詳しく解説する。

なお、今回取り上げる3つの機能はいずれもまだプレビューの段階なので、使用するにはコンパイルおよび実行時にコマンドラインオプションとして「--enable-preview」を付ける必要がある。IntelliJ IDEAの場合は、プロジェクトの言語レベルとして「15 (Preview)」を選択すればよい。

  • IntelliJ IDEAでJava 15のプレビュー機能を使用する設定

    IntelliJ IDEAでJava 15のプレビュー機能を使用する設定

JEP 384: Records (Second Preview)

JEP 384はrecordという機能について提案しているJEPである。Java 14でPreviewとして追加されており、そのフィードバックを受けてJava 15ではSecond Previewになった。順調にいけば次期バージョンのJava 16で正式版になる。

recordは、不変なデータを保持するためのクラスを簡単に定義できるようにする新しい構文になる。例えば、フィールド値としてx座標とy座標の値をもつPointクラスを定義することを考える。この場合、通常であれば次のようなコードを記述することになるだろう。

class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return this.x;
    }
    public int getY() {
        return this.y;
    }

    // その他、toString()はequals()などのメソッド
}

呼び出し側のコードは次のようになる。

Point point = new Point(10,20);
System.out.println("x = " + point.getX());
System.out.println("y = " + point.getY());

このようなクラスは、フィールド値とコンストラクタ、ゲッター、セッターを持つほとんど定型的なクラス定義であるにもかかわらず、記述量が多く冗長になりがちだ。recordは、このようなクラスをもっとシンプルに記述できるようにする目的で提案された。具体的には、上のPointクラスは次のように宣言できるようになる。

record Point(int x, int y) {}

recordは、「class」の代わりに「record」というキーワードを付けて宣言する。このコードは、コンパイルするとPointクラスが生成される。Pointクラスの内容は、だいたい次のような宣言のクラスと同等のものになる。

public class Point extends Record {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int x() {
        return this.x;
    }
    public int y() {
        return this.y;
    }

    @Override
    public int hashCode() { ... }
    @Override
    public boolean equals(Object o) { ... }
    @Override
    public String toString() { ... }
}

フィールド値を引数に取るコンストラクタや、フィールド値を取得するためのメソッド、hashCode()、equals()、toString()などのメソッドが自動で生成されることがわかる。コンストラクタの引数の順番は、必ずフィールドが宣言されている順番と同じ順番になる。引数を取らないコンストラクタは生成されない。取得用のメソッドはgetX()のような名前ではなく、フィールド値の変数名と同じ名前のメソッドになる。フィールド値はfinalで不変なので、設定用のメソッドは無い。なお、hashCode()やequals()などの実装は、実行時にinvokeDynamic命令を使って実装コードが生成される。

record宣言によって生成されるクラスがjava.lang.Recordを継承する仕組みになっている点も注目してほしい。このことは、recordのクラスは他のクラスを継承できないことも意味している(多重継承になってしまうため)。またjava.lang.Recordはrecord宣言のために用意されたものなので、通常のクラスがこれを継承することはできない。

呼び出し側のコードでは、次のように普通にPointクラスを宣言した場合と同様に扱うことができる。

Point point = new Point(10,20);
System.out.println("x = " + point.x());
System.out.println("y = " + point.y());

コンストラクタを明示的に定義してレコードを作成することもできる。次の例では、インスタンスを生成する際に渡された2つの引数が指定範囲外だった場合に、コンストラクタによって自動で値を補正するような例である。このようなコンストラクタをカノニカルコンストラクタと呼ぶ。

public record Range(int lo, int hi) {
    public Range(int lo, int hi){
        this.lo = (lo<0)?0:lo;
        this.hi = (hi>100)?100:hi;   // 省略するとコンパイルエラー
    }
}

カノニカルコンストラクタを用意する場合、引数の型と数、順序は必ずフィールドの宣言と一致していなければならない。引数が一致していない次のような宣言はコンパイルエラーになる。

public record Range(int lo, int hi) {
    public Range(int lo) {  // 引数が一致しないとコンパイルエラー
        this.lo = (lo<0)?0:lo;
    }
}

また、カノニカルコンストラクタ内では必ずすべてのフィールド値を初期化しなくてはならない。たとえ引数が一致していたとしても、次の例のように初期化しないフィールドがある場合にはコンパイルエラーになる。

public record Range(int lo, int hi) {
    public Range(int lo, int hi){
        this.lo = (lo<0)?0:lo;
        //this.hi = (hi>100)?100:hi;   // 省略するとコンパイルエラー
    }
}

次の例ように、引数の指定を完全に省略したコンストラクタを宣言することもできる。この場合は、フィールド変数名と同じ型・名前の仮引数が自動的にセットされる。このようなコンストラクタをコンパクトコンストラクタと呼ぶ。

public record Range3(int lo, int hi) {
    public Range3 {
        if (lo > hi) {
            lo = hi;          // 仮引数の書き換えは可能
            //this.lo = hi;   // コンポーネントフィールドに直接値を代入するとエラーになる
        }
    }
}

コンパクトコンストラクタではコンポーネントフィールドへの値の代入はできないが、上の例のように仮引数のloの値を書き換えれば結果的にコンポーネントフィールドに反映される。コンポーネントフィールドに直接値を代入しようとするとコンパイルエラーになる。

JEP 375: instanceofのパターンマッチング (Second Preview)

JEP 375は、instanceof演算子を拡張して、クラス名の代わりに定数や変数宣言を比較対象として指定できるようにするという提案である。instanceofは、ある値が指定されたクラスのインスタンスであるかどうかを検査するための演算子で、従来であれば次のような使い方が一般的だった。

Objetc obj = ...;
if (obj instanceof String) {
    String str = (String) obj;
    // strを使った処理
}

変数objをStringにキャストするには、中身が本当にStringインスタンスなのかを確認するためにinstanceofによる検証が必要となる。このような時、JEP 375では次のように書けるようになる。

if (obj instanceof String str) {
    // strを使った処理
}

「String str」の部分がマッチさせたいパターンで、このケースではobjの値がStringインスタンスであればtrueを返し、その上でstrにobjの値が代入される。Stringインスタンスでない場合にはfalseが返され、変数strへの代入は行われない。

instanceofのパターンパッチングは、次の例のように想定できるクラスが複数ある場合などに記述をシンプルにできるため便利である。

String formatted;
if (obj instanceof Integer i) {
    formatted = String.format("int %d", i);
} else if (obj instanceof Double d) {
    formatted = String.format("double %f", d);
} else if (obj instanceof String s) {
    formatted = String.format("String %s", s);
} else {
    formatted = "unknown";
}

将来的には、これと同様のパターンマッチ機能が、次の例のようにswitchでも利用可能にするJEPが提出されているが、今のところターゲットとするJavaのバージョンはまだ決定していない。

String formatted;
switch (obj) {
    case Integer i: formatted = String.format("int %d", i); break;
    case Double d:  formatted = String.format("double %f", d); break;
    case String s:  formatted = String.format("String %s", s); break;
    default:        formatted = "unknown";
}

JEP 360: Sealed Classes (Preview)

JEP 360で提案されたSealed Classesは、特定のクラスにしか継承できないクラスを定義できるようにするものになる。従来のクラスは、final宣言されていなければ自由に継承することができた。一方でfinal宣言されたクラスは、誰からも継承できなかった。それに対してJEP 360のSealedクラスは、クラス宣言で明示的に指定されたクラスにだけ継承が許され、それ以外のクラスからは継承することができない。

具体的には、次のようにsealedという識別子を付けて宣言したものがSealedクラスになる。継承を許可するクラスはpermitsという修飾子の後ろにカンマ区切りで宣言する。

public abstract sealed class Shape permits Circle, Rectangle {
    public abstract double area();
}

この例の場合、CircleクラスおよびRectangleクラスはShapeを継承することができるが、それ以外のクラスはShapeを継承してサブクラスになることはできない。Permits指定されたCircleとRectangleは必ず同じモジュール内、またはモジュール化されていない場合は同じパッケージ内に定義する必要がある。

さらに、permitsされたクラスは、必ずsealedかnon-sealed、またはfinalのいずれかを付けて定義する必要がある。もしサブクラスもSealedクラスにしたい場合にはsealed宣言を、Sealedクラスにしたくない場合にはnon-sealedを明示的に付ける。それ以上継承を許さない場合にはfinalにする。

次のCircleクラスとRectangleクラスは、それぞれfinalとnon-sealedで定義した例である。Rectangleクラスの方はnon-sealedなのでほかのクラスから自由に継承することができる。

public final class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    public double area() {
        return Math.pow(radius, 2) * Math.PI;
    }
}
public non-sealed class Rectangle extends Shape {
    private double x;
    private double y;

    public Rectangle(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double area() {
        return this.x * this.y;
    }
}

なお、sealed修飾子はクラスだけでなくインタフェースに付けて宣言することもできる。

Sealedクラスは、前述のパターンマッチングと組み合わせて考えると便利さがわかる。例えば、次の例を見てみよう。ここではshapeが何のクラスのインスタンスなのかを調べ、それに応じて処理を変えている。

Shape shape = new Circle(10);

if (shape instanceof Circle c) {
    System.out.println("円の面積: " + c.area());

} else if (shape instanceof Rectangle r) {
    System.out.println("四角の面積: " + r.area());
} else {
    // ShapeのサブクラスはCircleとRectangleだけという
    // ことが保証されているので、このelseは不要
}

説明のためにelse節を書いてあるが、ShapeのサブクラスはCircleかRectangleだけということが確定しているので、実際にはこのelse節は不要になる。

さらにswitchに対するパターンマッチングができるようになれば、次のようにdefaultの指定をしなくても全パターンの網羅が可能になる。

Shape shape = new Circle(10);

switch(shape) {
    case Circle c: 
        System.out.println("円の面積: " + c.area()); 
        break;
    case Rectangle r: 
        System.out.println("四角の面積: " + r.area()); 
        break;
}

このように、パターンマッチングやSealedクラスはまだ発展途上の段階であり、今後も関連するJEPが追加される見込みとなっている。

さて、Java 9以降で導入されたJavaの便利な機能を紹介してきた本連載は、今回で最終回になる。「レガシー」だと言われることも多いJavaだが、新しいリリースサイクルでの開発が始まって以来どんどん新しい機能が取り入れられて進化を続けている。開発者としては、常に最新動向をチェックしてこの進化に追随していく必要があるだろう。連載中に取り上げ切れなかった機能もまだたくさんあるので、それについては別の機会に紹介できればと思っている。