進歩についていけない焦りを抱えた50代が、

若手のコードを理解できるようになることを目標に、Java7以降を一から学び直す記録です。
ラベル Java7 の投稿を表示しています。 すべての投稿を表示
ラベル Java7 の投稿を表示しています。 すべての投稿を表示

2026年2月11日水曜日

【Java進化史 第9回】Java7 〜nullと戦ってきた世代へ〜

昔、nullと戦っていた。

NullPointerException。
あの文字を見るだけで、少し気分が重くなる。

だから、書いた。
ひたすら書いた。


if (obj == null) {
    throw new IllegalArgumentException("obj is null");
}

引数の数だけ書く。
メソッドの数だけ書く。

equalsを書くときも同じだった。


(a == b) || (a != null && a.equals(b))

毎回これを書く。
コピペする。
少し修正する。

死ぬほどNullチェックを書いた。

でも、Nullぽが出るんだ。

あれだけ守ったつもりなのに。
あれだけ防御したつもりなのに。

■ それはボイラープレートだった

こういう「毎回ほぼ同じ形で書かされる定型コード」を、 ボイラープレート(boilerplate)という。

本質的な業務ロジックではない。
だが、安全のために、仕様のために、必ず書かされるコード。

nullチェックもそう。
equalsの安全比較もそう。

本当に書きたいのは業務ロジックなのに、
周辺の儀式コードがどんどん増えていく。

可読性は下がる。
コピペミスも起きる。
そして本質が埋もれる。

だから思った。

「これ、言語や標準API側で何とかならないのか?」

■ Commonsを入れる時代

多くの現場で入れた。

  • Jakarta Commons Lang
  • Apache Commons Lang

ObjectUtils.equals。
Validate.notNull。
HashCodeBuilder。

便利だった。
本当に助かった。

だが同時に、こうも思っていた。

「これ、最初からJDKにあってもよくないか?」

■ Java7で静かに入ったもの

Java7で java.util.Objects クラスが登場する。


Objects.equals(a, b);
Objects.requireNonNull(obj);
Objects.hash(f1, f2, f3);

たったこれだけだ。

派手ではない。
ラムダもない。
文法革命でもない。

だが、あの頃の自分には刺さった。

■ null安全な比較


Objects.equals(a, b);

もう


(a == b) || (a != null && a.equals(b))

を書かなくていい。

条件式の読み間違いも減る。
equalsの呼び出し元がnullでも落ちない。

コードが静かになる。

■ 引数チェックも一行で


this.name = Objects.requireNonNull(name);

if文を書かなくていい。

「nullなら例外」
その意図が一行で伝わる。

しかも例外は明示的。
どこで壊れたかがすぐ分かる。

■ hashCodeも簡潔に


@Override
public int hashCode() {
    return Objects.hash(id, name, age);
}

昔は31を掛けた。
あるいはHashCodeBuilderを使った。

それが標準で書ける。

■ Commonsはいらなくなったのか?

全部ではない。

StringUtilsもある。
CollectionUtilsもある。
高度なユーティリティはまだ強い。

だが少なくとも、

  • null安全比較
  • nullチェック
  • hashCode生成

このあたりは標準で足りる。

毎回Commonsを入れなくてもいい。
社内共通ライブラリを作らなくてもいい。

JDKだけで書ける。

■ これは思想の変化

Javaは昔、最小限のライブラリだった。

足りないものは外部で補う。

だがJava7は少し違った。

多くの現場で繰り返されている安全処理は、標準に取り込む。

それはつまり、

ボイラープレートを減らす方向への進化

だった。

■ あの頃の自分に言いたい

死ぬほどnullチェックを書いていた自分へ。

それでもNullぽが出て、夜にログを追いかけていた自分へ。

少しだけ、楽になる。

Java7は派手ではない。

だが、

地味だけど、確実に設計や実装の質を上げる進化をしている。

こういう進化、好きなんだよなぁ。




【Java進化史 第8回】Java7 〜例外を“正確に投げ直せる”ようになった日〜

