1. 概要

Java SE 17リリースでは、プレビュー機能として switch 式およびステートメント( JEP 406 )のパターンマッチングが導入されています。 パターンマッチングは、スイッチケースの条件を定義する際の柔軟性を高めます。

パターンを含めることができるケースラベルに加えて、セレクター式は少数のタイプに限定されなくなりました。 パターンマッチングの前は、 switch のケースは、定数値と正確に一致する必要があるセレクター式の単純なテストのみをサポートしていました。

このチュートリアルでは、switchステートメントに適用できる3つの異なるパターンタイプについて説明します。 また、すべての値のカバー、サブクラスの順序付け、null値の処理など、switchの詳細についても説明します。

2. Switchステートメント

Javaでswitchを使用して、いくつかの事前定義されたcaseステートメントの1つに制御を転送します。 どのステートメントが選択されるかは、switchセレクター式の値によって異なります。

以前のバージョンのJavaでは、セレクター式は、数値、文字列、または定数である必要がありました。 また、ケースラベルには定数のみを含めることができます。

final String b = "B";
switch (args[0]) {
    case "A" -> System.out.println("Parameter is A");
    case b -> System.out.println("Parameter is b");
    default -> System.out.println("Parameter is unknown");
};

この例では、変数bが final でない場合、コンパイラーは定数式に必要なエラーをスローします。

3. パターンマッチング

一般に、パターンマッチングは、JavaSE14のプレビュー機能として最初に導入されました。

これは、パターンの1つの形式(タイプパターン)のみに制限されていました。 典型的なパターンは、タイプ名と結果をバインドする変数で構成されます。

instanceof演算子に型パターンを適用すると、型のチェックとキャストが簡単になります。 さらに、両方を1つの式に組み合わせることができます。

if (o instanceof String s) {
    System.out.printf("Object is a string %s", s);
} else if (o instanceof Number n) {
    System.out.printf("Object is a number %n", n);
}

この組み込みの言語拡張機能により、コードの記述が少なくなり、読みやすさが向上します。

4. スイッチのパターン

instanceof のパターンマッチングは、JavaSE16の永続的な機能になりました。

Java 17では、パターンマッチングのアプリケーションもスイッチ式に拡張されています

ただし、それでもプレビュー機能であるため、プレビューを有効にして使用する必要があります。

java --enable-preview --source 17 PatternMatching.java

4.1. タイプパターン

タイプパターンと演算子のインスタンスをswitchステートメントに適用する方法を見てみましょう。

例として、 if-else ステートメントを使用して、さまざまなタイプをdoubleに変換するメソッドを作成します。 タイプがサポートされていない場合、このメソッドは単にゼロを返します。

static double getDoubleUsingIf(Object o) {
    double result;
    if (o instanceof Integer) {
        result = ((Integer) o).doubleValue();
    } else if (o instanceof Float) {
        result = ((Float) o).doubleValue();
    } else if (o instanceof String) {
        result = Double.parseDouble(((String) o));
    } else {
        result = 0d;
    }
    return result;
}

switch の型パターンを使用すると、少ないコードで同じ問題を解決できます。

static double getDoubleUsingSwitch(Object o) {
    return switch (o) {
        case Integer i -> i.doubleValue();
        case Float f -> f.doubleValue();
        case String s -> Double.parseDouble(s);
        default -> 0d;
    };
}

以前のバージョンのJavaでは、セレクター式は少数のタイプに制限されていました。 ただし、タイプパターンでは、スイッチセレクター式は任意のタイプにすることができます。

4.2. ガードパターン

タイプパターンは、特定のタイプに基づいて制御を転送するのに役立ちます。 ただし、場合によっては、渡された値に対して追加のチェックを実行する必要もあります。

たとえば、 if ステートメントを使用して、Stringの長さを確認できます。

static double getDoubleValueUsingIf(Object o) {
    return switch (o) {
        case String s -> {
            if (s.length() > 0) {
                yield Double.parseDouble(s);
            } else {
                yield 0d;
            }
        }
        default -> 0d;
    };
}

ガードされたパターンを使用して同じ問題を解決できます。 パターンとブール式の組み合わせを使用します。

static double getDoubleValueUsingGuardedPatterns(Object o) {
    return switch (o) {
        case String s && s.length() > 0 -> Double.parseDouble(s);
        default -> 0d;
    };
}

