皆さんこんにちは。
SB C&Sの佐藤梨花です。
開発者の皆さんは普段、どのような言語でコーディングを行っていますか?
様々な言語がありますが、私はwebアプリケーションの開発を行っていたため「Java」をメインで使用していました。
Javaは歴史が長く、新しい言語が沢山出てきた中でもいまだに人気の言語です。その理由の1つとして「常にアップデートされて新しいバージョンが出て来ている」という点が挙げられると思います。
とは言え業務で使用していると特定のバージョンからなかなかアップデート出来ないということもあるのではないでしょうか。結果として、新バージョンでどのような便利ファンクションが追加されたのか、なかなか知ることが出来ないと思います。
今回はそんな皆さんに向けて、私が「Java17」で面白いと思った2機能について、連載形式で解説していきます。
また「Java17」を選定した理由としましては、SpringBoot3.0以降で公式サポートされているバージョンであるからです。そんなSpringBootですが、2.7のサポートが「2025年8月」で終了となります。※
本件について先日行ったセミナーが後日公開予定となっております。公開後こちらの記事にもリンクを貼らせて頂きますので、SpringBoot2.7以前のバージョンをお使いの皆様は是非ご覧ください。
※2023/9/11に半年の延長が発表されました
JEP 406:Pattern Matching for switch
■概要
「Pattern Matching for switch」はJava17でプレビュー登場し、その後もバージョンアップを続けJava21で正式採用された機能になります。それぞれJEPが振られており、「406」はJava17での番号になります。Java21では「441」が振られています。
この機能は既に皆さんがお使いになっているであろう「switch」の進化バージョンとなります。
これまでのSwitchが使い辛かった理由として「設定可能な条件が限られている」というものがあったと思われます。その結果、単純な比較であっても基本的にswitchは使用せず、if文で対応している、という方も多いのではないでしょうか。
今回の「Pattern Matching for switch」はそんな悩みを解決する内容となっています。
■機能内容
進化した内容を一言で表すと「比較可能パターンの大幅増加」です。具体的には以下のようなパターンが比較可能となりました。
- パターンマッチの使用許可
- null指定許可
- default指定許可
- レコードクラスの指定許可
- switch式でSealedクラスが指定可能 ※Java18で網羅性強化
- ガードパターン:caseに「when」で条件を追加可能 ※Java19以降、19以前は「&&」
- switch式の簡素化 ※Java20
- enum定数をcase定数として利用可能 ※Java21
これらの進化によりこれまでの「String、long以外の整数プリミティブ型、enum型(定数NG)のみが対象」という条件が大幅改良され、if文で対応していた内容もswitchで対応可能となりました。
実際にどのように変化するか以下に例を記載します。
Before
class Shape {}
class Triangle extends Shape { int calculateArea() { ... } }
static String formatter(Object obj) {
String formatted = "unknown";
if (s == null) {
formatted = String.format("Null %s", s);
}else if (obj instanceof Integer i) {
formatted = String.format("int % d", i);
} else if (obj instanceof Long l) {
formatted = String.format("long %d", l);
} else if (obj instanceof Double d) {
formatted = String.format("double %f", d);
} else if (obj instanceof String s) {
formatted = String.format("String %s", s);
}return formatted;
}
これまでであればよく見かけるような構図になっていると思われます。そしてこのソースコードも(このメソッド単体で考えれば)問題なく動作します。
しかしこのコードには大きく2点の問題があります。
問題1
この処理の目的は「if...elseの各条件のうちのいずれかでformatted変数に値を代入する」ことですが、コンパイラがこの不変性を識別し、強制することは不可能です。
そのためこの例ではあらかじめformatted変数に値を代入し、formatted変数がnullでリターンされることを防止しています。またこの処理をよく見ると、if文は「else if」で終了しており、「else」による条件該当なし時の処理が設定されていません。そのため網羅性はなく、どの条件にも合致しない可能性があります。
そのため先ほどのnull対策を入れない状態だと、呼び出し元でnullリターンによるバグが発生する危険性があります。
問題2
If文は最適化がされていないため、本来この処理ではいずれか1つの条件のみを実行出来ればよいところ、最大で5つの条件を実行する必要があります。そのため実行時間が長くなる可能性があります。
このような問題に対処するため「実行される可能性が高い条件をなるべく前に持ってくる」という対策を行っている方も多いのではないでしょうか。
このような問題を抱えていたif文を新しいswitchで書き換えると、以下のようになります。
After
static String formatterPatternSwitch(Object obj) {
return switch (obj) {
case null -> String.format("Null %s", s);
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 -> obj.toString();
}
}
一目見て、メソッドの要件に対する実現度や可読性やの高さをご確認頂けるのではないうでしょうか。
特に注目頂きたいポイントとしては2つあります。
1つ目は「null」「Double」といったこれまでであれば比較不可だった型の比較を実現している点です。従来であればこういった比較不可項目がある場合は前述のようなif文や、もしくは比較不可部分のみをswitchから外出しするといった方法で対処していたと思われます。しかしその必要はなく、シンプルに1つのswitchで実現することが出来ました。
2つ目は「default」句の追加です。これにより上位caseで合致しなかったものは全て集約され、漏れがなくなります。結果としてIf文におけるelseの役割を果たしてくれるため、switchの網羅性が向上しバグ対策になります。
またこのswitchとsealed やrecordクラスを掛け合わせて使用することで、更に実現範囲が広がります。
例を見てみましょう。
sealed interface Employee permits Regular, Contract, Part {}
record RegularEmployee(String name, Date entryDate) implements Employee {}
record ContractEmployee(String name, int type) implements Employee {}
record PartEmployee(String name, int standardTime) implements Employee {}public String getMessage(Employee e) {
return switch (e) {
case RegularEmployee(var name, var entryDate) -> return "Hello, " + name + ". You are Regular! And the induction date for Creepy is + " entryDate + "!"
case ContractEmployee (var name, var type) -> return "Hello, " + name + ". You are Contract!";
case PartEmployee(String name, int standardTime) -> return "Hello, " + name + ". You are Part! And your standard working day is eight hours is " + standardTime + ".";
}}
sealed である親クラスの「Employee」と、その子クラスとして3つのrecordクラスを定義しています。switchでは親クラスの「Employee」を条件として渡し、それぞれの子クラスと一致するかを確認しています。
ここでsealed、 recordクラス、それぞれ注目して頂きたいポイントを解説します。
■sealed
Java18で追加されたsealedに対する網羅性の強化により、子クラスが全て条件に追加されているかのチェックが行われます。追加されていないクラスがある場合はコンパイルエラーとなるため、switchチェック漏れの心配や、子クラス追加時の対応漏れに対処出来ます。
もちろんdefaultが存在する場合は全ての子クラスを定義する必要はありません。
■recordクラス
Recordクラスのフィールド変数を定義し、使用することが可能です。これによりswitchで一致した場合に再定義することなく、処理に使用出来ます。
例では単純なフィールドですが、recordのフィールドを使用した計算処理や加工をしてreturnする必要がある場合にとても便利です。
まとめ
ここまで「Pattern Matching for switch」について解説しました。いかがでしょうか?
個人的にはこれまでは条件が厳しすぎて単純な比較であっても特に考えずif文で実装してきましたが、処理要件によって使い分けが出来るようになったことがとても嬉しく思います。また可読性のみでなく処理スピード改善にもなるので、積極的に使っていきたいです。
是非皆さんもJava17以降をお使いでしたら試してみてください。
今回解説した以外にも追加されている機能や便利な使い方がありますので、是非公式ドキュメントも併せてご確認ください。
次回は「sealedクラス」について解説します。
DevOps関連情報はこちらから!
著者紹介
SB C&S株式会社
テクニカルマーケティングセンター
佐藤 梨花
勤怠管理システムの開発(使用言語:Java)に約8年間従事。
現在はエンジニア時の経験を活かしたDevOpsやDX推進のプリセールスとして業務に精励しています。