Java

Java 20 と IntelliJ IDEA

Read this post in other languages:

私は長らく Java の新リリースについて執筆してきましたが(Java 10 以降)、開発者が 6 か月ごとにJava の新機能について知り、使用できるというのは素晴らしいことだと思っています。

過去のリリースと比べると、Java 20 に追加された機能はそれほど多くはありません。 Java 20 ではスレッド内やスレッド間でのイミュータブルデータの共有を可能にして仮想スレッドをサポートする Scoped Valuesインキュベート API として導入されています。 2 回目のプレビューを開始したレコードパターンではジェネリックレコードパターンのサポートが改善され、拡張 for ステートメントでの使用が可能になっています。 4 回目のプレビューを開始した switch のパターンマッチングでは、網羅的な switch、単純な switch ラベル、およびジェネリックレコードパターンの推論される型引数を操作する際の使い勝手が改善されています。

Java 20 で 2 回目のプレビューとなる Foreign Function and Memory API では、Java コードと JVM 外部のコードとデータの通信を可能にする機能が引き続き改善されています。 構造化された並行性によってサポートされ、マルチスレッドアプリケーションの作成に革命をもたらす軽量の仮想スレッドが最新の Java リリースでもう一度プレビューとなっています。 今回で 5 回目のプレビューを開始した Vector API により、コードのベクトル演算が操作しやすくなっています。

このブログ記事では、ジェネリックレコードパターン、網羅的な switch ステートメント、および式の型推論や拡張 for ヘッダーでのレコードパターンといった最も重要な変更点を中心に、IntelliJ IDEA によるレコードパターンと switch 用のパターンマッチングなどの言語機能のサポートについて説明します。

では始めましょう。

IntelliJ IDEA の構成

Java 20 のサポートは IntelliJ IDEA2023.1 で提供されています。 今後の IntelliJ IDEA リリースでは、さらにサポートを充実させる予定です。

switch 用のレコードパターンやパターンマッチングなどの Java 20 の新しい言語機能を使用するには、ProjectSettings(プロジェクト設定)| Project(プロジェクト)に移動し、プロジェクトの SDK を 20、プレジェクトの言語レベルを ’20 (Preview) – Record patterns (second preview), Pattern Matching for switch (fourth preview)’(20 (プレビュー) – レコードパターン (2 回目のプレビュー)、switch のパターンマッチング (4 回目のプレビュー))に設定します。

システムにダウンロード済みのバージョンの JDK を使用するか、‘Edit’ (編集)をクリックして ‘Add SDK(SDK の追加) >’ ‘Download JDK…’(JDK のダウンロード)をクリックして別のバージョンをダウンロードすることができます。 ダウンロードする JDK のバージョンは、ベンダーのリストから選択します。

Module(モジュール)タブで、モジュールに同じ言語レベル(20 (Preview) – Record patterns (second preview), Pattern Matching for switch (fourth preview))(20 (プレビュー) – レコードパターン (2 回目のプレビュー)、switch のパターンマッチング (4 回目のプレビュー))が選択されていることを確認します。

これを選択すると、IntelliJ IDEA が今後のバージョンで Java プレビューの言語機能のサポートを中止する可能性があることを通知するポップアップが続けて表示される場合があります。 プレビューの機能は(まだ)恒久的に提供される機能ではなく、今後の Java リリースで変更(または削除)される可能性があるためです。
では、Java 19 で導入されたレコードパターンについて簡単におさらいしましょう。

レコードパターン

レコードインスタンスに実行したい操作といえば、そのコンポーネントの値を抽出してアプリケーション内で使用することでしょう。 レコードパターンはまさにこの操作を実行します。

レコードパターンとは何か、そしてそれが必要な理由について簡単に復習しましょう。

レコードパターンのおさらい

レコードは、データを透過的に運搬するキャリアを作成する単純で明瞭な手法です。 レコードを使用すると、複数の値(コンポーネントとも呼ばれます)をまとめて集計できます。 逆に、レコードパターンはレコードインスタンスをコンポーネントに分解し、コンポーネントの値を簡単に使用できるようにします。

たとえば、以下のようなレコードがあるとします。

record Name       (String fName, String lName) { }
record PhoneNumber(String areaCode, String number) { }
record Country    (String countryCode, String countryName) { }
record Passenger  (Name name,
                  PhoneNumber phoneNumber,
                  Country from,
                  Country destination) { }

以下のコードでは、レコードパターンと instanceof 演算子を一緒に使用してレコードインスタンスのコンポーネントを一連のパターン変数に分解する明確なコードを定義する方法を示しています。

