はじめに
よくプロジェクトを異動しているのも相まって、Javaのバージョンも行ったり来たりしています。
今回は新しいバージョンに慣れた後、古いバージョンのプロジェクトに異動したとき「あれ便利だったな~」と恋しくなった機能を紹介していきたいと思います。
といっても、戻ったバージョンはJava17 -> Java11なので、このバージョン間でLanguageの差分はそう多くないですが...。
https://openjdk.org/projects/jdk/17/jeps-since-jdk-11
紹介する機能の詳細は各JEPのページに任せ、ここでは個人的に好きなところをピックアップして紹介していきたいと思います。
JEP 361: Switch Expressions(Switch式)
- 個人的に好きなところ:Enum型を使うときに便利
- 値を返す時に全パターン網羅していないとエラーになる
- 全パターン網羅していれば、defaultが要らない
サンプル
以下のようなEnumがあったとします。
public enum Season {
SPRING, SUMMER, AUTUMN, WINTER;
}
Switch式を使わなかった場合と使った場合はそれぞれ以下のように書けます。
// Switch式を利用しない場合(従来のSwitch文)
public int sampleOld(Season season) {
switch (season) {
case SPRING:
return 2;
case SUMMER:
return 3;
case AUTUMN:
return 5;
case WINTER:
return 7;
}
// Enumのパターンは全て網羅しているが、defaultまたはSwitch文の外を書かないとコンパイルエラー
throw new RuntimeException("あり得ない");
}
// Switch式を利用した場合
public int sampleNew(Season season) {
// Enumのパターンを全て網羅しているのでOK
return switch (season) {
case SPRING -> 2;
case SUMMER -> 3;
case AUTUMN -> 5;
case WINTER -> 7;
};
}
Switch式を利用しない場合、Season型のパターンを全て網羅していてもdefaultまたはSwitch文の外を書かないといけません。一方、Switch式を利用した場合、不要となります。
また、Season型の要素が増えた場合、Switch式を利用しない場合だと要素が増えてもエラーにならず、実際に動作させたときにRuntimeExceptionが発生して気付きます。一方、Switch式を利用した場合であればコンパイルエラーになるので、実装時点で気付くことができます。
ちなみに、Switch文/式に関わらず、以前は各caseの中でやることがシンプルでも一々returnを書いていましたが、アロー演算子の利用でシンプルに書けるようになりました。
サンプルコードに関する留意事項
今回は説明用のサンプルであり、簡素化のためSeason型の要素ごとに決まった値を返しているだけのメソッドとしています。
Enumの要素ごとに決まった値返すのであれば、Enum側にフィールド持たせて、getterを作る方が一般的かと思います。
JEP 395: Records
- 個人的に好きなところ:イミュータブルなクラスが短い記述で書ける
サンプル
// recordを使わない場合
class ConceptMapClass {
/** 名前 */
private final String name;
/** 変換元 */
private final String source;
/** 変換先 */
private final String target;
public ConceptMapClass(String name, String source, String target) {
this.name = name;
this.source = source;
this.target = target;
}
public String getName() {
return name;
}
public String getSource() {
return source;
}
public String getTarget() {
return target;
}
}
// recordを使った場合
/**
* @param name 名前
* @param source 変換元
* @param target 変換先
*/
record ConceptMapRecord(String name, String source, String target) {
}
recordを使わない場合はコンストラクタやgetterを記述する必要がありますが、recordを使う場合はこれらが不要になります。また、recordはイミュータブルなクラスになるので、各フィールドにfinalを付与する必要もないです。
注意点としては、recordで生成されたクラスのgetterに相当するメソッド名は、get{フィールド名}ではなく{フィールド名}となります。
サンプルコードに関する留意事項
サンプルコードはJavaの標準ライブラリのみの利用を前提としていますが、recordを使わない場合でもlombokが使える環境であれば、@RequiredArgsConstructorや@Getterを利用してjavaファイルのコードを短くできます。
JEP 394: Pattern Matching for instanceof
- 個人的に好きなところ:パターンマッチングがスッキリと書けて読みやすい
サンプル
// パターンマッチングを利用しない場合
public String toStringByCast(Number num) {
if (num instanceof BigDecimal) {
// 括弧が多い
return ((BigDecimal) num).toPlainString();
}
return num.toString();
}
// パターンマッチングを利用した場合
public String toStringByPatternMatching(Number num) {
if (num instanceof BigDecimal bigDecimal) {
return bigDecimal.toPlainString();
}
return num.toString();
}
パターンマッチングを利用しない場合、var bigDecimal = (BigDecimal) bigDecimal;を1行書くか((BigDecimal) num)で括弧がいっぱいの処理を書くかになります。一方、パターンマッチングを利用した場合、シンプルに記述できます。
ちなみにですが、instanceofを利用しているif文で否定形 かつ if文の中で処理が終わる(returnやthrowを使った)場合、if文の外側でパターンマッチング変数を使うことができます。
// パターンマッチングを利用した場合(発展)
public String toStringByPatternMatchingElse(Number num) {
if (!(num instanceof BigDecimal bigDecimal)) {
return num.toString();
}
// if文の中でreturnしているので、ここに到達するのはnumがBigDecimal型場合だったときのみ
return bigDecimal.toPlainString();
}
さいごに
以上、今のプロジェクトでJava11を使っていて日々恋しく思っている機能でした。
(補足)この記事を書いている間にプロジェクトでJava21にバージョンアップする話が出てきました。Java17->21で追加された機能も色々遊べそうなので楽しみです。