はじめに
こんにちは、もんすんです。
前回のJava10→11編では、LTSの意味から始まり、文字列メソッドの強化・標準HttpClient・単一ファイル実行などを見ていきました。
今回は Java11→17編です。
この回が、連載の中で最もボリューミーです。なぜかというと——Java12〜16は短命リリース(各6ヶ月)なのに、この5バージョン分の変化を1本にまとめているからです。
でも大丈夫です。詰め込みすぎずに「実務で効く項目」に絞っています。
(私が全部理解できていないということもあります)
「switch が式になった。テキストが複数行書ける。record がある。sealed がある——つまり、Javaが現代的になってきた。」
そういう回です。やっていきましょう。
対象読者
- Java10→11編を読んだ方(または Java11 の概要を知っている方)
-
record、switch式、テキストブロックを使ったことがない方
Java11からJava17へのレベルアップ
1. switch 式(Java14より)
従来の switch 文が、値を返す「式」 として書けるようになりました。
Before(Java 11 以前の switch 文)
String day = "MONDAY";
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 IllegalArgumentException("無効な曜日: " + day);
}
break を書き忘れると次の case に落ちる(フォールスルー)。冗長でバグの温床です。
After(Java 14 以降の switch 式)
String day = "MONDAY";
int numLetters = switch (day) {
case "MONDAY", "FRIDAY", "SUNDAY" -> 6;
case "TUESDAY" -> 7;
case "THURSDAY", "SATURDAY" -> 8;
case "WEDNESDAY" -> 9;
default -> throw new IllegalArgumentException("無効な曜日: " + day);
};
-
->構文でbreak不要 - 複数の
caseをカンマでまとめられる - 全ケースを網羅していないとコンパイルエラー(安全)
- 値を直接変数に代入できる
実務で switch を使うたびに「あっ、これ式で書けばよかった」となります。
ブロックが必要なときは yield
String result = switch (code) {
case 200 -> "OK";
case 404 -> "Not Found";
default -> {
var message = "Unknown: " + code;
System.out.println(message);
yield message; // ブロック内で値を返すときは yield
}
};
2. テキストブロック(Java 15 にて正式へ)
複数行の文字列を、見た目通りに書けるようになりました。
Before(Java 14 以前)
// JSON を文字列で持つとき
String json = "{\n" +
" \"name\": \"田中\",\n" +
" \"age\": 30\n" +
"}";
// HTMLを文字列で持つとき
String html = "<html>\n" +
" <body>\n" +
" <p>こんにちは</p>\n" +
" </body>\n" +
"</html>";
エスケープが多くて読みにくい上、閉じダブルクォートを忘れやすい。
After(Java 15 以降のテキストブロック)
// """ で始まり """ で終わる
String json = """
{
"name": "田中",
"age": 30
}
""";
String html = """
<html>
<body>
<p>こんにちは</p>
</body>
</html>
""";
-
"""で囲むだけ、エスケープ不要 - インデントは自動で除去(終端
"""の位置が基準) - SQL クエリや設定テンプレートにも重宝
私の実務では、テスト用の JSON やメール本文テンプレートを書くときに毎回助かっています。
3. record クラス(Java 16 から正式へ)
データを保持するだけのクラスを、極限まで簡潔に書くための構文です。
Before(Java 15 以前)
// 名前と年齢だけを持つクラス
public final class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String name() { return name; }
public int age() { return age; }
@Override
public boolean equals(Object obj) { ... }
@Override
public int hashCode() { ... }
@Override
public String toString() { ... }
}
これを全部書くのが Java の「儀式」でした(Lombokで省略している現場も多かった)。
After(Java 16 以降の record)
public record Person(String name, int age) {}
1行で終わります。上記の全部(コンストラクタ・getter・equals・hashCode・toString)が自動生成されます。
var person = new Person("田中", 30);
System.out.println(person.name()); // 田中
System.out.println(person.age()); // 30
System.out.println(person); // Person[name=田中, age=30]
var person2 = new Person("田中", 30);
System.out.println(person.equals(person2)); // true(値で比較)
record の特徴と制限
| 特徴 | 詳細 |
|---|---|
| 不変(immutable) | フィールドは final。生成後に変更不可 |
| 継承不可 |
extends できない(implements は可) |
| コンパクトコンストラクタ | バリデーションを簡潔に書ける |
// コンパクトコンストラクタでバリデーション
public record Person(String name, int age) {
public Person { // 引数リストを省略するコンパクトコンストラクタ
if (age < 0) throw new IllegalArgumentException("年齢は0以上");
name = name.strip(); // 前後空白を取る
}
}
実務では「DTOやAPIのレスポンスモデルを record に置き換える」だけで、クラスファイルが劇的にスッキリします。
4. sealed クラス(Java 17 から正式へ)
継承できるクラスを明示的に限定する 機能です。
// Shape を継承できるのは Circle, Rectangle, Triangle だけと宣言
public sealed class Shape
permits Circle, Rectangle, Triangle {}
public final class Circle extends Shape {
private final double radius;
// ...
}
public final class Rectangle extends Shape {
private final double width, height;
// ...
}
public non-sealed class Triangle extends Shape {
// non-sealed = さらに継承を許可
}
何がうれしいのか
sealed がある状態で switch のパターンマッチング(Java21で正式)を使うと、コンパイラが「全ての型を網羅しているか」をチェックしてくれます。
// Java 21 以降のパターンマッチング(sealed と相性が良い)
double area = switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> t.base() * t.height() / 2;
// 全ケース網羅 → default 不要
};
今すぐ全部使いこなすのは難しくても、「継承を制限して安全性を上げるしくみ」として覚えておくと、後のJava21編でスムーズに繋がります。
5. instanceof のパターンマッチング(Java 16 から正式へ)
instanceof でキャストまで自動でやってくれる構文です。
Before
Object obj = "こんにちは";
if (obj instanceof String) {
String s = (String) obj; // キャストが必要
System.out.println(s.toUpperCase());
}
After(Java 16 以降)
Object obj = "こんにちは";
if (obj instanceof String s) { // キャストと変数宣言を同時に
System.out.println(s.toUpperCase()); // そのまま s として使える
}
地味ですが、instanceof → キャスト → 使う、の3ステップが1ステップになります。レガシーなコードを整理するときにどんどん使えます。
6. NPEメッセージの詳細化(Java 14)
これは「機能」というよりデバッグ体験の改善ですが、知っておいて損はありません。
Before(Java 13 以前)
// NullPointerException: null
user.getAddress().getCity().toUpperCase();
どこが null なのか、スタックトレースの行番号を見てコードと突き合わせるしかありませんでした。
After(Java 14 以降)
// NullPointerException: Cannot invoke "Address.getCity()" because the return value of "User.getAddress()" is null
user.getAddress().getCity().toUpperCase();
「何が null だったのか」をメッセージが教えてくれます。 メソッドチェーンが長いコードで特に助かります。
実務でJava11→17に上げるときの注意
--illegal-access の撤廃(Java 17)
Java9でモジュールシステムが導入されて以来、JDK内部のAPIへのリフレクションアクセスが制限されてきました。Java16で警告に変わり、Java17では --illegal-access オプション自体が削除されました。
古いライブラリ(特にByte Buddy、Javassist を使うフレームワーク系)がこの影響を受けることがあります。Spring Boot 3 は Java 17 ベースで設計されているので、最新の Spring を使う限りは問題ありません。旧来の Spring Boot 2.x を使いながら Java17 に上げる場合は注意が必要です。
おわりに
Java11→17の5バージョン分をまとめて見てきました。
-
switch式: フォールスルーのバグを撲滅 - テキストブロック: JSON・HTML の文字列がストレスゼロに
-
record: データクラスが1行になる -
sealed: 継承の範囲を安全に制限 -
instanceofのパターンマッチング: キャストを1ステップに - NPEメッセージの詳細化: デバッグが楽になった
どれも「コードが読みやすく・安全になる」方向の変化です。Java8から17に上げたとき「あ、Javaって進化してたんだ」と初めて気づきました——飛び級だったので。
次回は Java17→21編。Virtual Threads(仮想スレッド)というJavaの並行処理を根本から変える機能が登場します。Spring Boot 3.2以降との組み合わせも熱い回です。一緒にレベルアップを目指しましょう。
→ [Java17→21編](公開後にリンクを追加します)