
[Java] switchのパターンマッチングにプリミティブ型を使う
こんにちは!
カサレアルでJavaのコースを担当している櫻庭です。
前回の 大きく変化したswitchのすべて で、switchを式として扱えることや、パターンマッチングが導入されて型によって処理の分岐が行えることを紹介しました。
今回はswitchの続きで、今後導入予定のパターンマッチングでプリミティブ型を使えるようになる仕様変更について紹介していきましょう。
OpenJDKでは導入予定の機能をJDK Enhancement Proposal (JEP)でまとめています。たとえば、2026年9月にリリース予定のJava 27では9個のJEPが導入予定です。
JEPには番号がつけられており、Java 27におけるパターンマッチングでプリミティブ型を使う機能は JEP 532 です。
Java 27ではお試し機能ですが、これまでのフィードバックを取り入れてある程度安定してきていることから、次のLTSであるJava 29までには正式な機能になるのではないかと櫻庭は予想しています。
今回は、Java 27を使用して、この機能を試してみましょう。
なお、Java 27ではお試し機能のため、コンパイルや実行時に–enable-previewオプションを指定する必要があります。
プリミティブ型でパターンマッチング
パターンマッチングでプリミティブ型を使う前に、簡単にswitchにおけるパターンマッチングをおさらいしておきましょう。
形状を表すShapeインタフェースが定義されているとし、Shapeインタフェースの実装クラスとしてCircleクラス、Triangleクラス、Squareクラスが定義されているとします。
Shapeインタフェースの実装クラスの型によって処理を分岐させることを考えてみましょう。たとえば、型に応じた文字列を返すのであれば、次のように記述できます。
Shape shape = ......;
String result = switch (shape) {
case Circle c -> "Circle";
case Square s -> "Square";
case Triangle t -> "Triangle";
default -> "Unknown";
};このように、switchでパターンマッチングを使用する場合、caseラベルに型名と値を代入するローカル変数を記述します。
現状のJava 26までは、この型に参照型しか使えませんでした。つまり、caseラベルに記述できるのはクラスやインタフェースだけで、プリミティブ型を使えませんでした。
そこで、プリミティブ型もパターンマッチングで使えるようにしようというのがJEP 532です。
switchで使用できるプリミティブ型の拡充
従来、switchで使えるプリミティブ型は、long型以外の整数型だけでした。
これに対し、パターンマッチングで使用できるように、すべてのプリミティブ型をswitchで使用できるようになりました。
もちろん、パターンマッチングではなく、値で処理の分岐をさせることもできます。
たとえば、以下のコードで示したように、boolean型の変数でもswitchで扱うことができます。
boolean x = ......;
String result = switch (x) {
case true -> "TRUE";
case false -> "FALSE";
};if文と違ってswitch式では値が返せるので、switchでの処理の結果を返す場合はいいかもしれません。値を返さないのだとしたら、櫻庭は従来通りif文使っちゃいそうです。
また、doubleやfloatといった浮動小数点数を扱う場合、比較は==を使う場合と同じです。このため、10進数から2進数に変換する時の誤差などは考慮してくれません。その点はご注意ください。
プリミティブ型での記法
プリミティブ型でパターンマッチングをする場合でも、参照型と同じようにcaseラベルに型とローカル変数を記述します。
もちろん、オートボクシング/オートアンボクシングにも対応しているため、プリミティブ型のラッパークラスの変数をプリミティブ型のcaseラベルで分岐処理することもできます。
たとえば、IntegerクラスやDoubleクラスのスーパークラスであるNumberクラスを使うと、次のようなコードを記述できます。
Number x = ......;
String result = switch (x) {
case byte b -> b +"はbyte";
case short s -> s +"はshort";
case int i -> i + "はint";
case long l -> l + "はlong";
case float f -> f + "はfloat";
case double d -> d + "はdouble";
case BigInteger bi -> bi + "はBigInteger";
default -> x + "はその他の型";
};このコードに対し、
x = 10;とすれば”10はint”が結果として返ります。また、
x = 0.9f;とすれば、結果は”0.9はfloat”となります。
さらに、プリミティブ型をパターンマッチングで使用した場合の特徴として、値での比較と型での比較を混在させることができます。また、参照型のパターンマッチングの場合と同様に、when句を使用して条件を追加することも可能です。
たとえば、int型の変数に対し、値が0の場合、100未満の場合、それ以外の整数の場合で処理を分けることを考えてみましょう。
int x = ......;
String result = switch (x) {
case 0 -> x +"はゼロ";
case int i when i < 100 -> i + "は100未満";
case int i -> i + "は100以上";
};100未満の場合とそれ以上の場合で同じ変数名iを使用しています。矢印(->)の後はこのコードでは省略していますが波括弧でくくられたブロックであり、変数のスコープもそのブロックに限定されます。このため、異なるcaseラベルであれば、同じ変数名を使うことができるのです。
型の変換
プリミティブ型でパターンマッチングといっても、プリミティブ型には継承もないし、複数の型を並べることはないんじゃないのと思いますよね。櫻庭もそう思っていました。
もちろん、上記のNumberクラスを使う例などはあります。しかし、intなどのプリミティブ型で宣言された変数やメソッドの戻り値でパターンマッチングを行う場合、複数の型をcaseラベルに記述することはないはず……
ここで思い出してほしいのが、プリミティブ型の型変換を行うキャストです。
キャストには値の精度が落ちないワイドニングキャストと、精度が落ちてしまうナローイングキャストがあります。
たとえば、byte型の値をint型にキャストするのは精度が落ちないのでワイドニングキャストです。逆に、long型の値をint型にキャストするのはナローイングキャストです。
一般的には、ワイドニングキャストはよいけれど、ナローイングキャストはバグの原因になりがちなので避けられるはずです。
ここで重要なのは精度が維持されるかどうかということです。
プリミティブ型でパターンマッチングを行う時も、キャストのような型変換を行います。
この時、値が型変換を行っても精度が落ちなければ、その型のcaseラベルでマッチします。この型変換がナローイングであったとしても、精度が維持されるかどうかをコンパイラがチェックします。
たとえば、次のようなコードはどうでしょう。
int x = ......;
String result = switch (x) {
case byte b -> b +"はbyte";
case short s -> s +"はshort";
case int i -> i + "はint";
};この場合、xの値がbyte型の範囲にあれば精度は落ちないので、byte型のcaseにマッチします。つまり、xの値が-128から127の範囲にある場合、byte型のcaseにマッチするということです。
byte型のcaseにマッチした場合、xはbyte型にキャストされ、bに代入されます。
同様に、xの値が-32,768 (= 0x8000)から32,767 (= 0x7FFF)の範囲にあれば、short型のcaseにマッチします。
このswitch式にdefaultがないのは、最後のint型のcaseですべての場合を網羅できるためです。ここでも、網羅性がチェックされるため、不要なdefaultが記述してあるとコンパイルエラーになります。
注意が必要なのはchar型です。char型はshort型と同じ16bit幅ですが、char型は文字を表すということから符号なし整数の扱いになります。
このため、たとえばchar型からshort型に変換する場合には精度が落ちてしまうことがあります。ということは、switchでchar型の値をshort型のcaseにマッチさせようとしても、できないことがあるということです。
これに関しては、次に説明する包含関係も関係します。
包含関係の厳密なチェック
siwtchでパターンマッチングを記述すると、すべてのcaseに到達するかどうかをコンパイル時にチェックします。
たとえば、次のコードはコンパイルエラーになります。
int x = ......;
String result = switch (x) {
case int i -> i + "はint";
case byte b -> b +"はbyte"; // NG コンパイルエラー
};byte型の方がint型よりも範囲が狭いため、先にint型のcaseラベルがあると、byte型のcaseラベルとはマッチしなくなるためです。これは言い方を変えると、byte型はint型に包含されるということです。
型が包含される場合、caseラベルで包含する方を先に記述してしまうとコンパイルエラーになります。
おもしろいのが、このチェックは型だけでなく、値を使用したcaseラベルでも行われるということです。
たとえば、次のコードもコンパイルエラーになります。
int x = ......;
Streing result = switch (x) {
case byte b -> "Byte";
case 42 -> "The Answer"; // NG コンパイルエラー
case int i -> "Integer";
};byte型は-128から127の範囲なので、次のcaseラベルの42には到達することがないからです。
もちろん、このコードは以下のようにcaseラベルの順番を変更すれば、正しく動作します。
int x = ......;
Streing result = switch (x) {
case 42 -> "The Answer";
case byte b -> "Byte";
case int i -> "Integer";
};まとめ
近い将来、正式な機能として導入予定のプリミティブ型のパターンマッチングについて紹介してきました。
プリミティブ型であれば型と値の両方を混在できますし、when句を使えば範囲指定などの条件も追加できます。
if文では値が返せないので、代わりにswitch式を使う場面が増えてきそうです。ぜひ、活用してみてください!
カサレアルでは、Javaを学ぶ方に向けて「Javaプログラミング入門」や「Javaプログラミング基礎」などのコースを開催しています。
Javaに関するコースの詳細や開催日程に関しては以下のリンクをご覧ください。
バックエンド開発を学ぶ研修一覧
https://www.casareal.co.jp/ls/service/openseminar/search/backend