昔、本当に例外処理ばかり書いていた。

正常処理より catch の方が長い。
そんなクラスも珍しくなかった。

とにかく捕まえる。
とにかく投げる。
とにかく throws を増やす。

そして気がつけば、メソッドの宣言はこうなる。


public void process() throws Exception

もう何でもありだ。

「結局、何が飛んでくるの?」

呼び出し側はこう書く。


try {
    process();
} catch (Exception e) {
    // で、何が来るの?
}

IOException?
SQLException?
それとも別の何か?

結局、process() を読む。
さらにその中で呼ばれているメソッドも読む。

これをカプセル化と呼べるのか。

メソッドのシグネチャを見れば契約が分かる。
それがオブジェクト指向の約束ではなかったのか。

だが現実は違った。

throws Exception は便利だった。

便利だが、それは「設計の曖昧さ」を広げていただけだった。

■ Java6までの“濁り”

例えば、こんなコード。


public void process() throws Exception {
    try {
        readFile();   // throws IOException
    } catch (Exception e) {
        throw e;
    }
}

readFile() は IOException しか投げない。

だが catch (Exception e) と書いた瞬間、
コンパイラはこう判断する。

「Exception が投げられる可能性がある」

だから throws Exception になる。

正確ではない。
広がっている。

そして、その広がりは呼び出し側へ伝染する。

設計が、少しずつ濁っていく。

■ Java7の“地味な革命”

Java7で導入されたのが、
precise rethrow(例外の再スローの型推論) だ。

同じコードでもこう書ける。


public void process() throws IOException {
    try {
        readFile();   // throws IOException
    } catch (Exception e) {
        throw e;
    }
}

一見、何も変わっていない。

だがコンパイラが変わった。

Java7はこう考える。

「tryブロックの中で、実際に投げられる例外型は何か?」

readFile() が投げるのは IOException だけ。

だから再スローされる型も IOException だけだと判断する。

その結果、


throws IOException

で済む。

例外が“正確になる”。

■ 何が嬉しいのか

  • throws が広がらない
  • APIが引き締まる
  • 呼び出し側が迷わない
  • シグネチャが契約として機能する

呼び出し側はこう書ける。


try {
    process();
} catch (IOException e) {
    // 何が来るか分かっている
}

読まなくていい。
追いかけなくていい。

契約が戻ってくる。

これは小さいが、本質的な改善だ。

■ それでも万能ではない

catch 変数を再代入すると、この推論は効かない。


catch (Exception e) {
    e = new Exception();  // 再代入すると従来通り広がる
    throw e;
}

コンパイラが追跡できる範囲でのみ働く。

魔法ではない。
だが思想は明確だ。

「正確に宣言せよ」

■ 例外はどこで処理するのか

  • 発生箇所 → 技術的例外をそのまま投げる
  • 中間層 → 無理に握りつぶさない
  • 最上位 → ログ、通知、終了判断

Java6時代、広がった throws Exception は、その責任を曖昧にした。

Java7のprecise rethrowは、例外を“適切な型で上に渡す”ことを助ける。

それはつまり、責任の所在を明確にすることだ。

■ あの頃の自分に言いたい

Java7の再スローは地味だ。

だが、例外に振り回されてきた世代にとっては、 設計を少しだけ取り戻すための機能だった。

派手な進化ではない。
だが、確かに“進化”だった。

地味だけど、確実に設計や実装の質を上げる「例外の再スローの型推論」。
こういう機能、好きだなぁ。

【Java進化史 第7回】Java7 〜マルチキャッチでcatch地獄脱出〜


Java6までしか知らない自分にとって、 例外処理は「とにかくcatchを書くもの」だった。

同じ処理でも、例外ごとに延々とcatchを書く。 あの頃、どれだけcatchを書いただろう。


■ Java6以前の世界 〜catch地獄〜


try {
    process();
} catch (IOException e) {
    logger.error(e);
} catch (SQLException e) {
    logger.error(e);
} catch (ParseException e) {
    logger.error(e);
}

