虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

Java 21 までに導入される Project Amber の新文法のおさらい

こんにちは。虎の穴ラボ S.A です。

つい先日 Java 20 がリリースされました

新しい構文やAPIの変更などの Java の変更はJEP(JDK Enhancement-Proposal)と呼ばれる提案で管理されており、 今回のリリースでも7つのJEPの変更内容が導入されました。 さっそく Java 20 の新機能を紹介しようと思いましたが、 7件の変更はいずれもインキュベータ/プレビューと呼ばれる準備段階の変更で、正式な変更はありませんでした。

というわけで今回は新機能を紹介する代わりに、 これまでに取り込まれた新文法について振り返ってみようと思います。

Java で現在進行中の4大プロジェクト

Java では現在、4つの大規模プロジェクトが進められており、 各プロジェクトから出されたJEPが都度リリースで取り込まれています。

プロジェクト名 目的
Project Amber より見やすく安全なコードが書けるよう文法を改良する
Project Panama JVM外のリソースに簡単にアクセスできるようにする
Project Loom より軽量で信頼性の高いマルチスレッド処理ができるようにする
Project Valhalla プリミティブ型・参照型を統一的に扱えるようにする

どれも Java に大きな変化をもたらすプロジェクトで、 このうち文法に影響を与えるプロジェクトは、Project Amber と Project Valhalla の二つになります。

Project Valhalla のJEPに関しては、まだまだリリース時期が先になりそうです。 Project Amber は既に多くのJEPが取り込まれており、次のLTSであるJava 21 のリリースまでに多くの変更が出揃いそうです。

Java 10 からローカル変数の型宣言が var キーワードでできるようになり、Java でのコーディング体験がかなり快適になったのですが、 これもこの Project Amber から出されたJEPによるものです。

Java 21 までに取り込まれる Project Amber のJEP

これまでに Project Amber から出されてリリースされた、もしくはプレビュー版としてリリースされJava 21で導入予定のJEPは以下の通りです。

このうち最初の二つはそれぞれローカル変数の宣言、ラムダ式の変数の宣言で var キーワードが使えるようになる改修です。 続くText Block、String Templatesは、それぞれ複数行にまたがる文字列リテラルが使えるようになる、変数を埋め込めるテンプレートが使えるようになるといった文字列関係のJEPになります。

残る6つについては、どれもインパクトのある新文法で、 組み合わせて用いることで今までにないスタイルのコーディングができます。 今回はこの6つの新文法について紹介します。

Project Amber の新文法のJEP

①Switch Expression

Switch Expression は switch が式で書けるようになる文法改修です。Java 14で導入されました。

従来の switch 文はコロン:で書きますが、アロー->で書くことで switch 式となります。

// 従来のswitch
String message;
switch(day){
    case SATURDAY:
    case SUNDAY:
        message = "休日ですね!";
        break;
    case MONDAY:
    case TUESDAY:
    case WEDNESDAY:
        message = "お仕事頑張って!";
        break;
    case THURSDAY:
    case FRIDAY:
        message = "今週も後少し!";
        break;
    default:
        message = "";
}
// 新しいswitch
var message = switch(day){
   case SATURDAY, SUNDAY -> "休日ですね!";
   case MONDAY, TUESDAY, WEDNESDAY -> "お仕事頑張って!";
   case THURSDAY, FRIDAY -> "今週も後少し!";
};

break を書く必要がなくなったこと、値をカンマ区切りで書けるようになったことで、だいぶ記述がスッキリしました。 また、従来の switch は文ですが、式になったことで変数に代入できるようになりました。

switch 式は網羅的(どれかの分岐に必ず入る必要がある)でないとコンパイルエラーとなりますが、 enum 型の場合、全要素の case を書いていれば網羅的と判断されるのも便利です。

②Pattern Matching for instanceof

続いて、instanceof によるパターンマッチです。Java 16で導入されました。 名前からは分かりにくいですが、 instanceof の後ろに変数名を書くことで、その後ブロック内でわざわざキャストせずとも変数が使えるようになりました。

// 従来の instanceof
if(object instanceof String){
   String s = (String) object; //キャストが必要!
   System.out.println("size: "+s.length());
}
// 新しい instanceof
if (object instanceof String s){ //ここで変数宣言できるようになった!
   System.out.println("size: "+s.length());
}

③Pattern Matching for switch

続いて、switch によるパターンマッチです。 こちらは Java 20 現在もまだプレビューの機能ですが、Java 21 で正式に導入されそうです。 switch 式で値を書く代わりに型を書くことで、instanceof 版 switch が書けるようになりました。

// 従来の if 文での書き方
String message;
if (object instanceof Integer i){
   message = String.format("integer %d", i);
}else if (object instanceof Long l){
   message = String.format("long %d", l);
}else if (object instanceof String s){
   message = String.format("string %s", s);
}else{
   message = "unknown";
}
// 新しい switch での書き方
var message = switch(object){
   case Integer i -> String.format("integer %d", i);
   case Long l -> String.format("long %d", l);
   case String s-> String.format("string %s", s);
   default -> "unknown";
};

これまでは複数の型のパターンマッチは、instanceof の if 文を羅列するしかありませんでしたが、 switch で書けるようになり記述がだいぶスッキリしました。

④Sealed Classes

シールクラスは、class や interface に sealed 修飾子をつけることで、 permits 句で指定したクラス以外での継承/実装を禁止できる仕組みです。 Java 17 で導入されました。

// 長方形クラスと円クラスだけが実装できる図形インターフェース
sealed interface Shape permits Rectangle, Circle{
}
// 辺の長さを持つ長方形クラス
final class Rectangle implements Shape{
    private final int width;
    private final int height
    public Rectangle(int width, int height){
        this.width = width;
        this.height = height;
    }
    public int getWidth(){
         return width;
    }
    public int getHeight(){
        return height;
    }
}
// 半径を持つ円クラス
final class Circle implements Shape{
    private final int radius;
    public Circle(int radius){
        this.radius = radius;
    }
    public int getRadius(){
         return radius;
    }
}

permits 句で指定したクラス以外で implements しようとするとコンパイルエラーとなります。 また、指定したクラスも、継承されないように final にしておく必要があります。

シールクラスは、単にアクセス先を制限する目的以外にも、 ③Pattern Matching for switch で導入されたパターンマッチにおいて、permits された全クラスを列挙することで網羅的と判断されるといった利点があります。

// sealed class の型が網羅されていれば、default を描かなくてOK!
var message = switch(shape){
   case Circle c -> "円 半径="+c.getRadius();
   case Rectangle r -> "長方形 幅="+r.getWidth()+" 高さ="+r.getHeight();
};

⑤Record

Java 16 でレコードが導入され、不変(immutable)なフィールドを持つクラスを簡単に作ることができるようになりました。 レコードは次のように宣言できます。

// 新しいレコードクラスの書き方
record Point(int x, int y){ }

これだけ書けば、フィールド、コンストラクタ、アクセッサ(フィールド名と同じ)、equals/hashCode、toString が内部的に生成されるので、次のコードと同等のクラスになります。

// 従来の書き方
public class Point {
    private final int x;
    private final int y;
    public Point(int x, int y){
        this.x = x;
        this.y = y;
    }
    public int x(){
        return x;
    }
    public int y() {
        return y;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }
    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }
    @Override
    public String toString() {
        return "Point{x="+x+", y="+y+'}';
    }
}

⑥Record Pattern

最後は、レコードパターンです。 instanceof や switch のパターンマッチで、レコードクラスを扱う場合は、 レコードクラスの型だけでなく、フィールドの型までも展開してパターンマッチできます。 現在プレビュー段階ですが、Java 21で正式導入されそうです。

// このようなレコードクラスがあったとして...
record Point(int x, int y){}
record TaggedPoint(Point p, String name){}
// instanceof の後にレコードのフィールドの中身まで展開して書けるようになった!
if (object instanceof TaggedPoint(Point(int x, int y), String name)){
   System.out.println(name+"=("+x+","+y+")");
}

パターンマッチとポリモーフィズム

Project Amber によって導入された record や switch といった新文法で、 不変なクラスの作成や、パターンマッチでの処理の記述が簡単にできるようになりました。

