CodeGym /コース /JAVA 25 SELF /switch のパターンマッチング (Java 17/21+)

switch のパターンマッチング (Java 17/21+)

JAVA 25 SELF
レベル 65 , レッスン 2
使用可能

1. 古典的な switch: 過去の制約

pattern matching が登場する前の Java の switch がどのようなものだったかを思い出してみましょう。率直に言えば、かなり保守的でした。扱えるのはプリミティブ(intchar、後に enumString)だけで、異なる型のオブジェクトを処理したい場合は、長い if-else を書く必要がありました。

Object obj = ...;
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println("文字列: " + s);
} else if (obj instanceof Integer) {
    Integer i = (Integer) obj;
    System.out.println("数値: " + i);
} else if (obj == null) {
    System.out.println("null!");
} else {
    System.out.println("その他");
}

便利でしょうか? それほどでもありません。コードは冗長でミスもしやすく、新しい型が増えたら if-else の連鎖を書き直さなければなりません。

2. switch のパターンマッチング: Java 17/21+ の革命

Java 17(プレビュー)で導入され、Java 21+ で正式になった新しい switch は、パターンマッチングをサポートします。これにより、switch はプリミティブだけでなく、任意のオブジェクトや型、さらには null にも対応し、追加条件(ガード式)も利用できます。

新しい構文: 簡潔さと強力さ

switch による簡単なパターンマッチングの例:

Object obj = ...;
switch (obj) {
    case String s -> System.out.println("文字列: " + s);
    case Integer i -> System.out.println("数値: " + i);
    case null -> System.out.println("null!");
    default -> System.out.println("その他");
}

ここで何が起きているのか?

  • obj が文字列なら、変数 s は自動で String 型になります。
  • obj が整数なら、変数 iInteger になります。
  • objnull なら、専用の分岐が実行されます。
  • それ以外は default です。

なぜ便利なのか?

  • 手動キャストが不要: Java が型安全性を担保します。
  • 記述量が減る: 余計な括弧やキャスト、重複が不要です。
  • より安全: コンパイラが可能な分岐を見落としていないか検査します(特に sealed クラスでは後述)。
  • 新しい型の追加が容易: case を 1 つ足すだけです。

3. switch のパターンマッチング: 基本的な例

例 1: 異なる型の処理

Object obj = 42;

switch (obj) {
    case String s -> System.out.println("文字列: " + s);
    case Integer i -> System.out.println("数値: " + i);
    case null -> System.out.println("これは null");
    default -> System.out.println("未知の型");
}

出力:

数値: 42

例 2: ガード式 (when) の使用

型を知るだけでは不十分で、追加の条件を付けたいことがあります。

Object obj = "Hello, world!";

switch (obj) {
    case String s when s.length() > 10 -> System.out.println("長い文字列: " + s);
    case String s -> System.out.println("短い文字列: " + s);
    case Integer i when i > 0 -> System.out.println("正の数: " + i);
    case Integer i -> System.out.println("正でない数: " + i);
    default -> System.out.println("その他");
}

出力:

長い文字列: Hello, world!

注意:

  • when はガード式であり、case に対する追加チェックです。
  • 条件が満たされない場合、次の case に進みます。

例 3: null の処理

Object obj = null;

switch (obj) {
    case null -> System.out.println("これは null!");
    default -> System.out.println("null ではない");
}

出力:

これは null!

4. クラスと継承における switch のパターンマッチング

switch のパターンマッチングは、クラス階層との相性が特に良好です。

sealed interface Shape permits Circle, Rectangle, Square {}

final class Circle implements Shape {
    final double radius;
    Circle(double radius) { this.radius = radius; }
}

final class Rectangle implements Shape {
    final double width, height;
    Rectangle(double width, double height) { this.width = width; this.height = height; }
}

final class Square implements Shape {
    final double side;
    Square(double side) { this.side = side; }
}
Shape shape = new Rectangle(2, 3);

String description = switch (shape) {
    case Circle c -> "半径 " + c.radius + " の円";
    case Rectangle r -> "長方形 " + r.width + "x" + r.height;
    case Square s -> "一辺 " + s.side + " の正方形";
};
System.out.println(description);

出力:

長方形 2.0x3.0

ポイント: sealed クラスを使い、すべてのバリアントを処理している場合は、default 分岐は不要です。コンパイラが「他の可能性はない」と判断できます。