全部同じ処理なのに、分けて書くしかなかった。

コピペして、微妙に修正して、また増える。 気づけばcatchだらけ。

例外が増えるたびに、catchも増える。 設計ではなく、文法に縛られていた。


■ Java7で登場したマルチキャッチ


try {
    process();
} catch (IOException | SQLException | ParseException e) {
    logger.error(e);
}

一行で済む。

たったそれだけの違い。 しかし、これは確実に「進化」だった。

例外を「どう扱うか」でまとめられるようになったからだ。


■ 例外クラス自体は変わらない


public class BusinessException extends Exception {
    public BusinessException(String message, Throwable cause) {
        super(message, cause);
    }
}

例外を作る方法は変わらない。




■ Java7がもたらした本当の進化

マルチキャッチは単なる文法改善ではない。

「同じ扱いをする例外は、まとめられる」

つまり、

例外は“型”ではなく、“扱い”で考える。

Java7で、ようやく catch地獄から脱出できた。

50代、Javaを学び直して思う。

【Java進化史 第6回】Java7 - 数字は読めるようになったのか ― Java7数値リテラルと浮動小数点の現実

Java6までしか知らない50代が、Java7以降をやり直すシリーズ。

今回はJava7で改善された数値リテラルと、
その裏にある浮動小数点の現実について整理する。


■ 昔、数字は読めなかった

昔、数値は「正しければいい」と思っていた。
だが、あとからコードを読むとこうなる。

「これ、何桁だ?」


double amount = 1000000.123456;
double rate   = 0.00001234;
  • 桁が読みにくい
  • 金額か割合か直感で分からない
  • コメントが必要になる

ビットマスクも同じだった。


int mask = 8;   // 1000?

毎回、頭の中で2進数に変換していた。


■ Java7で何が変わったのか

1. アンダースコアが使えるようになった


double amount = 1_000_000.123_456;
double rate   = 0.000_012_34;

桁が視覚的に分かる。
コメントがなくても意味が伝わる。

2. 2進数リテラル(0b)が使えるようになった


int mask = 0b1000;
int flag = 0b0101;

ビットの意味が直接読める。

Java7は、大きな変更だけでなく、
読む人のための改善を入れてきた。


■ しかし ― 浮動小数点の罠

数字が読みやすくなっても、
計算が正確になったわけではない。

0.1 + 0.2 問題


System.out.println(0.1 + 0.2);

出力:


0.30000000000000004

浮動小数点は2進数で近似表現される。
そのため、正確な10進小数にはならない。


■ 浮動小数点どうしの比較は危険

危険な比較


if (a == b) {
    // 同じ?
}

閾値(ε)を使う


double EPS = 1e-10;

if (Math.abs(a - b) < EPS) {
    // ほぼ同じとみなす
}

実務ではこの考え方が必須になる。


■ 正確な計算が必要なら BigDecimal


BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
BigDecimal result = a.add(b);

System.out.println(result); // 0.3

ポイントは文字列で生成すること

やってはいけない例


BigDecimal x = new BigDecimal(0.1);
System.out.println(x);

出力:


0.1000000000000000055511151231257827021181583404541015625

doubleの誤差をそのまま引き継いでしまう。

安全な書き方


BigDecimal x = new BigDecimal("0.1");
// または
BigDecimal y = BigDecimal.valueOf(0.1);

■ 昔、私はこう信じていた

Javaを習い始めたころ、
「小数点があるなら float や double だろう」と思っていた。

そして見事にハマった。

  • 計算はズレる
  • 比較は一致しない
  • 表示すると変な桁が出る

そのとき初めて知った。

浮動小数点は“正確な小数”ではない。


■ まとめ

項目 float/double BigDecimal
精度 近似値 正確
2進数表現 あり なし(10進ベース)
比較 誤差考慮が必要 equalsで比較可能
金融用途 不向き 推奨

Java7は数字を読みやすくした。
だが、数字の本質は変わらない。

「数字が読める」ことと
「数字が正しい」ことは別問題である。




【Java進化史 第5回】Java7 - NIO.2 ― Fileで戦った俺たちは、なぜ疲れていたのか

Java6までしか知らない50代が、Java7以降をやり直すシリーズ。

今回は Java7で導入された NIO.2(java.nio.file) を扱う。


■ あの頃、ファイル操作は「共通処理」だった

昔、ファイル操作はすべて共通処理にしていた。
ディレクトリ再帰、コピー、削除、存在チェック。
全部、自分たちで書いた。
テストも自分たちでやった。

例外処理も、ログも、バッファサイズも、全部。

正直、たいへんだった。

でも当時は、それが「実力」だと思っていた。


■ Java6以前 ― Fileクラスの世界


▼ 再帰削除(Java6)


public static void deleteDirectory(File dir) {
    if (dir.isDirectory()) {
        File[] files = dir.listFiles();
        if (files != null) {
            for (File file : files) {
                deleteDirectory(file);
            }
        }
    }
    dir.delete();
}
  • listFiles() が null を返す
  • delete() は boolean を返すだけ
  • 失敗理由がわからない
  • 再帰は自分で書く

だから「共通処理」にした。
そうしないと怖かった。

▼ ファイルコピー(Java6)


public static void copy(File src, File dest) throws IOException {
    InputStream in = new FileInputStream(src);
    OutputStream out = new FileOutputStream(dest);
    byte[] buffer = new byte[1024];
    int length;
    while ((length = in.read(buffer)) > 0) {
        out.write(buffer, 0, length);
    }
    in.close();
    out.close();
}
  • close漏れの恐怖
  • 例外時の後処理
  • バッファサイズ設計
  • テストケースの山

「File操作ユーティリティ」は、どの現場にもあったはずだ。


■ そしてJava7 ― NIO.2の登場

Java7で導入されたのが java.nio.file パッケージ
通称 NIO.2。

▼ Pathという思想


Path path = Paths.get("sample.txt");

Fileはクラス。
Pathはインターフェース。

ここに設計思想の違いがある。


▼ コピー(Java7)


Files.copy(
    Paths.get("a.txt"),
    Paths.get("b.txt"),
    StandardCopyOption.REPLACE_EXISTING
);

終わり。
共通処理、いらない。


▼ 再帰削除(Java7)


Files.walk(Paths.get("targetDir"))
     .sorted(Comparator.reverseOrder())
     .forEach(p -> {
         try {
             Files.delete(p);
         } catch (IOException e) {
             e.printStackTrace();
         }
     });
  • 再帰を意識しない
  • APIが責任を持つ
  • 例外が明確

■ NIO.2の「2」の意味

NIO(Java1.4)は Non-blocking IO。
だが NIO.2 の本質は非同期ではない。

File APIの再設計 である。

Fileクラスは拡張性が低く、例外設計も弱く、 シンボリックリンク対応も不十分だった。

だから、作り直した。


■ 思ったこと

あの頃、俺たちはファイル操作を「共通化」していた。

それは技術力の証だと思っていた。

でも違った。

足りなかったのは俺たちじゃない。
足りなかったのはAPIだった。


■ まとめ

項目 Java6以前 Java7以降
再帰処理 自分で実装 Files.walk
コピー ストリーム手書き Files.copy
削除 boolean戻り値 IOException
設計思想 クラス中心 インターフェース中心

NIO.2は単なる便利機能ではない。
設計思想の転換点だった。




2026年2月8日日曜日

【Java進化史 第3回】Java7 - switch文は何を守り、何を変えなかったのか?

Java7には、もう一つ地味な変更がある。

switch文で String が使えるようになった ことだ。


■ Java6までのswitch

switchで使えるのは、 int や enum などの限定的な型だけだった。


switch (statusCode) {
    case 0:
        ...
        break;
    case 1:
        ...
        break;
}

文字列で分岐したい場合、 if-else を使うことが多かった。


if ("OK".equals(status)) {
    ...
} else if ("ERROR".equals(status)) {
    ...
}

あるいは、enumを用意して対応する。

少し回り道が必要だった。


■ Java7での変更

Java7からは、 Stringがswitchで使えるようになった。


switch (status) {
    case "OK":
        ...
        break;
    case "ERROR":
        ...
        break;
}

可読性は確かに上がる。

素直に書けるようになった。

これは小さいが、現場では確実に効く変更だ。


■ だが、変わらなかったものがある

それが fall-through だ。

breakを書かなければ、 次のcaseへ処理が流れる。


switch (level) {
    case 3:
        log("HIGH");
    case 2:
        log("MEDIUM");
    case 1:
        log("LOW");
        break;
}

これはC言語由来の仕様。

Javaはここを変えなかった。


■ 他言語との違い

近年の言語では、 fall-throughは基本的に採用されていない。

  • Kotlin → 自動で終了(break不要)
  • Swift → 自動で終了
  • Rust → fall-throughなし

一方で、CやC++では明示的なbreakが必要だ。

breakを書き忘れてバグになる。

経験のある人も多いのではないだろうか。


■ なぜJavaは残したのか?

理由は明確だ。

後方互換性

既存コードを壊さない。

Javaはこの原則を徹底している。

そしてもう一つ。

Javaは「削除」ではなく 「追加」で進化する言語だからだ。

古い構文は残す。

その上で、新しい選択肢を足していく。


■ C言語世代の安心感

50代後半以上のエンジニアの多くは、 C言語からキャリアを始めたのではないだろうか。

私もその一人だ。

fall-throughを見ると、 どこか安心する。

「ああ、これはCと同じだ」と。

JavaがCに似ている部分を残しているのは、 偶然ではない気がする。

だが同時に、 言語設計は少しずつ安全性と明確さへ向かっている。

守るものと、変えるもの。

そのバランスの上に、 Javaの進化はある。


■ まとめ

Java7のswitch変更は、 革命ではない。

だが、

Javaは「何を変えないか」を選び続けている。

それがJavaらしさだ。

守りながら、少しずつ進む。

それがこの言語の進化の形なのだと思う。





【Java進化史 第2回】Java7 - diamond演算子は何を変えたのか?

最初に見たとき、正直こう思った。

「なんじゃこりゃ?」

「そもそも演算子なのか?」

「三項演算子に似てるな…?」

だが、使ってみると――

地味に、楽。


■ Java6までの書き方


List<String> list = new ArrayList<String>();

型を2回書く。

宣言側と、インスタンス生成側。

Genericsが導入されたJava5以降、 この「2回書き」は当たり前だった。

特に型が長くなると、地味に辛い。


■ Java7で何が変わったのか


List<String> list = new ArrayList<>();

右側の型が消えた。

<> だけ。

これが diamond演算子 と呼ばれる。

見た目がダイヤモンドに見えるからだ。


■ 何が嬉しいのか?

  • 記述が短くなる
  • 可読性が上がる
  • 型変更が楽になる

例えば、


List<Integer> list = new ArrayList<Integer>();


List<Long> list = new ArrayList<Long>();

に変えるとき、 2箇所修正が必要だった。

diamondなら1箇所で済む。

これは地味に効く。


■ 本当に「演算子」なのか?

実は、厳密には演算子ではない。

Genericsの型推論を コンパイラに任せるための構文だ。

つまり、

型推論の第一歩


■ 2記事書いて感じたこと

正直に言うと、 私はこれまでJava7を体系的に学んだことがなかった。

今回あらためて調べ、 第1回のtry-with-resources、 そして今回のdiamond演算子を追ってみて思った。

Java7は派手ではない。

だが、

開発者の「面倒くささ」を確実に減らしている。

例外処理の煩雑さを減らし、 Genericsの重複記述を減らし、 小さなストレスを一つずつ削っている。

革命ではない。

だが、 確実に優しい進化だ。

この2本を書いてみて、 Java7は「地味だけど開発者想いのバージョン」だったのではないか、 と感じ始めている。




【Java進化史 第1回】Java7 - try-with-resourcesは何を変えたのか?

Java7は地味だと言われる。

だが、現場を救った機能がある。

それが try-with-resources だ。


■ 昔のclose地獄

Java6まで、リソースを扱うコードはこうだった。


FileInputStream fis = null;
try {
    fis = new FileInputStream("test.txt");
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

何度これを書いただろう。

  • close忘れ
  • 二重try
  • ネスト地獄

そして一番怖いのは、 close忘れによるDB接続リークだ。

Javaから接続しているDBコネクションが解放されず、 接続数がMAXに達する。

ある日突然、全体が止まる。

原因は、たった一行のclose忘れ。

実際に、トラブルの火種になり得る。


■ JDBCコネクションプールとの関係

「でも今はコネクションプールがあるから大丈夫では?」

確かに、最近の現場では HikariCPなどのコネクションプールを使うのが普通だ。

だが重要なのはここだ。

close()は“破棄”ではない。

プール利用時のclose()は、 接続を物理的に切断するのではなく、 プールへ「返却」しているだけだ。

つまり、close()を呼ばなければ、 プールに戻らない。

結果として、

  • 接続が枯渇する
  • 待ちが発生する
  • 最悪、システム停止

try-with-resourcesは、 プール利用環境でも極めて重要なのだ。


■ 実務では自分で書かない?

正直に言うと、 DB接続処理などは共通部品化されていることが多い。

だから普段は、自分でcloseを書くことは少ない。

だが――

「ちょっとした検証コード」
「一時的なバッチ」
「ローカルでのテスト」

こういうときに、自分で書く。

そして、うっかりcloseを書き忘れる。

私はある。


■ Java7で何が変わったのか


try (FileInputStream fis = new FileInputStream("test.txt")) {
    // 処理
} catch (IOException e) {
    e.printStackTrace();
}

tryの丸括弧の中に「resource」を書く。

すると、ブロック終了時に自動でclose()が呼ばれる。

finallyは不要。

ネストも不要。


■ 名前の意味

try-with-resources。

直訳すれば、

「リソース付きtry文」。

リソース(=closeが必要なもの)を tryと一緒に宣言する。

だから、try with resources。


■ AutoCloseableとは何か

対象となるのは、 AutoCloseable を実装しているクラスだ。

つまり「close()メソッドを持つ型」。

  • ファイル(FileInputStream / BufferedReader など)
  • データベース(Connection / Statement / ResultSet)
  • ソケット(Socket)
  • ZipFile
  • Scanner

JDK標準ライブラリの多くはAutoCloseableに対応している。

「closeが必要なものは大体いける」と思ってよい。

さらに、自作クラスでも実装できる。


class MyResource implements AutoCloseable {
    public void close() {
        System.out.println("closed");
    }
}

これは単なる糖衣構文ではない。

リソース管理の統一ルールを作った機能なのだ。


■ Java6方式と混在しても動くのか?

結論から言えば、動く。

だが、保守性は確実に落ちる。

  • 書き方が統一されない
  • レビュー時の負担が増える
  • ミスの温床になる

可能であれば段階的に置き換えるべきだ。

もちろん、 予算と時間が許せば。

だが新規コードでは、必ず使う。


■ まとめ

派手さはない。

ラムダのような衝撃もない。

だが、

try-with-resourcesは、 「書きやすくした機能」ではない。

事故を減らすための設計思想だ。

Javaは派手に変わることもある。
だが、本当に現場を救うのは、
こういう地味な進化かもしれない。





【Java進化史 第27回】Java9でSetとMapにも of() が? 〜 なんじゃこりゃ、の続き 〜

前回、こんなコードで手が止まった。 List<String> names = List.of("Tanaka", "Sato", "Suzuki"); List.of() 。 変更できないリストを、一...