この記事はHubble Advent Calendar 2025の7日目の記事です。
はじめに
こんにちは。
Hubbleでバックエンドエンジニアをやっています@subjectです。
Hubbleは契約書管理クラウド HubbleというSaaSの提供をしており、Ruby on Railsで主な開発を行っています。
Kaigi on Rails 2025でのRuby Sponsorをするなど会社をあげてRuby on Railsを推している弊社ですが、実は一部のシステムがJavaで作られております。
2025年9月に最新LTSであるJava25が出てきたこのご時世に筆者の知識はJava8で止まっていたため、時代に追いつくべく奮闘?した結果を本記事にまとめます。
tl:dr
varとswitch式を押さえておけばOK
Javaバージョンアップのおさらい
綺麗な年表ですね、きっちり半年毎にバージョンアップしていて流石の貫禄を感じさせてくれます。
改めてみると8から25というのはなかなかの上がり具合です(なにせ8年分です)。
各バージョンでリリースされた機能数(プレビュー機能含む)はなんと計299件もありました、しんどい。
※OpenJDKの各バージョンのFeatures欄の記載数を計算
さすがに全てを見るのは厳しすぎるので、言語仕様とAPIの変更に絞って見ます(それでも多いですが)。
これはと思ったアップデート
var変数
Java10で正式リリースされた機能です。変数の型推論を行なってくれるので、明示的に型を書かなくてよくなります。
ネストしたMapとか定義するとジェネリクス部分も相まって型定義がかなり長くなるので、こういうのがサクッと書けるのは良いですね。
// 従来の書き方
Map<String, List<Integer>> data = new HashMap<String, List<Integer>>();
// varを使った書き方
var data = new HashMap<String, List<Integer>>();
とはいえ書きやすさは読みやすさ(≒情報量)とのトレードオフ、上記のコードだと良いですが
var result = processData();
みたいなコードだとパッと見では変数の型がわかりません(Rubyならそれが当たり前というツッコミはなしです)。
機能作った側も思うところがあるようで、ありがたいことにガイドラインがありました。
これを参考にするのが良さそうです。
switch式
Java14で正式リリースされた機能です。従来のswitch文と異なり、式として値を返すことができます。
// 従来のswitch文
String result;
switch (status) {
case "success":
result = "処理成功";
break;
case "error":
result = "処理失敗";
break;
default:
result = "不明";
break;
}
// switch式
String result = switch (status) {
case "success" -> "処理成功";
case "error" -> "処理失敗";
default -> "不明";
};
式として値を返せるようになるため、変数への代入をシンプルに書けるようになっています。
breakが不要になったもの良いですね(なんだかんだbreak忘れのバグって起きがちなので)。
またswitch式は新しく追加された書き方case "success" ->(case L ->ラベル)だけでなく、従来の書き方case "success":(case L:文)でも引き続き記述できるようです。
case L:文の場合はyield文というもので式の戻り値を指定します。
case内で複数行の処理を行いたい場合はcase L:文の方で記述する必要がありそうです。
// switch式(case L ->ラベル)
String result = switch (status) {
case "success" -> "処理成功";
case "error" -> "処理失敗";
default -> "不明";
};
// switch式(case L:文)
String result = switch (status) {
case "success":
log.info("処理が成功しました");
yield "処理成功";
case "error":
log.error("処理が失敗しました");
yield "処理失敗";
default:
log.warn("不明なステータス: {}", status);
yield "不明";
};
switchでのパターンマッチ
Java21で正式リリースされた機能です。switchで型による分岐を行うことができます。
// パターンマッチングを使った書き方
String result = switch (column) {
case StringColumn(String value) -> "文字列: " + value;
case IntegerColumn(int value) -> "整数: " + value;
case SelectColumn(String value, List<String> options) -> "選択: " + value + " (選択肢数: " + options.size() + ")";
default -> "その他のカラム";
};
// パターンマッチングがない場合の書き方
String result;
if (column instanceof StringColumn) {
StringColumn stringColumn = (StringColumn) column;
result = "文字列: " + stringColumn.value();
} else if (column instanceof IntegerColumn) {
IntegerColumn integerColumn = (IntegerColumn) column;
result = "整数: " + integerColumn.value();
} else if (column instanceof SelectColumn) {
SelectColumn selectColumn = (SelectColumn) column;
result = "選択: " + selectColumn.value() + " (選択肢数: " + selectColumn.options().size() + ")";
} else {
result = "その他のカラム";
}
インターフェースの実装クラスごとに分岐したいような処理を書く場合にかなりスッキリさせられそうです。
Record
Java16で正式リリースされた機能です。イミュータブルなオブジェクトを簡潔に定義できます。
// Recordを使わない場合
public class Request {
private final String name;
private final int age;
public Request(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) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Request request = (Request) obj;
return age == request.age && Objects.equals(name, request.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Request[name=" + name + ", age=" + age + "]";
}
}
// Recordを使った場合
public record Request(
String name,
int age
) {}
記述量の差が歴然ですねw
ただこの機能は記述を簡素化するという点よりも「イミュータブルなオブジェクトを作る」というところがポイントなようです(単にデータ構造を表現するだけではない)。
記述の簡素化という点ではこの機能が出る前だとLombokで行うことが一般的だったかなと思いますが、Lombokを代替するものというよりは、新たな選択肢ができたというイメージです(イミュータブルオブジェクトの代表格たる?DDDでいうところのVOのようなものを定義するのにうまく使えると良さそうです)。
Gather
Java24で正式リリースされた機能です。Streamに対するカスタム中間操作を作ることができます。
中間操作とはなんぞやという話ですが、 Streamでよく使うmapとかfilterなどのデータを変換している部分のことです。
ちなみに最後に取得するデータの形式を決める部分は終端処理と言います。
Stream<Integer> stream = List.of(1, 2, 3, 4, 5, 6).stream()
.filter(n -> n % 2 == 0) // 中間操作:偶数をフィルタ
.map(n -> n * 2) // 中間操作:各要素を2倍
.toList(); // 終端操作:Listに変換
終端操作にはCollectorというカスタム終端操作を作るためのインターフェースがありましたが、中間操作に対してはこれまでなかった感じですね。
Gathererの面白いところは、従来はできなかった処理順序の考慮や要素の塊に対する処理(SQLで言うところのwindow)ができるようになった点です。
外部APIへの送信やDBへのバルクインサートなど、データを「N件ずつの塊」にして処理したいケースでうまいこと使えそうに思います。
また「カスタム中間操作を作る」とありますが、Collectorと同じく組み込みのもの(Gatherers、CollectorだとCollectorsですね)もあるため、ある程度のケースは組み込みのもので実装できるかと思います。
// windowFixed: 固定サイズのウィンドウに要素をグループ化
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
List<List<Integer>> windows = numbers.stream()
.gather(Gatherers.windowFixed(3))
.toList();
// 結果: [[1, 2, 3], [4, 5, 6]]
便利機能系
requireNonNullElse
Java9で追加されたObjectsのメソッドです。nullチェックとデフォルト値の設定を簡潔に記述できます。
// 従来の書き方
String value = (obj != null) ? obj : "default";
// requireNonNullElseを使った書き方
String value = Objects.requireNonNullElse(obj, "default");
宣言的に書けるので好みです。
不変コレクションのファクトリ
Java9で追加されたコレクションのファクトリメソッドです。イミュータブルなコレクションを簡潔に作成できます。
// 従来の書き方
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
List<String> immutableList = Collections.unmodifiableList(list);
// ファクトリメソッドを使った書き方
List<String> immutableList = List.of("a", "b", "c");
Set<Integer> immutableSet = Set.of(1, 2, 3);
Map<String, Integer> immutableMap = Map.of("key1", 1, "key2", 2);
// 10個以上の要素を持つMapの場合
Map<String, Integer> map = Map.ofEntries(
Map.entry("key1", 1),
Map.entry("key2", 2),
// ...
);
Map.ofだけちょっと無茶なインターフェースですねw

※Oracleのjavadocより抜粋
おわりに
Java8のラムダ式のような考え方がガラッと変わるような変更がなくて良かったです。
こうした安定感のあるアップデートはやはりJavaの魅力ですね。
改めてJavaを書いていきたいと思える良い機会となりました!(注:弊社HubbleはRuby on Railsを推している会社です)
明日は@ksakae1216さんです!
余談
実務で使うことはないと思いますが、Java25で一番インパクトがあったのは魔法の呪文(パブリックスタティックヴォイドメイン)がなくなることでした。
// 従来の書き方
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
// Java25からできる書き方
void main() {
IO.println("Hello World!");
}