これらは大きな変化ですが、 一方でパターンマッチで記述できることは、従来のポリモーフィズムを用いた書き方でも実現可能です。

例えば、図形を表す Shape インターフェースと、 そのサブクラスの長方形を表す Rectangle クラスと円を表す Circle クラスがあるとします。 また、それぞれの面積を求める機能が必要だとします。 パターンマッチでこれらのプログラムを記述するとしたら次のようになります。

//パターンマッチによる記述
public sealed interface Expr{ // 内部クラスであれば permits は省略可
    record Rectangle(int width, int height) implements Expr { }
    record Circle(int radius) implements Expr{ }

    public static double area(Shape shape){
        return switch (shape){
            case Rectangle(int w, int h) -> (double)w*h;
            case Circle(int r)-> r*r*Math.PI;
        };
    }
}

一方で、同等のことを従来のポリモーフィズムで次のように書くこともできます。

//従来のポリモーフィズムによる記述
public interface Shape {
    double area();

    record Rectangle(int width, int height) implements Shape{
        @Override
        public double area() {
             return (double)width * height;
       }
    }

    record Circle(int radius) implements Shape{
        @Override
        public double area() {
            return radius*radius*Math.PI;
       }
    }
}

こうしてみると、パターンマッチによる記述が従来のポリモーフィズムによる記述に比べ劇的に記述量が減るわけでもなさそうです。 ではパターンマッチでの記述による利点とは何かというと、以下の二つにあると思います。

  • 処理(機能)の変更に強い
  • データと処理を分離して記述できる

処理(機能)の変更に強い

まず、「処理(機能)の変更に強い」ですが、先の例でいうと、 新たに 図形の周囲の長さを求める機能が必要になった場合、 ポリモーフィズムの書き方では、Shapeインターフェース、Rectangleクラス、Circleクラスの全てに変更が必要ですが、 パターンマッチの書き方では、新たに関数を一つ用意すれば良いだけで、既存のソースに手を加えなくて済みます。

逆に新しく三角形などの要素が追加になった場合、 ポリモーフィズムの書き方では、Triangle クラスを追加すれば良いだけですが、 パターンマッチの書き方では、Triangle クラスの追加と area関数の修正が必要になります。

ただ、現実のシステムでは、要素の追加より機能の追加の方が要件として多い傾向にあるため、 この点でパターンマッチが有用になるケースが多々見受けられると思います。

ポリモーフィズム パターンマッチ
要素の追加 ○ クラスを一つ追加するだけで良い △ 全メソッドを修正する必要がある
機能(処理)の追加 △ 全クラスを修正する必要がある ○ メソッドを一つ追加するだけで良い

データと処理を分離して記述できる

また、データと処理を分離して記述できるのもパターンマッチの魅力です。 ポリモーフィズムの書き方では面積を求める処理が各図形のクラスに分散していますが、 パターンマッチの書き方では面積を求める処理が1ヶ所にまとまっています。

処理が分離されると、複雑さが回避できるだけでなく、 コードの再利用性が高まる、テスタビリティが高まるなどのメリットがあります。

Project Amber の目指す「データ指向」

Project Amber はもともとコードをより見やすく安全にすることを目的としていましたが、 前提として、ステートフルなクラスの使い方はシステムを複雑にしがちであるという問題意識があったようです。

これを解消すべく、 状態を持たない(Immutableな)データを使い、データと処理を分離した記述にする 「データ指向(Data-Oriented)」と呼ばれる設計が提案されました。

(データ指向についてはこちらで詳しく紹介されています!)

レコードクラスやパターンマッチなど、今回紹介した Java の新文法は、 まさにこの「データ指向」な設計を開発者に提供するためのものでした。

あとがき

今回は Java 21までに導入された Project Amber の新文法をおさらいしました。

冒頭でも触れたように、Java で現在進行中のプロジェクトは Project Amber の他にもあります。 特に Project Valhalla がリリースされたら、これまで以上に大きく Java が変化すると思われます。

今後もJavaの最新動向に注目していきたいと思います。

P.S.

採用

虎の穴では一緒に働く仲間を募集中です!
この記事を読んで、興味を持っていただけた方はぜひ弊社の採用情報をご覧下さい。
yumenosora.co.jp