お疲れさまです、みやもとです。
前回ちょっと泣き言が入ったJava学び直し、続きはあと2回で終われそうです。
当初の予定より1回増えましたががんばっていきます。
Java 10の変更点
Geminiセレクトの重要変更、Java 10に関しては1つだけでした。
汎用インスタンス作成のための型推論
Geminiさんが「Type Inference for Generic Instance Creation」とおそらく公式ドキュメントから引っ張ってきたのだろう英文を出してきたので訳した結果、いまいちピンと来ない感じになりました。
要は変数宣言の際にvarを使えるようになった、という話しです。
varと聞くと「JavaScriptやん!」と反応したのも昔の話、今はJavaでも使えるんですね。
個人的には静的型付けの方が何入ってるかわかりやすくて好きなのですが、多分そういう要望があったのでしょう。
改めてサンプルコードを書いてもらってみました。
まずはJava 6バージョン。
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry; // Map.Entry をインポート
public class PreJava10Example {
public static void main(String[] args) {
// 1. Mapの宣言と初期化 (型名を繰り返す)
// Java 7以降は new HashMap<>() のようにダイヤモンド演算子が使えますが、
// 左辺の型宣言は必須です。
Map<String, List<Integer>> userScores = new HashMap<String, List<Integer>>();
// データの追加
List<Integer> aliceScores = new ArrayList<Integer>();
aliceScores.add(100);
aliceScores.add(95);
userScores.put("Alice", aliceScores);
List<Integer> bobScores = new ArrayList<Integer>();
bobScores.add(88);
bobScores.add(92);
userScores.put("Bob", bobScores);
System.out.println("--- Java 6/7/8 Style ---");
// 2. Mapのエントリセットを取得してループ (ここでも長い型名が必要)
Set<Entry<String, List<Integer>>> entrySet = userScores.entrySet();
for (Entry<String, List<Integer>> entry : entrySet) {
String userName = entry.getKey();
List<Integer> scores = entry.getValue();
System.out.println("User: " + userName + ", Scores: " + scores);
// 3. ループ内で一時的なリストを作成する場合も型宣言が必要
List<Integer> processedScores = new ArrayList<Integer>();
for (Integer score : scores) {
processedScores.add(score + 1); // 例: スコアに1を加える処理
}
System.out.println(" Processed: " + processedScores);
}
}
}
続いてJava 10バージョン。
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// var を使えば Map.Entry や Set の明示的なインポートが不要になることも多い
public class Java10VarExample {
public static void main(String[] args) {
// 1. Mapの宣言と初期化 (var で型名を省略)
// コンパイラは右辺の new HashMap<String, List<Integer>>() から型を推論する
var userScores = new HashMap<String, List<Integer>>();
// データの追加 (リストの初期化にも var が使える)
var aliceScores = new ArrayList<Integer>();
aliceScores.add(100);
aliceScores.add(95);
userScores.put("Alice", aliceScores);
// new ArrayList<>() とダイヤモンド演算子を使っても良い
var bobScores = new ArrayList<Integer>();
bobScores.add(88);
bobScores.add(92);
userScores.put("Bob", bobScores);
System.out.println("--- Java 10+ Style with var ---");
// 2. Mapのエントリセットを取得してループ (var で型名を省略)
// userScores.entrySet() の戻り値の型から entry の型を推論
for (var entry : userScores.entrySet()) {
// entry.getKey() や entry.getValue() の戻り値型から推論
var userName = entry.getKey();
var scores = entry.getValue();
System.out.println("User: " + userName + ", Scores: " + scores);
// 3. ループ内で一時的なリストを作成する場合も var で簡潔に
var processedScores = new ArrayList<Integer>();
for (var score : scores) { // score も Integer と推論される
processedScores.add(score + 1); // 例: スコアに1を加える処理
}
System.out.println(" Processed: " + processedScores);
}
}
}
こうして比べてみると、確かにちょっと便利かも。
Java 7で追加されたダイヤモンド演算子が「宣言した変数の型からnewでインスタンス化するArrayListに格納するものの型を判断する」機能だったのに対して、varは「newしたインスタンスの型から変数の型を判断する」という機能なわけですね。
どっちか片方書いてあったらええやん!という。
Geminiが挙げたメリットとして
- 記述量の削減
- 可読性の向上
- リファクタリングの容易化
があり、特に3つめのリファクタリングに関しては「変数の型を変更したい場合、varを使用している箇所は初期化式の型を変えるだけで済むことが多く修正箇所が少なく済む」というのは気付かなかったので「なるほど!」となりました。
一方でなんでもかんでもvarが使えるわけではなく、以下の注意点が挙げられました。
- クラスのフィールド、メソッドのパラメータ、戻り値の型には使用できない
(何を渡されるかわからんメソッドも何が返るかわからんメソッドも作れない) - 初期化式から型を推論するため、varを使用する変数は宣言と同時に初期化する必要がある
(初期化なしとかnullでの初期化は不可) - ダイヤモンド演算子 new ArrayList<>() と var を組み合わせる場合は注意が必要
(ArrayListと判断される可能性があるので期待する型がある場合はnew時に記述する)
Java 15の変更点
Java 15の主要な変更点も1つだけ。
個人的には一番ライトな気もしましたが、言われてみれば無かったな、と気付いた機能でもありました。
テキストブロック
複数行にわたる文字リテラルを作成できる機能です。
私は業務で複数行に渡る(=改行コードが必要になる)ような文字列をまず使わないので「あれば便利かもだけど別によくね?」ぐらいの感覚。
これもサンプル出してもらいましょう。
まずJava 6バージョン。
public class PreJava15StringExample {
public static void main(String[] args) {
// --- 1. 複数行のSQLクエリ ---
// 各行末に \n を追加し、+ で連結する必要がある。
// コードのインデントと文字列内のインデントが混ざりやすい。
String sqlQuery = "SELECT\n" +
" p.product_id,\n" +
" p.product_name,\n" +
" c.category_name\n" +
"FROM\n" +
" products p\n" +
"JOIN\n" +
" categories c ON p.category_id = c.category_id\n" +
"WHERE\n" +
" p.price > 1000 AND c.category_name = 'Electronics'\n" +
"ORDER BY\n" +
" p.product_name;";
System.out.println("--- Pre-Java 15 SQL ---");
System.out.println(sqlQuery);
System.out.println("------------------------\n");
// --- 2. JSONデータ ---
// ダブルクォーテーション " を \" とエスケープする必要がある。
// \n と + を多用するため、読みにくく、編集も煩雑。
String jsonData = "{\n" +
" \"user\": {\n" +
" \"id\": 12345,\n" +
" \"name\": \"Taro Yamada\",\n" +
" \"email\": \"taro.yamada@example.com\",\n" +
" \"roles\": [\n" +
" \"Administrator\",\n" +
" \"Editor\"\n" +
" ],\n" +
" \"active\": true\n" +
" }\n" +
"}";
System.out.println("--- Pre-Java 15 JSON ---");
System.out.println(jsonData);
System.out.println("-------------------------");
}
}
SQL文字列とJSONデータになる文字列をいちいち+で結合して書いている模様。
こうして見るとSQLはともかく、JSONはダブルクオートのたびにエスケープが要るので読みにくいし書きにくそう。
次にJava 15バージョン。
public class Java15TextBlockExample {
public static void main(String[] args) {
// --- 1. 複数行のSQLクエリ ---
// 見たままの形で記述できる。
// 不要なインデント(Incidental whitespace)は自動的に除去される。
// (除去されるインデントの基準は、終了を示す"""の位置で決まる)
String sqlQuery = """
SELECT
p.product_id,
p.product_name,
c.category_name
FROM
products p
JOIN
categories c ON p.category_id = c.category_id
WHERE
p.price > 1000 AND c.category_name = 'Electronics'
ORDER BY
p.product_name;
"""; // この行のインデントが基準になる
System.out.println("--- Java 15 Text Block SQL ---");
System.out.println(sqlQuery);
System.out.println("-----------------------------\n");
// --- 2. JSONデータ ---
// ダブルクォーテーション " のエスケープが不要になり、非常に読みやすい。
// JSONデータをそのままコピー&ペーストできる。
String jsonData = """
{
"user": {
"id": 12345,
"name": "Taro Yamada",
"email": "taro.yamada@example.com",
"roles": [
"Administrator",
"Editor"
],
"active": true
}
}
""";
System.out.println("--- Java 15 Text Block JSON ---");
System.out.println(jsonData);
System.out.println("------------------------------");
}
}
なるほど見やすい。
実際に実行したところ、記述した文字列と出力文字列に差が無いので変数の内容をイメージしやすいだろうなという感想になりました。
JSON以外でもエスケープの入力が必要になる文字を編集する場合は楽そうですね。
Java 16の変更点
GeminiセレクトによるJava 16の主要な変更点は2つでした。
instanceof演算子のパターンマッチング
個人的にどうにも覚えられないもの上位に入るパターンマッチング。
ろくに使った記憶がないinstanceof演算子と一緒に学び直しです。
instanceof演算子とは
instanceof演算子は、あるオブジェクトが指定されたクラス(またはそのサブクラス)のインスタンスであるか、あるいは指定されたインターフェースを実装しているかどうかを判定するための演算子です。
Object型はすべてのオブジェクトの親クラスなのでプリミティブ型1以外であれば基本なんでも入れられるのですが、何でも入れられるということは何が入っているかわからないということ。
パラメータで受け取ったObject型をStringだと思って文字列操作しようとしたらBigDecimalだったためにエラーになった2、とかならないためには受取先で中身を確認する必要があります。
そんな時にinstanceofを使って、Object型をキャストするわけですね。
Object obj = "Hello, Java!";
if (obj instanceof String) {
String s = (String) obj; // 型チェック後に明示的なキャストが必要
System.out.println("文字列の長さ: " + s.length());
}
で、Java 16からこのinstanceof演算子にパターンマッチングが使えるようになったと。
これもGeminiにサンプルコードをお願いしました。
パターンマッチング対応前がこちら。
public class BeforeJava16 {
public static void main(String[] args) {
processObject("こんにちは");
processObject(123);
processObject(null);
}
public static void processObject(Object obj) {
// 1. instanceof で型をチェック
if (obj instanceof String) {
// 2. 明示的に String 型へキャスト
String s = (String) obj;
// 3. キャストした変数を使用
System.out.println("文字列です: " + s + ", 長さ: " + s.length());
} else if (obj instanceof Integer) {
Integer i = (Integer) obj;
System.out.println("整数です: " + i);
} else {
System.out.println("文字列でも整数でもありません。");
}
}
}
続いて対応後、Java 16バージョン。
public class SinceJava16 {
public static void main(String[] args) {
processObject("こんにちは");
processObject(123);
processObject(null);
}
public static void processObject(Object obj) {
// 1. instanceof で型チェックと同時にパターン変数 's' を宣言
if (obj instanceof String s) {
// 2. キャスト不要で、パターン変数 's' を直接使用
// 's' のスコープはこの if ブロック内 (正確にはフロー スコーピングに従う)
System.out.println("文字列です: " + s + ", 長さ: " + s.length());
} else if (obj instanceof Integer i) {
// 同様に Integer 型の場合もパターン変数 'i' を使用
System.out.println("整数です: " + i);
} else {
System.out.println("文字列でも整数でもありません。");
}
// System.out.println(s); // ここでは変数 s はスコープ外のためコンパイルエラー
}
}
対応前が「型の判定→キャスト」と段階を踏んでいたのに対して、対応後は型の判定とキャストを同時にやっている感じですね。
便利だしわかりやすい…と思うのと同時に、私の頭に疑問符が。
パターンマッチングって正規表現と違うの?
Javaの型/構造パターンマッチング
上記質問をGeminiに聞いてみたところ、「違います」と即答。
違いを表にしてくれたので以下に引用します。
特徴 | 正規表現のパターンマッチング | Javaの型/構造パターンマッチング |
---|---|---|
主な対象 | 文字列 (テキスト) | オブジェクトの型や構造 |
パターン | 特殊文字を使ったテキスト形式 (.* , \d+ など) |
型名、変数名、レコード構成要素 (String s , Point(x, y) ) |
主な目的 | テキスト検索、検証、置換 | 型検査、安全なキャスト、構造の分解と要素の取り出し |
Java実装 |
java.util.regex パッケージ |
instanceof 演算子、switch 文/式 |
ここでのパターンマッチングは「型と一致するかどうかを判定し、一致する場合はキャストして変数に取り出す」という意味だったようです。
最初に「instanceof演算子のパターンマッチング」と見て「インスタンスを正規表現で書く…?」とか思って混乱したのですが、これでようやく合点がいきました。
Recordクラス
今回最後にしてたぶん最大の変更、Recordクラスの追加です。
Recordクラスはテータを保持することを目的としたクラスで、一度生成すると内部の値を変更することはできません。
わざわざ追加しなくても普通にプロパティだけのクラス作ったら良いのでは?と思ったのですが、内部の値を変更できないというのが重要なのでしょうか。
とりあえずサンプルコードを見ましょう。
まずJava 6バージョン。
// Java 6 (getter, setter, equals, hashCode, toStringを手動で実装)
public class Java6Person {
private final String name;
private final int age;
public Java6Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Java6Person that = (Java6Person) o;
if (age != that.age) return false;
return name != null ? name.equals(that.name) : that.name == null;
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
@Override
public String toString() {
return "Java6Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
getterとかsetterとか、いちいち書かないといけないのを改めて見ると面倒ですね。
Springやlombokのおかげでほとんど業務では書かないので忘れてました。
続いてJava 16バージョン。
// Java 21 (Recordを使用)
public record Java21Person(String name, int age) { }
//recordを使用することで、同値比較、ハッシュ値、文字列表現のメソッドが自動で定義されます。
短すぎる。
ちょっとびっくりするくらい短い。
getter/setterだけでなくequalsやtoStringも自動で定義してくれるのであればコード書くのも読むのも確かに楽でしょうね。
イミュータブルであることのメリットについては具体的な使い方をイメージできないのでちょっとまだあやふやですが、使いこなせたらすごく便利そうです。
一旦まとめ
学び直しも残りは17と21になりました。
サンプルコードのほとんどは実際に動かして確認していることもあり、変更内容を実感しながら進められていると思います。
次回無事に終われるようにがんばります。