Javaにおける条件分岐の代表格といえばif文とswitch文ですが、近年のJavaアップデートによりswitchの機能は劇的な進化を遂げました。

従来の「単なる値の一致確認」という役割を超え、現在では「データ構造に基づいた柔軟な条件分岐」を可能にするパターンマッチングへと昇華されています。

本記事では、初心者向けの基礎知識から、Java 21以降で標準化された最新の機能まで、エンジニアが実務で活用できるレベルで詳しく解説します。

Javaのswitch文とは:基本概念と役割

Javaのswitch文は、一つの変数や式に対して複数の値を比較し、一致するブロックの処理を実行するための制御構文です。

多岐にわたる条件分岐を記述する場合、if-else ifを並べるよりもコードの見通しが良くなるというメリットがあります。

もともとJavaのswitchは、整数のプリミティブ型や列挙型(Enum)、Stringクラスといった限定的な型のみを対象としていました。

しかし、Java 12から始まったswitch式(Switch Expressions)の導入や、Java 17・Java 21でのパターンマッチング(Pattern Matching)の追加により、その利便性は飛躍的に向上しました。

現在のJavaにおいてswitchをマスターすることは、単に古い文法を知ることではなく、モダンなJavaプログラミングの手法を身につけることに直結します。

従来のswitch文(ステートメント)の使い方

まずは、Javaの初期から存在する伝統的なswitch文の書き方をおさらいしましょう。

この形式は「switchステートメント」と呼ばれ、各ケースの最後にbreakを記述して処理を抜ける必要があります。

基本的な構文

Java
public class TraditionalSwitch {
    public static void main(String[] args) {
        int dayOfWeek = 2;
        String dayName;

        // 従来のswitch文
        switch (dayOfWeek) {
            case 1:
                dayName = "Monday";
                break;
            case 2:
                dayName = "Tuesday";
                break;
            case 3:
                dayName = "Wednesday";
                break;
            default:
                dayName = "Unknown";
                break;
        }

        System.out.println("Day: " + dayName);
    }
}
実行結果
Day: Tuesday

従来のswitch文における注意点

従来のswitch文には、いくつか注意すべき「罠」が存在します。

フォールスルー(Fall-through)

breakを書き忘れると、次のcaseラベルの処理まで連続して実行されてしまいます。

これは意図しないバグの温床になりやすいポイントです。

スコープの不明瞭さ

すべてのcaseが同じブロックに属しているため、変数名の重複を避けるのが難しい場合があります。

冗長なコード

値を割り当てるためだけに、何度も変数への代入処理を書かなければなりません。

これらの課題を解決するために導入されたのが、次節で解説する「switch式」です。

モダンJavaの旗手:switch式(Switch Expressions)

Java 14で正式に標準化されたswitch式は、文(Statement)としてではなく「式(Expression)」として値を返すことができます。

これにより、コードの簡潔さと安全性が格段に向上しました。

アロー構文(->)による簡略化

switch式では、従来のコロン(:)の代わりにアロー(->)を使用します。

アロー構文を使用すると、break文が不要になり、フォールスルーの問題が完全に解消されます。

Java
public class SwitchExpression {
    public static void main(String[] args) {
        int month = 4;

        // switchを変数代入に直接利用できる
        String season = switch (month) {
            case 12, 1, 2 -> "Winter";
            case 3, 4, 5  -> "Spring";
            case 6, 7, 8  -> "Summer";
            case 9, 10, 11 -> "Autumn";
            default       -> "Invalid month";
        };

        System.out.println("The season is: " + season);
    }
}
実行結果
The season is: Spring

yieldによる複数行の処理

switch式の中で、単一の値ではなく複雑な計算を行いたい場合は、中括弧 {}yield キーワードを使用します。

yield は、そのブロックから返す値を指定するために使われます。

Java
public class YieldExample {
    public static void main(String[] args) {
        String input = "LOG";
        
        int result = switch (input) {
            case "START" -> 1;
            case "STOP"  -> 0;
            case "LOG"   -> {
                System.out.println("Logging process started...");
                yield 100; // 戻り値を指定
            }
            default -> -1;
        };

        System.out.println("Result code: " + result);
    }
}
実行結果
Logging process started...
Result code: 100

網羅性(Exhaustiveness)のチェック

switch式を使用する際の最も強力なメリットの一つが、コンパイラによる網羅性チェックです。

switch式が値を返す以上、あらゆる入力に対して必ず何らかの値を返さなければなりません。

そのため、defaultケースを記述するか、列挙型(Enum)や封印クラス(Sealed Class)ですべてのパターンをカバーすることが強制されます。

Java 21以降の最新機能:パターンマッチング

Java 17からプレビューが始まり、Java 21で正式に導入されたパターンマッチング(Pattern Matching for switch)は、switchの概念を根本から変えました。

これにより、型に基づいて分岐させるだけでなく、同時に変数のキャストまで自動で行えるようになります。

型パターンの活用

従来のJavaでは、オブジェクトの型を判定するためにinstanceofを使い、その後に明示的なキャストが必要でした。

パターンマッチングを使えば、これらを1行で記述できます。

Java
public class PatternMatchingSwitch {
    public static void main(String[] args) {
        Object obj = "Hello Java 21";

        String result = switch (obj) {
            case Integer i -> String.format("Integer: %d", i);
            case String s  -> String.format("String of length %d: %s", s.length(), s);
            case Double d  -> String.format("Double: %f", d);
            default        -> "Unknown type";
        };

        System.out.println(result);
    }
}
実行結果
String of length 13: Hello Java 21

このコードでは、case String s の部分で「もし objString 型であれば、それを変数 s に代入して使用する」という処理が行われています。

キャストの手間が省けるだけでなく、型安全性が保証されるのが大きな特徴です。

ガード条件(when句)の導入

特定の型であることに加え、さらに詳細な条件を指定したい場合には when 句(ガード条件)を使用します。

これにより、if文をネストさせる必要がなくなり、宣言的な記述が可能になります。

Java
public class GuardConditions {
    public static void main(String[] args) {
        Object obj = 150;

        String description = switch (obj) {
            case Integer i when i > 100 -> "Large Integer";
            case Integer i when i > 0   -> "Positive Integer";
            case Integer i              -> "Non-positive Integer";
            case String s               -> "A string: " + s;
            default                     -> "Not an integer or string";
        };

        System.out.println(description);
    }
}
実行結果
Large Integer

null値のハンドリング

従来のswitch文では、対象の変数が null の場合に NullPointerException が発生していました。

最新のJavaでは、case null を直接記述できるようになっています。

Java
public String handleInput(String input) {
    return switch (input) {
        case null    -> "Input is null";
        case "valid" -> "Process starts";
        default      -> "Invalid input";
    };
}

これにより、switch文の前にわざわざ if (input == null) というチェックを書く必要がなくなりました。

レコードパターン(Record Patterns)によるデータ分解

Java 21で正式導入されたもう一つの強力な機能が、レコードパターンです。

Javaの record クラス(不変のデータを保持するためのクラス)とswitchを組み合わせることで、データ構造を「分解(解体)」しながら処理できます。

実装例:図形オブジェクトの処理

以下の例では、点を表すレコード Point と、それを構成要素に持つ Circle を定義し、switchで分解して処理しています。

Java
record Point(double x, double y) {}
record Circle(Point center, double radius) {}

public class RecordPatternExample {
    public static void main(String[] args) {
        Object shape = new Circle(new Point(10.0, 20.0), 5.0);

        if (shape instanceof Circle(Point(double x, double y), double r)) {
            System.out.printf("Circle at (%.1f, %.1f) with radius %.1f%n", x, y, r);
        }

        // switchでの利用
        String description = switch (shape) {
            case Circle(Point(var x, var y), var r) when r > 10 -> "Big circle";
            case Circle(Point(var x, var y), var r)             -> "Normal circle";
            default -> "Unknown shape";
        };
        
        System.out.println(description);
    }
}
実行結果
Circle at (10.0, 20.0) with radius 5.0
Normal circle

このように、ネストされたデータ構造であっても、一気に中身の変数を取り出すことができるのがレコードパターンの凄みです。

これにより、ゲッターメソッドを何度も呼び出すような冗長なコードを排除できます。

switch文とswitch式の使い分け

新しい機能が便利だからといって、常にswitch式を使うべきとは限りません。

状況に応じた使い分けが重要です。

特徴switch文(ステートメント)switch式(エクスプレッション)
戻り値なしあり
構文case value:case value ->
終了処理break が必要不要(自動で終了)
網羅性チェックされないコンパイラにより厳格にチェックされる
用途処理の実行(副作用が目的)値の算出や代入が目的

基本的には、「何か値を返す場合」や「単純な分岐を行う場合」はswitch式を優先的に使用し、副作用(ログ出力やDB操作など)を伴う複雑な一連の手続きを行う場合には、従来のswitch文や if 文を検討するのが良いでしょう。

実践:封印クラス(Sealed Classes)との親和性

パターンマッチングを最大限に活かす手法として、封印クラス(Sealed Classes)との併用が挙げられます。

封印クラスは、継承を許可するクラスを制限する機能です。

これをswitch式と組み合わせると、default ケースを書かずに、安全かつ網羅的な条件分岐が実現できます。

Java
// 継承できるクラスを限定
sealed interface Response permits Success, Error {}

final record Success(String message) implements Response {}
final record Error(int code, String message) implements Response {}

public class SealedSwitch {
    public static void main(String[] args) {
        Response res = new Error(404, "Not Found");

        // 全ての許可された型を網羅していれば default は不要
        String status = switch (res) {
            case Success s -> "Success: " + s.message();
            case Error e   -> "Error " + e.code() + ": " + e.message();
        };

        System.out.println(status);
    }
}

この方法の利点は、将来的に Response インターフェースに新しい実装(例:Pending)が追加された際、コンパイラが「switch式が網羅されていません」とエラーを出してくれる点にあります。

これにより、修正漏れによるバグを未然に防ぐことが可能になります。

switch文を使用する際のベストプラクティス

より美しく、保守性の高いコードを書くためのポイントをまとめます。

可能な限りswitch式を利用する

コードが簡潔になり、break忘れによるバグを根絶できます。

マジックナンバーを避ける

case 1: のように数値を直接書くのではなく、定数やEnumを使用してください。

列挙型(Enum)を積極的に活用する

状態の分岐にはEnumが最適です。

switch式との相性も抜群で、網羅性チェックの恩恵をフルに受けられます。

複雑すぎるガード条件は避ける

when 句の中にあまりに長い論理式を書くと、可読性が低下します。

その場合は、事前に判定ロジックをメソッドとして切り出すことを検討してください。

フォールスルーが必要な場合のみコロン形式を使う

意図的に複数のケースで同じ処理を通したい場合を除き、アロー構文(->)を選択するのが安全です。

まとめ

Javaのswitchは、長い年月を経て「値の単純な比較」から「データの構造を捉える強力な武器」へと進化しました。

従来のswitch文

副作用を伴う処理に利用するが、break忘れに注意が必要。

switch式

アロー構文で簡潔に記述でき、値を返す処理に最適。

パターンマッチング

型判定とキャスト、さらにガード条件を組み合わせた高度な分岐が可能。

レコードパターン

複雑なデータ構造を直感的に分解して処理。

特にJava 21以降を利用できる環境であれば、パターンマッチングと封印クラス、レコードを組み合わせた設計を積極的に取り入れることで、コードの堅牢性と可読性は飛躍的に向上します。

本記事で解説した最新の文法を駆使して、よりクリーンでバグの少ないJavaプログラムを目指しましょう。