保護されたパターンにより、switchステートメントの追加のif条件を回避できます。 代わりに、 条件付きロジックをケースラベルに移動します

4.3. 括弧で囲まれたパターン

ケースラベルに条件付きロジックがあることに加えて、括弧で囲まれたパターンにより、それらをグループ化することができます

追加のチェックを実行するときは、ブール式で括弧を使用するだけです。

static double getDoubleValueUsingParenthesizedPatterns(Object o) {
    return switch (o) {
        case String s && s.length() > 0 && !(s.contains("#") || s.contains("@")) -> Double.parseDouble(s);
        default -> 0d;
    };
}

括弧を使用することで、追加のif-elseステートメントを回避できます。

5. スイッチの詳細

次に、switchでパターンマッチングを使用する際に考慮すべきいくつかの特定のケースを見てみましょう。

5.1. すべての値をカバー

switch でパターンマッチングを使用する場合、Javaコンパイラはタイプカバレッジをチェックします。

任意のオブジェクトを受け入れるが、Stringの場合のみをカバーするswitch条件の例を考えてみましょう。

static double getDoubleUsingSwitch(Object o) {
    return switch (o) {
        case String s -> Double.parseDouble(s);
    };
}

この例では、次のコンパイルエラーが発生します。

[ERROR] Failed to execute goal ... on project core-java-17: Compilation failure
[ERROR] /D:/Projects/.../HandlingNullValuesUnitTest.java:[10,16] the switch expression does not cover all possible input values

これは、スイッチ ケースラベルには、セレクター式のタイプを含める必要があるためです。

特定のセレクタータイプの代わりに、defaultケースラベルを適用することもできます。

5.2. サブクラスの注文

スイッチでパターンマッチングのあるサブクラスを使用する場合、ケースの順序が重要になります

Stringの場合がCharSequenceの場合の後に来る例を考えてみましょう。

static double getDoubleUsingSwitch(Object o) {
    return switch (o) {
        case CharSequence c -> Double.parseDouble(c.toString());
        case String s -> Double.parseDouble(s);
        default -> 0d;
    };
}

StringCharSequenceのサブクラスであるため、の例では次のコンパイルエラーが発生します。

[ERROR] Failed to execute goal ... on project core-java-17: Compilation failure
[ERROR] /D:/Projects/.../HandlingNullValuesUnitTest.java:[12,18] this case label is dominated by a preceding case label

このエラーの背後にある理由は、メソッドに渡された文字列オブジェクトは最初のケース自体で処理されるため、実行が2番目のケースに進む可能性がないためです。

5.3. ヌル値の処理

以前のバージョンのJavaでは、null値をswitchステートメントに渡すたびに、NullPointerExceptionが発生していました。

ただし、タイプパターンを使用すると、nullチェックを個別のケースラベルとして適用できるようになりました。

static double getDoubleUsingSwitch(Object o) {
    return switch (o) {
        case String s -> Double.parseDouble(s);
        case null -> 0d;
        default -> 0d;
    };
}

null固有のケースラベルがない場合、合計タイプのパターンラベルはnull値と一致します。

static double getDoubleUsingSwitchTotalType(Object o) {
    return switch (o) {
        case String s -> Double.parseDouble(s);
        case Object ob -> 0d;
    };
}

switch 式は、nullケースとtotalタイプのケースの両方を持つことができないことに注意してください。

このようなswitchステートメントは、次のコンパイルエラーを引き起こします。

[ERROR] Failed to execute goal ... on project core-java-17: Compilation failure
[ERROR] /D:/Projects/.../HandlingNullValuesUnitTest.java:[14,13] switch has both a total pattern and a default label

最後に、パターンマッチングを使用する switch ステートメントは、NullPointerExceptionをスローする可能性があります。

ただし、これができるのは、switchブロックにnullが一致する大文字と小文字のラベルがない場合のみです。

6. 結論

この記事では、 Java SE 17 のプレビュー機能であるスイッチ式とステートメントのパターンマッチングについて説明しました。ケースラベルでパターンを使用すると、その選択はパターンによって決定されることがわかりました。単純な同等性チェックではなく、マッチング。

例では、switchステートメントに適用できる3つの異なるパターンタイプについて説明しました。 最後に、すべての値のカバー、サブクラスの順序付け、null値の処理など、いくつかの特定のケースを調査しました。

いつものように、完全なソースコードはGitHubから入手できます。