TL;DR
- 生産性が劇的に上がるということはないが、開発者の意図がより伝わりやすくなり、品質・保守性の向上に寄与する
- AIコード生成との親和性も高くなる
はじめに
Javaは現役で広く利用される言語のなかでは歴史が長いほうです。それ以前の言語に慣れた人にも移行しやすいよう、書き方もC言語などのクラシックなスタイルを継承しています。
Javaの進化が停滞していた時期に「もっと開発しやすく」という要望に応じてKotlinなどのモダンな言語が挑戦者として登場しました。
Javaもこの流れに追随するように「Project Amber」というプロジェクトで、競合する言語に匹敵するモダンな書き方ができるようになりました。
この記事では、進化した書き方の主要なポイントをざっと紹介していきます。詳細な記載方法は公式その他を検索してみてください。
進化した言語ルール
ローカル変数型推論(JDK10~)
こちらの記事を参考にしてください。
Records (JDK16~)
DTOやValueObjectとして使われるデータ保持クラスをつくるには、フィールド、コンストラクタ、アクセッサメソッドなどを定義する必要があり、いわゆるボイラープレートコードになりがちでした。
この問題にはLombokが効果的ですが、JDK単体でも解決できるようになりました。recordというデータ保持に特化したクラスの登場です。
// Before
public class Money {
private final int amount;
private final String currency;
public Money(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
public int amount() { return amount; }
public String currency() { return currency; }
@Override
public boolean equals(Object o) {
if (!(o instanceof Money)) return false;
Money m = (Money) o;
return amount == m.amount && currency.equals(m.currency);
}
@Override public int hashCode() { return Objects.hash(amount, currency); }
@Override public String toString() { return amount + " " + currency; }
}
// After:1行で同等の意味を持つ
record Money(int amount, String currency) {}
// 使い方
var price = new Money(980, "JPY");
System.out.println(price); // 980 JPY
System.out.println(price.amount()); // 980 ← getAmount() ではなく amount()
この記述により、パラメータを指定するだけで関連する実装が暗黙的に行われます。
Recordsの特徴として「生成後は更新不可」という点があります。適用シーンとしてはイベントや業務トランザクションなど外部から取得したデータなどの格納などがフィットします。
これにより「このインスタンスは更新不可の前提でやりとりしている」という意図が明確になります。この点は開発者に不自由を強いていると考えるべきではなく、上記意図を反映するための仕様であることをまず理解しておくべきでしょう。
Lombokは柔軟性が高いというメリットもありますが、上記特徴を持つデータの場合はRecordsの利用が適していそうですね。
この仕様をより深く理解したい方には、こちらのブログが参考になります。
パターンマッチング(JDK16~)
JEP 394: Pattern Matching for instanceof
以前はインスタンスの操作を行う際に、instanceofで型の判定(チェック)を行った後、その型で扱うために明示的なキャストと変数への代入が必要であり、コードが少し冗長でした。
これを、インスタンス評価と代入をまとめて実施できるようになりました。instanceofが登場する局面では恩恵が受けられます。
// Before
if (obj instanceof String) {
String s = (String) obj;
connectDb(s);
}
// After:sは自動的にキャストされる
if (obj instanceof String s) {
connectDb(s);
}
Switch式(JDK14~)
Switchにはいくつかの進化がありました。
まずSwitchは文であり戻り値を持ちませんでしたが、戻り値を持つ書き方ができるようになりました。ひとつひとつにbreak;を記載しなくてもよくなり、見易さが向上しています。
// Before:break忘れると下に流れバグにつながる
String category;
switch (statusCode) {
case 200:
category = "成功";
break;
case 404:
category = "未検出";
break;
case 500:
category = "サーバーエラー";
break;
default:
category = "不明";
}
// After:値を返す+アロー構文の導入によるフォールスルーの防止
String category = switch (statusCode) {
case 200 -> "成功";
case 301, 302 -> "リダイレクト"; // 複数caseをカンマで並べられる
case 404 -> "未検出";
case 500, 503 -> "サーバーエラー";
default -> "不明";
};
Switchでのパターンマッチング(JDK21~)
JEP 441: Pattern Matching for switch
Swtichでの評価対象が値だけでなくデータ型に拡張されました。
外部から取得するデータが複数パターンの場合、パターンマッチングが効いていてもif文を連ねる必要がありましたが、これがキレイに書けるようになります。
「クラスの型が識別の意味を持つ」という使い方、よく見る例ではExceptionとその子クラスが該当しますが、取得したクラスの型によって処理を分ける場合などに威力を発揮するでしょう。
クラスの型ではありませんがnullを識別することもできるため、null時の対応も組み込みやすくなりました。
String message = switch (value) {
case null -> "値なし";
case String s -> "文字列: " + s;
case Integer i when i > 0 -> "正の整数: " + i;
default -> "その他";
};
System.out.println(message);
シールドクラス(JDK17~)
継承できる(=finalではない)クラスを親とするクラスを生成することに、特に制約はありませんでした。再利用を前提としたツールなどのクラスでは問題ないのですが、業務での実装で意図をもって親子クラスをつくっているにも関わらず、別の開発者が意図をくみ取らずに新しい子クラスをつくるケースも考えられます。
シールドクラスでは、継承するクラスを宣言してそれ以外のクラスが継承することを防ぐことができます。
// sealed で継承可能なクラスを明示的に列挙する
sealed interface Notification
permits EmailNotification, PushNotification, SmsNotification {}
record EmailNotification(String to, String subject, String body) implements Notification {}
record PushNotification (String deviceToken, String message) implements Notification {}
record SmsNotification (String phoneNumber, String text) implements Notification {}
こちらも先の場合と同じく「クラスの型が識別の意味を持つ」使い方で効果を発揮しそうです。
テキストブロック(JDK15~)
長い文字列を表現する場合、画面からはみ出すほど横に伸びてしまいます。
そのため複数行に分割、かつそれらを連結する形がよく使用されました。ただ表記内容が直観的に判読できない面もありました。
テキストブロック表記では、結合子などを省略して記載できるようになりました。
// Before:エスケープだらけで構造がわかりにくい
String query = "SELECT u.name, u.email\n" +
"FROM users u\n" +
"WHERE u.active = true\n" +
" AND u.created_at > ?\n" +
"ORDER BY u.name";
// After:SQLがそのまま読める
String query = """
SELECT u.name, u.email
FROM users u
WHERE u.active = true
AND u.created_at > ?
ORDER BY u.name
""";
formattedと組み合わせることで、動的な部分を更新することもできます。
String name = "Alice";
int age = 30;
String json = """
{
"name" : "%s",
"age" : %d,
"active" : true
}
""".formatted(name, age);
無名変数(JDK22~)
JEP 456: Unnamed Variables & Patterns
Catch句に記載する Exception e (もアバウトな記載ですが)で、eを置いてみたものの実際には参照していない、というケースは数多く存在するのではと考えています。
このような場合に、変数名を「_」(アンダースコア)で置き換えることができます。これにより「この変数は使うつもりがない」という開発者の意思表示が読み取れて、コードの可読性向上に寄与します。
// Exceptionの例
try {
int result = Integer.parseInt(input);
} catch (NumberFormatException _) {
System.out.println("数値ではありません");
}
// ループでの利用例
var list = java.util.List.of("A", "B", "C");
for (var _ : list) {
System.out.println("要素がある分だけ何かを実行");
}
おわりに
これらの進化は、主に
- 手続き的な記述を削減する
- 開発者の意図を明確に表現する
という価値を持っているように思います。生産性が劇的に改善するといった方向ではありませんが、より開発者の意思が反映しやすくなったことで長期的な品質の維持が期待できると思います。
今後はAIによる開発・保守が本格化していくと考えられます。このとき、開発者の意図が読み取りやすければAIの出力精度も向上するでしょう。
これらの記法はできれば活用していきたいですが、使えなくても必要なロジックは作れるので、新しいJDKへの移行に躊躇する必要はないでしょう。
これらの記法の存在をまずは開発メンバー内で理解して読めるようになることが第一歩で、プロダクションコードへの適用は新旧どちらでも可としながら自然に移行するという形がいいかもしれません。