5. record クラスとネストしたパターンでの switch の利用

switch のパターンマッチングは、record クラスやネストしたパターン(Java 21+ 以降)もサポートします。

record Point(int x, int y) {}
record Line(Point start, Point end) {}

Object obj = new Line(new Point(1, 2), new Point(3, 4));

String result = switch (obj) {
    case Line(Point(int x1, int y1), Point(int x2, int y2)) ->
        "(" + x1 + "," + y1 + ") から (" + x2 + "," + y2 + ") までの線";
    case Point(int x, int y) -> "点 (" + x + "," + y + ")";
    default -> "不明なオブジェクト";
};
System.out.println(result);

出力:

(1,2) から (3,4) までの線

6. switch におけるパターンマッチングの特徴と制約

変数は該当 case の内部でのみ有効

パターンで宣言された変数(例: Strings)は、対応する case ブロック内でのみ有効です。ブロック外では参照できません。

switch (obj) {
    case String s -> System.out.println(s); // ここでは s が見える
    // System.out.println(s); // ここはコンパイルエラー!
}

null の扱い

  • 従来のオブジェクトに対する switch では、null を渡すと NullPointerException が発生していました。
  • 新しいパターンマッチングの switch では、case nullnull を明示的に処理できます。

ガード式 (when)

追加条件(when)は、expression スタイルの switch(矢印構文 ->)でのみ利用できます。

case String s when s.isEmpty() -> System.out.println("空文字列");

JDK と IDE の要件

  • switch のパターンマッチングは、Java 17(プレビュー)および Java 21+(安定)で提供されます。
  • 動作させるには JDK 21 と、新機能をサポートする IDE(例: IntelliJ IDEA 2023+)が必要です。

7. 実アプリでのパターンマッチング switch の活用

学習用アプリとして、シンプルなタスクトラッカーを考えます。抽象的なタスク型とその種類があるとします:

sealed interface Task permits SimpleTask, DeadlineTask, MeetingTask {}

final class SimpleTask implements Task {
    final String description;
    SimpleTask(String description) { this.description = description; }
}

final class DeadlineTask implements Task {
    final String description;
    final java.time.LocalDate deadline;
    DeadlineTask(String description, java.time.LocalDate deadline) {
        this.description = description;
        this.deadline = deadline;
    }
}

final class MeetingTask implements Task {
    final String topic;
    final java.time.LocalTime time;
    MeetingTask(String topic, java.time.LocalTime time) {
        this.topic = topic;
        this.time = time;
    }
}

ロジックは 1 つのコンパクトな switch にまとまります:

Task task = new DeadlineTask("プロジェクトを提出する", java.time.LocalDate.of(2024, 7, 1));

String info = switch (task) {
    case SimpleTask st -> "通常のタスク: " + st.description;
    case DeadlineTask dt -> "締切付きタスク: " + dt.description + ", 期限 " + dt.deadline;
    case MeetingTask mt -> "ミーティング: " + mt.topic + " の時刻 " + mt.time;
};
System.out.println(info);

出力:

締切付きタスク: プロジェクトを提出する, 期限 2024-07-01

8. switch のパターンマッチングでよくある誤り

誤り 1: null の処理を忘れる。 objnull になり得るのに case null を追加しないと、MatchException(あるいは古い switch では NullPointerException)が発生します。可能であれば常に null を明示的に処理しましょう。

誤り 2: sealed クラスの全派生型を考慮していない。 sealed クラスを使っているのに、switch で全バリアントを処理していない場合、コンパイラはエラーにしたり、default 分岐を要求したりします。階層を拡張したら、新しい case を忘れずに追加してください。

誤り 3: パターン変数は case ブロック外では使えない。 パターンで宣言された変数は、矢印ブロック内でのみ有効です。ブロック外で使おうとするとコンパイルエラーになります。

誤り 4: 古い JDK や IDE で新機能を使おうとする。 switch のパターンマッチングには Java 21+ と、それをサポートする IDE が必要です。古いバージョンを使うと、コンパイルエラーや不正確な補完が発生します。

誤り 5: ガード式の取り扱いを混同する。 古いブロックスタイルの switchwhen を使おうとしていませんか?できません。ガードは矢印構文のみに対応しています(->)。

誤り 6: オープンな階層で default 分岐を忘れる。 switchsealed 以外の階層も扱う場合は、必ず default を加えて、全ての可能性をカバーしてください。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION