5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

JavaAdvent Calendar 2023

Day 24

【Java】switch式とswitchパターンマッチング

Last updated at Posted at 2023-12-23

はじめに

Java Advent Calendar 24日目の記事です。よろしくお願いします。

「instanceofのパターンマッチング」(この記事)について調べている時にswitch""があることを初めて知ったので、Javaのバージョンごとにswitchをどう使えるかざっくりと調べてみました。

switchパターンマッチングが知りたい方はJava 17まで飛んでください。

Java 12

Preview版としてswitch式が導入される。

そもそもは、switch文のフォールスルーが厄介だったっぽい。

switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        System.out.println(6);
        break;
    case TUESDAY:
        System.out.println(7);
        break;
    case THURSDAY:
    case SATURDAY:
        System.out.println(8);
        break;
    case WEDNESDAY:
        System.out.println(9);
        break;
}

このようなコードって分かりにくいしバグの温床ですよねと。

そこで、case L ->の構文(Arrow label)が提案されてます。

switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
    case TUESDAY                -> System.out.println(7);
    case THURSDAY, SATURDAY     -> System.out.println(8);
    case WEDNESDAY              -> System.out.println(9);
}

これはcaseのラベルが一致した時にラベルの右側のコードだけが実行されます。

ついでにswitch文を拡張してとしても拡張します。

T result = switch (arg) {
    case L1 -> e1;
    case L2 -> e2;
    default -> e3;
};

式なので、:arrow_down:のように書けたりもします。

static void howMany(int k) {
    System.out.println(
        switch (k) {
            case  1 -> "one"
            case  2 -> "two"
            default -> "many"
        }
    );
}

ここで、switch式のcaseは網羅的でなければいけません。

default句があれば問題ありませんが、網羅していないとコンパイルエラーになります。

public class SwitchSample {

	private enum WEEK {
 
		MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
	}

	public static void main(String[] args) {

		WEEK day = WEEK.MONDAY;

        // 網羅しているので問題なし
		int numLetters = switch (day) {
			case MONDAY, FRIDAY, SUNDAY -> 6;
			case TUESDAY -> 7;
			case THURSDAY, SATURDAY -> 8;
			case WEDNESDAY -> 9;
		};

        // 網羅していないのでコンパイルエラー
		int numLetters1 = switch (day) {
			case TUESDAY -> 7;
			case THURSDAY, SATURDAY -> 8;
			case WEDNESDAY -> 9;
		};

        // default句で網羅しているので問題なし
		int numLetters1 = switch (day) {
			case TUESDAY -> 7;
			case THURSDAY, SATURDAY -> 8;
			case WEDNESDAY -> 9;
			default -> throw new IllegalStateException("Wat: " + day);
		};
	}

}

->switch式2つを導入したおかげで、やりたいことが分かりにくかった従来のコードもスッキリです。

従来.java
int numLetters;
switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        numLetters = 6;
        break;
    case TUESDAY:
        numLetters = 7;
        break;
    case THURSDAY:
    case SATURDAY:
        numLetters = 8;
        break;
    case WEDNESDAY:
        numLetters = 9;
        break;
    default:
        throw new IllegalStateException("Wat: " + day);
}
今後.java
int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY                -> 7;
    case THURSDAY, SATURDAY     -> 8;
    case WEDNESDAY              -> 9;
    default                     -> throw new IllegalStateException("Wat: " + day);
};

式を複数書くようなブロックが必要な場合は、breakの後に書いた値が戻り値となります。

int j = switch (day) {
    case MONDAY  -> 0;
    case TUESDAY -> 1;
    default      -> {
        int k = day.toString().length();
        int result = f(k);
        break result; // breakで値を返却
    }
};

まとめると

  • ->が導入されて、フォールスルーが無いように書けるようになった。
  • switch式が導入された。
    • ブロックを使いたい場合は、break 値で値を返せる。

Java 13

まだPreview版ですが、switch式に変更がありました。

switch式でブロックを使う場合、Java 12のPreview版ではbreak 値で値を返却していましたが、代わりにyield文で返すようになりました。

int j = switch (day) {
    case MONDAY  -> 0;
    case TUESDAY -> 1;
    default      -> {
        int k = day.toString().length();
        int result = f(k);
        yield result; // yieldで値を返却
    }
};

Java 14

LTSであるJava 14でswitch式が(Preview版ではなく)導入されました。

Java 13までに提案されていたPreview版のswitch式から変更はありません。
要は、、、

  • Arrow label->が導入されました。
  • switch式が導入されました。
    • ラベルを網羅していないとコンパイルエラーが発生します。
  • yield文が導入されました。

です。

Java 17

Preview版でswitchパターンマッチングが導入されました。

switchパターンマッチングとは簡単に言うと、Java 16で導入されたinstanceofパターンマッチングswitch版です。

Java 16で導入したinstanceofパターンマッチングを使って:arrow_down:のように書く内容を、、、

従来.java
static String formatter(Object o) {
    String formatted = "unknown";
    if (o instanceof Integer i) {
        formatted = String.format("int %d", i);
    } else if (o instanceof Long l) {
        formatted = String.format("long %d", l);
    } else if (o instanceof Double d) {
        formatted = String.format("double %f", d);
    } else if (o instanceof String s) {
        formatted = String.format("String %s", s);
    }
    return formatted;
}

:arrow_down:のように書けてスッキリします。

今後.java
static String formatterPatternSwitch(Object o) {
    return switch (o) {
        case Integer i -> String.format("int %d", i);
        case Long l    -> String.format("long %d", l);
        case Double d  -> String.format("double %f", d);
        case String s  -> String.format("String %s", s);
        default        -> o.toString();
    };
}

この拡張に合わせてswitchがいろいろ拡張されています。

使える型の拡張

そもそもは、switchcaseに使える型は以下のいずれかでした。

  • 整数のプリミティブ型(char, byte, short, int)
  • 上のラッパ(Character, Byte, Short, Integer)
  • String
  • enum

これを拡張して、整数のプリミティブ型か任意の参照型を使えるようになります。

nullの扱い

nullの評価について、従来は外側で行う必要がありました。

static void testFooBar(String s) {
    if (s == null) {
        System.out.println("oops!");
        return;
    }
    switch (s) {
        case "Foo", "Bar" -> System.out.println("Great");
        default           -> System.out.println("Ok");
    }
}

が、case nullを書けるようになり、nullの評価ができるようになります。

static void testFooBar(String s) {
    switch (s) {
        case null         -> System.out.println("Oops");
        case "Foo", "Bar" -> System.out.println("Great");
        default           -> System.out.println("Ok");
    }
}

いままでだとsnullの時は、NullPointerExceptionが発生してました。

補足

後方互換性を維持するためにswitchの動きは以下のようになります。

  • defaultは今まで通りnullに該当しません。
  • セレクタ(switch(s)sの部分)がnullと評価された場合、case nullcase Objectのような総パターンのcase **に該当するとみなしてcaseを探索します。そのようなcaseがない場合は、NullPointerExceptionが発生します。
  • セレクタがnullでないと評価された場合、該当するcase **を探します。

guarded patternの追加

また、switchのパターンを洗練する取り組みも行われました。

先にguarded patternとやらの導入前のコードから書きます。

これはShapeの変数sが「Triangle型でかつ、面積calculateArea100より大きい場合」とそれ以外で分岐するswitch文です。

class Shape {}
class Rectangle extends Shape {}
class Triangle  extends Shape { int calculateArea() { ... } }

static void testTriangle(Shape s) {
    switch (s) {
        case null:
            break;
        case Triangle t:
            if (t.calculateArea() > 100) {
                System.out.println("Large triangle");
                break;
            }
        default:
            System.out.println("A shape, possibly a small triangle");
    }
}

ifブロック内にbreakをおいてフォールスルーを使っていることに注目です。
ややこしいですね。

これに対して、「パターンpをboolean式bで絞り込む」p && bが導入されました(これをguarded patternと言います)。

guarded patternを使うことで:arrow_down:のように書き直せます。

static void testTriangle(Shape s) {
    switch (s) {
        case Triangle t && (t.calculateArea() > 100) ->
            System.out.println("Large triangle");
        default ->
            System.out.println("A shape, possibly a small triangle");
    }
}

パターンの優位性

パターンマッチングを導入したことにより、case ****に複数該当する可能性が出てきます。

例えば:arrow_down:のコードを見てみましょう。

static void error(Object o) {
    switch(o) {
        case CharSequence cs ->
            System.out.println("A sequence of length " + cs.length());
        case String s -> 
            System.out.println("A string: " + s);
        default -> {
            break;
        }
    }
}

StringCharSequenceの実装クラスなので、case Stringに該当するならばcase CharSequenceに該当します。
caseは上から評価されていくため、:arrow_up:のコードだとcase Stringにマッチすることができません。
case Stringを先に書かないとコンパイルエラーとなります。

また、guarded patternですが、例えばcase String s && s.length() > 0case Stringに含まれるので、case String s && s.length() > 0を先にする必要があります。

要は到達不能なcaseを作っちゃダメということですかね。
try-catchExceptionをキャッチする時にサブクラスの方からcatchしないとコンパイルエラーになるやつと考え方は一緒だと思います。

その他

その他にも、JEPでは「パターンの網羅性」と「パターン変数のスコープ」について提言されています。
ざっくりいうと、

  • 「パターンの網羅性」
    • caseで取りうるパターンを完全に網羅するようにしなくちゃダメだよ
    • だいたいdefault句をつければいいよ
  • 「パターン変数のスコープ」
    • caseの後がパターン変数のスコープだよ
    • switch文を使う時にフォールスルーで変数が初期化されない可能性がある時はコンパイルエラーにするよ

ってことを言っています。
パターンの網羅性についてはsealedなクラスと相性がいいんですね。

Java 18

さてさて、Preview版ですが引き続きswitchのパターンマッチングが導入されてます。

変更点

Java 17からの主な変更点は以下になります。

  • パターンの優位性チェックに定数のチェックが入った
    • 例えば、case Integer icase 42を含みます
    • 例えば、case E ecase Aを含みます(Aenum Eの定数)
  • sealedな階層構造を持つswitchブロックの網羅性チェックがより正確になった

2つ目が分かりにくいですね。
例えば、以下のようなsealedな階層構造があったとします。

sealed interface I<T> permits A, B {}
final class A<X> implements I<String> {}
final class B<Y> implements I<Y> {}

この時に、以下のようなswitchブロックを考えると、

static int testGenericSealedExhaustive(I<Integer> i) {
    return switch (i) {  
        case B<Integer> bi -> 42;
    }
}

変数iの型I<T>I<Integer>となっているので、網羅性チェックではI<Y>を実装しているB<Y>のみをコンパイラはチェックします。A<X>I<String>を実装しているので不要です。

Java 19

引き続きPreview版です。

変更点

Java 18からの変更点は以下になります。

  • guarded pattern(p && bのやつ)がwhenに変更
    • case p when bと書くようになります

Java 20

引き続きPreview版です。

変更点

Java 19からの主な変更点は以下の通りです。

  • 列挙型が網羅されずに実行時にcase **に当てはまらなかった場合にスローされる例外が、IncompatibleClassChangeErrorからMatchExceptionに変更
  • ジェネリクスを使ったrecord patternに対する型引数の推論がswitch式とswitch文もサポートするようになった

2番目のrecordに対する話ですが、:arrow_down:が関係してます。

例えば、:arrow_down:のようにinstanceofパターンマッチングでRecordのパターン変数Point pを抽出してます。

record Point(int x, int y) {}

static void printSum(Object obj) {
    if (obj instanceof Point p) {
        int x = p.x();
        int y = p.y();
        System.out.println(x+y);
    }
}

このコードでは、pはフィールド変数xyにアクセスするために抽出してるだけです。

record patternとは、Point(int x, int y)のように書くことでフィールド変数を初期化して抽出してくれる仕組みのことです。

static void printSum(Object obj) {
    if (obj instanceof Point(int x, int y)) {
        System.out.println(x+y);
    }
}

で、本題に戻ってswitchでも:arrow_down:のように書けます。

record MyPair<S,T>(S fst, T snd){};

static void recordInference(MyPair<String, Integer> pair){
    switch (pair) {
        case MyPair(var f, var s) -> 
            ... // Inferred record Pattern MyPair<String,Integer>(var f, var s)
        ...
    }
}

case MyPair(var f, var s)としていますが、pairの型がMyPair<String, Integer>なので推測してくれます。

Java 21

LTSのJava 21で正式にリリースされました。

変更点

Java 20からの主な変更点は以下になります。

  • 修飾されたenum定数をcase ****に使うことを許可する

どういうことかというと、、
:arrow_down:今まではWEEK.MONDAYのように修飾したenum定数をcaseで書くことが許されておらずコンパイルエラーが出てしまっていました。

public class SwitchSample {

	private enum WEEK {
		MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
	}

	public static void main(String[] args) {

		WEEK day = WEEK.MONDAY;

		int numLetters = switch (day) {
			case WEEK.MONDAY, WEEK.FRIDAY, WEEK.SUNDAY -> 6; // コンパイルエラー
			case TUESDAY -> 7;
			case THURSDAY, SATURDAY -> 8;
			case WEDNESDAY -> 9;
			default -> throw new IllegalStateException("Wat: " + day);
		};
	}
}

The qualified case label SwitchSample.WEEK.MONDAY must be replaced with the unqualified enum constant MONDAY

Java 21以降では修飾を可にするということです。

これはenum型のswitchを使う時に冗長性の排除につながるらしいです。
:arrow_down:こう書かなければならないコードが、、

sealed interface CardClassification permits Suit, Tarot {}
public enum Suit implements CardClassification { CLUBS, DIAMONDS, HEARTS, SPADES }
final class Tarot implements CardClassification {}

static void exhaustiveSwitchWithoutEnumSupport(CardClassification c) {
    switch (c) {
        case Suit s when s == Suit.CLUBS -> {
            System.out.println("It's clubs");
        }
        case Suit s when s == Suit.DIAMONDS -> {
            System.out.println("It's diamonds");
        }
        case Suit s when s == Suit.HEARTS -> {
            System.out.println("It's hearts");
        }
        case Suit s -> {
            System.out.println("It's spades");
        }
        case Tarot t -> {
            System.out.println("It's a tarot");
        }
    }
}

:arrow_down:こう書ける

static void exhaustiveSwitchWithBetterEnumSupport(CardClassification c) {
    switch (c) {
        case Suit.CLUBS -> {
            System.out.println("It's clubs");
        }
        case Suit.DIAMONDS -> {
            System.out.println("It's diamonds");
        }
        case Suit.HEARTS -> {
            System.out.println("It's hearts");
        }
        case Suit.SPADES -> {
            System.out.println("It's spades");
        }
        case Tarot t -> {
            System.out.println("It's a tarot");
        }
    }
}

おわりに

平仄があってないのはすいません。
明らかな誤りや補足があれば、コメントいただけると嬉しいです。

結論だけ書いてもよかったんですが、流れがあった方が個人的に頭に入りやすいのでJavaのバージョンごとに書いてみました。

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?