boolean checkFirstNameAndCountryCodeAgain (Object obj) {
  if (obj instanceof Passenger(Name (String fName, var lName),
                               var phoneNumber,
                               Country from,
                               Country (var countryCode, String countryName) )) {
      if (fName != null && countryCode != null) {
          return fName.startsWith("Simo") && countryCode.equals("PRG");
      }
  }
  return false;
}

レコードパターンをまったく知らない方は、Java 19 and IntelliJ IDEA という私のブログ記事を読むことをお勧めします。レコードパターン自体の説明と使用すべき理由を詳しく説明しています。 この記事では、レコードパターンの Java 19 から Java 20 への変更点について説明しています。

Java 20 では、Java 19 で追加されたnamed Record Patterns(名前付きレコードパターン)の使用が廃止されました。 Java 20 ではジェネリックレコードの型引数の推論のサポートが改善され、拡張 for ループのヘッダーでレコードパターンを使用できるようになっています。

レコードコンポーネントの型推論

Java 19 ではレコードコンポーネントの推論がサポートされ、レコードコンポーネントの明示的な型を使用する代わりに var を使用できるようになりました。 前のセクションにあるレコードパターンの例をもう一度確認し、var の使用箇所に注意して見てください。

boolean checkFirstNameAndCountryCodeAgain (Object obj) {
  if (obj instanceof Passenger(Name (String fName, var lName),
                               var phoneNumber,
                               Country from,
                               Country (var countryCode, String countryName) )) {
      if (fName != null && countryCode != null) {
          return fName.startsWith("Simo") && countryCode.equals("PRG");
      }
  }
  return false;
}

var を使って定義されるすべてのローカル変数に適用できるように、IntelliJ IDEA は予約キーワード var を使って定義された変数の明示的な型を表示することができます。

ジェネリックレコードパターン

Java 20 はジェネリックレコードパターンの型引数の推論をサポートしています。 たとえば、腕時計(wristWatch)や書籍(Book)など、友達への贈り物(Gift)を例にジェネリックレコードパターンを理解してみましょう。

以下のように、ジェネリックでないクラスの BookWristWatch、ジェネリックレコードの Gift が定義されているとします。

class Book {...}
class WristWatch {...}
record Gift<T>(T t) {}

友達が贈り物を受け取り、包みを開こうとするとどうなると思いますか? 以下に定義される unwrap メソッドを呼び出すとします。 以下のコードでは、unwrap メソッドは Gift<wristwatch> (var watch) を使用しています。 このパターンにはすでにレコード名 Gift でジェネリック型の WristWatch が指定されているため、パターン変数 watchWristWatch 型として推論されます。

void unwrap(Gift<wristwatch> obj) {
   if (obj instanceof Gift<wristwatch> (var watch)) {
       watch.setAlarm(LocalTime.of(10, 25));
   }
}

以下の例は Java 19 では動作しませんが、Java 20 では動作します。

void unwrapAndRevealSurprise(Gift<WristWatch> obj) {
    if (obj instanceof Gift<WristWatch> (var watch)) {
        System.out.println(watch);
    }
}

void unwrapAndUseGift(Gift<WristWatch> obj) {
    if (obj instanceof Gift(var gift)) {
        gift.setAlarmTime(LocalTime.now());
    }
}

void birthdayGift(Gift<DiamondStudded<WristWatch>> gift) {
    if (gift instanceof Gift<DiamondStudded<WristWatch>>(DiamondStudded(var personalizedGift))) {
        System.out.println(personalizedGift);
    }
}

void performanceBonus(Gift<DiamondStudded<WristWatch>> personalizedGift) {
    if (personalizedGift instanceof Gift(DiamondStudded(var actualGift))) {
        System.out.println("Wrist watch" + actualGift);
    }
}

これによって、網羅的な switch コンストラクトの動作がどのように変化するかを次のセクションで見てみましょう。

ジェネリックレコードによる網羅的な switch コンストラクト

始める前に、基本を確かめましょう。 次の画像では、switch ステートメントまたは switch 式でセレクター式が参照しているものを示しています(switch コンストラクトに渡す変数または式)。

switch ステートメントと switch 式の構文でセレクター式の値を case ラベルの型パターンまたはレコードパターンに一致させようとする場合、構文を網羅的にしなければなりません。 言い換えると、セレクター式は case ラベルに定義されている 1 つ以上の値に一致しなければなりません。

Object 型のようにサブタイプの数が定まらない(それを拡張している他のクラスの数が確定しない)型の場合は、default の case ラベルを定義するか、Object 型自体を case ラベルの 1 つとして定義することで網羅的な switch コンストラクトにすることが可能です。 網羅的な switch 式と網羅的な switch ステートメントの有効な例を以下に示します。

String exhaustiveSwitchExpression(Object obj) {
   return switch (obj) {
       case String s -> "String";
       case Apple apple -> "Apple";
       default -> "everything else";
   };
}

void exhaustiveSwitchStatement(Object obj) {
   switch (obj) {
       case String s -> System.out.println("String");
       case Apple apple -> System.out.println("Apple");
       case Object object -> System.out.println("everything else");
   };
}

sealed クラスのようなサブタイプが確定している一部の型では、default ラベルを使って網羅的な switch ステートメントまたは switch 式を定義する必要はありません。 以下に例を示します。

sealed interface HighProtein permits Egg, Cheese {}
final class Egg implements HighProtein {}
final class Cheese implements HighProtein {}
 
int processHighProtein(HighProtein protein) {
   return switch (protein) {
       case Egg egg -> 2;
       case Cheese cheese -> 10;
   };
}

ただし、HighProtein インターフェースを通常のインターフェース(つまり、非 sealed インターフェース)として定義する場合は前のコードではコンパイルできません。

では、セレクター式がジェネリックレコードである場合の網羅的な case ラベルの定義方法について説明しましょう。これは、レコードパターンに対してマッチングされます。 以下は、別の HimalayanApple クラスによって拡張されている Apple ジェネリッククラス、Egg クラスと Cheese クラスで実装されている HighProtein sealed インターフェース、および型パラメーターを受け入れてその型の 2 つのコンポーネントを定義する Dish ジェネリックレコードの例です。

public class Apple {}
public class HimalayanApple extends Apple{}

sealed public interface HighProtein permits Egg, Cheese {}
public final class Egg implements HighProtein {}
public final class Cheese implements HighProtein {}

public record Dish<T> (T x, T y){}

このジェネリックレコードクラス Dish のインスタンスに切り替えた場合に動作する、または動作しない複数の組み合わせを説明します。

まずは orderAppleDish メソッドを見てみましょう。これは、型 Dish<Apple> appleDish のメソッドパラメーターを受け入れ、それを切り替えながらレコードパターンと照合します。 以下のコードは動作すると思いますか?

    int orderAppleDish(Dish<Apple> appleDish) {
        return switch (appleDish) {
            case Dish<Apple>(Apple apple, HimalayanApple himalayanApple) -> 1;
            case Dish<Apple>(HimalayanApple himalayanApple, Apple apple) -> 2;
        };
    }

HimalayanApple クラスは Apple クラスを拡張しているため、前の switch 式を網羅的な switch にするには以下の 2 つの組み合わせが必要となります。

Apple, Apple
HimalayanApple, HimalayanApple

以下の画像は IntelliJ IDEA が前のコードをエラーとして検出し、コードへの移動と修正を支援する様子を示しています。

参考までに、以下は前の画像で修正された後の最終的なコードです。

class Apple {}
class HimalayanApple extends Apple{}
record Dish<T> (T ingredient1, T ingredient2) {}

public class FoodOrder {

    int orderAppleDish(Dish<Apple> appleDish) {
        return switch (appleDish) {
            case Dish<Apple>(HimalayanApple apple1, HimalayanApple apple2) -> 4;
            case Dish<Apple>(Apple apple, HimalayanApple himalayanApple) -> 1;
            case Dish<Apple>(HimalayanApple himalayanApple, Apple apple) -> 2;
            case Dish<Apple>(Apple apple1, Apple apple2) -> 3;
        };
    }

}

以下の sealed インターフェースを使用する例と、ジェネリックレコード Dish を使ったこのインターフェースの実装を見てみましょう。

sealed public interface HighProtein permits Egg, Cheese {}
public final class Egg implements HighProtein {}
public final class Cheese implements HighProtein {}

public record Dish<T> (T x, T y){}

ある人が食べ物を注文し、Dish のインスタンスと型パラメーターの HighProtein インターフェースを渡したとします。 この場合、以下の switch 式は網羅的です。

参考までに、コードを示します。

public class FoodOrder {

    int orderHighProteinDish(Dish<HighProtein> proteinDish) {
        return switch (proteinDish) {
            case Dish<HighProtein>(HighProtein protein, Egg egg) -> 1;
            case Dish<HighProtein>(HighProtein protein, Cheese cheese) -> 2;
        };
    }

}

HighProtein は sealed インターフェースであるため、以下の switch コンストラクトの最初の case ラベルは EggCheese のどちらかがレコード Dish の 2 つ目の値として渡される場合にも対応できます。 つまり、case ラベルは 3 つしか定義されていませんが網羅的な switch 式となっています。

public class FoodOrder {

    int orderHighProteinDish(Dish<HighProtein> proteinDish) {
        return switch (proteinDish) {
            case Dish<HighProtein>(Egg protein, HighProtein highProtein) -> 1;
            case Dish<HighProtein>(Cheese cheese, Egg egg) -> 2;
            case Dish<HighProtein>(Cheese cheese1, Cheese cheese2) -> 4;
        };
    }

}

網羅的な switch とジェネリックレコードに関する次の最後の例では、switch 式の最初にある case ラベル一式は網羅的でありません。最初の材料が HighProtein のインスタンスで、2 つ目の値が Egg インスタンスである高たんぱく料理を処理できない可能性があるためです。

    int orderHighProteinDish(Dish<HighProtein> proteinDish) {
        return switch (proteinDish) {
            case Dish<HighProtein>(Egg protein, Cheese cheese) -> 1;
            case Dish<HighProtein>(Cheese cheese, Egg egg) -> 2;
            case Dish<HighProtein>(HighProtein highProtein, Cheese cheese) -> 3;
        };
    }

前のコードに case ラベルをもう 1 つ追加して、網羅的な switch 式にしてみましょう。

            case Dish<HighProtein>(HighProtein highProtein, Egg egg) -> 10;

拡張 for ステートメントでのレコードパターンの使用

Java 20 では、拡張 for ループのヘッダーでレコードパターンを使用できます。 ただし、複数の case ラベルを割り当てられる switch ステートメントまたは式の場合、拡張 for ループヘッダーで使用する単一のレコードパターンは for ループで反復するすべての値に一致する必要があります。 一致しない場合、コードはランタイム例外をスローします。

以下は、Point レコードと Triangle レコード、およびヘッダーにレコードパターンを使用して Triangle インスタンスのリストを反復する拡張 for ループの例です。

record Point (int x, int y) { }
record Triangle(Point pointA, Point pointB, Point PointC) { }

long addLowerRightCoordinates(List<Triangle> triangles) {
    long sum = 0;
    for (Triangle(Point a, Point b, Point (int x, int y)) : triangles) {
        sum += x + y;
    }
    return sum;
}

以下に、間違いの例をいくつか示します。

public class Test {

    sealed interface UpperWithPermit permits PermittedRecord {}
    record PermittedRecord(int x) implements UpperWithPermit {}
    interface Upper {}
    record Record(List<String> x) implements Upper {}

    void test1(List<Upper> lists) {
        for (Record(List<String> x) : lists) {} 
    }

    void test2(List<? super UpperWithPermit> lists) {
        for (PermittedRecord(var x) : lists) {} 
    }

    void test3(List<? super PermittedRecord> lists) {
        for (PermittedRecord(var x) : lists) {} 
    }

    void test4(List lists) {
        for (PermittedRecord(var x) : lists) {} 
    }

    void test5(List<?> lists) {
        for (PermittedRecord(var x) : lists) {} 
    }
}

switch のパターンマッチング

Java 20 では、switch のパターンマッチングは 4 回目のプレビューとなりました。 パターンマッチングをまったく知らない方は、まずはこちらのリンクで instanceof によるパターンマッチングを確認することをお勧めします。 switch でのパターンマッチングが初めての方は、こちらのリンクをご覧ください。

Java 20 では、この機能にいくつかの変更が追加されています。 switch のパターンマッチングを列挙型クラスで使用した場合、網羅的な switch ステートメントまたは switch 式を実行時に一致するラベルを検出できない場合に ImcompatibleClassChangeError ではなく MatchException がスローされるようになりました。 Java 20 のこの機能には、case ラベルにおけるジェネリックレコードパターンの型パラメーターの推論に関連する別の変更があります。 この機能については、このブログ記事の「ジェネリックレコードによる網羅的な switch コンストラクト」セクションで説明済みです。

概要

IntelliJ IDEAは、開発者が最新の Java 機能を使用できるように認知負荷の軽減に取り組んでいます。 IntelliJ IDEA 2023.1 は、「switch のパターンマッチング」や「レコードパターン」などの言語機能に対する Java 20 の変更点をサポートしています。 これらの機能で最も重要な変更は、拡張 for ヘッダーでレコードパターンを使用できる機能とジェネリックレコードパターンの型引数の型推論への改善です。

Happy Coding!

オリジナル(英語)ブログ投稿の作者:

image description