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

若手のコードを理解できるようになることを目標に、

Java7以降を一から学び直す記録です。

2026年2月28日土曜日

【Java進化史 第26回】Java9でListは何がどう変わったのか 〜 List.ofってなんじゃこりゃ? 〜

若手のコードをレビューしていて、手が止まった。


List<String> names = List.of("Tanaka", "Sato", "Suzuki");

……なんじゃこりゃ?

of

どこから来た?

new ArrayList<>() は?

一瞬、何をしているのか分からなかった。


■ Java7以前ならどう書いたか

まずは、昔の世界を思い出す。


List<String> names = new ArrayList<>();
names.add("Tanaka");
names.add("Sato");
names.add("Suzuki");

あるいは、


List<String> names = Arrays.asList("Tanaka", "Sato", "Suzuki");

これなら分かる。

リストを作っている。

見慣れた風景だ。


■ Java9で何が追加されたのか

Java9で追加されたのは、 コレクションのファクトリメソッド。

List.of()
Set.of()
Map.of()

簡単にコレクションを作れるようになった。

それだけ、と言えばそれだけ。


■ 何がどう違うのか

だが、ひとつ大きな違いがある。

List.of() で作ったリストは、 変更できない。


names.add("Yamada"); // 実行時例外

追加も削除もできない。

いわゆる immutable(変更不可)なリストだ。


■ 若手のコードをどう読むか

若手のコードに List.of() が出てきたら、 こう読む。

  • これは固定データだな
  • あとから変更しない前提だな
  • 安全側に倒しているな

昔の感覚だと、 「とりあえず ArrayList」だった。

だが今は違う。

変更しないなら、最初から変更できない形にする。

設計の発想が、少し変わっている。


■ 革命ではない。でも、読めないと止まる

Java8のような派手さはない。

だが、 知らないと読めない。

そして、 読めないと、レビューで止まる。

「なんじゃこりゃ?」で固まる。

それを一つずつ潰していく。

Java9は、そういうバージョンなのかもしれない。


2026年2月23日月曜日

【Java進化史 第25回】Java9って、あったっけ? 〜 なぜ話題にならなかったのか 〜

このブログで、Java8をあらためて勉強した。

ラムダ。 Stream。 Optional。

書き方そのものが変わっていた。

50代エンジニアが、若手のコードを読めなくなる状況を作った元凶だと知った。

では、その次は?

Java9。

……正直、印象が薄い。

あったっけ?


■ なぜ記憶が薄いのか

理由はいくつかありそうだ。

  • LTSではなかった
  • すぐにJava11が来た
  • 現場が積極的に採用しなかった

企業の多くは、Java8のまま様子を見た。

気がつけば、次はJava11。

Java9は、通過点のように見えた。


■ 革命ではなかった

Java8は「書き方」を変えた。

だがJava9は、そこまで派手ではない……らしい。

新しい構文が増えたわけでもない……ようだ。


■ モジュール? なんじゃそれ

Java9で、モジュールなるものが登場したらしい。

モジュール。

なんじゃそれ。

聞き慣れない。

なんとなく、 COBOLとかの世界に出てきそうな単語だ。

それがJavaに入ったという。


■ 本当に地味だったのか

派手ではない。

細かい改善は、いくつもあるらしい。

  • コレクション生成の簡略化
  • Stream APIの拡張
  • Optionalの強化
  • interfaceのprivateメソッド

名前だけ見ると、 正直あまりピンとこない。

本当に何もなかったのか。

それとも、 ちゃんと意味のある進化だったのか。

少しずつ、見てみよう。



【Java進化史 第24回】Java8で日付APIは何がどう変わったのか 〜 DateとCalendarからの卒業 〜

風の噂で聞いた。

Java8で、日付系のAPIが変わったらしい。

「また何か増えたのか?」

正直、最初はその程度の認識だった。

だが調べてみると、これは単なる追加ではない。

DateとCalendarからの、事実上の世代交代だった。


■ なぜ変更されたのか(軽く)

Java7以前、日付といえばこれだった。


java.util.Date
java.util.Calendar
java.text.SimpleDateFormat

動く。

だが、扱いづらい。

  • DateやCalendarはmutable(変更可能)
  • SimpleDateFormatはスレッドセーフではない
  • 月が0始まりなど、直感的でない仕様

そこでJava8で追加されたのが、 java.time パッケージである。


■ 何がどう変わったのか

① 現在日時の取得

Java7


Date now = new Date();

Java8


LocalDateTime now = LocalDateTime.now();

何が変わったか。

Dateは「日付と時刻の塊」だった。

LocalDateTimeは「日付+時刻」という意味が明確である。


② 日付だけ取得

Java7


Calendar cal = Calendar.getInstance();
int year = cal.get(Calendar.YEAR);

Java8


LocalDate today = LocalDate.now();
int year = today.getYear();

フィールド定数が不要になった。 読みやすさが違う。


③ 日付加算

Java7


Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_MONTH, 3);

Java8


LocalDate result = LocalDate.now().plusDays(3);

plusDays。 何をしているかが、そのまま読める。


④ フォーマット

Java7


SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String str = sdf.format(new Date());

※ SimpleDateFormatはスレッドセーフではない。

Java8


DateTimeFormatter formatter =
        DateTimeFormatter.ofPattern("yyyy-MM-dd");

String str = LocalDate.now().format(formatter);

DateTimeFormatterは不変(immutable)。 安全に使える。


⑤ 文字列から日付へ

Java7


SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = sdf.parse("2026-02-22");

Java8


LocalDate date = LocalDate.parse("2026-02-22");

APIが自然になった。


⑥ 日付の差分

Java7


long diff = date2.getTime() - date1.getTime();
long days = diff / (1000 * 60 * 60 * 24);

ミリ秒計算を自分でやる必要があった。

Java8


long days = ChronoUnit.DAYS.between(date1, date2);

意図が明確になった。


⑦ タイムゾーン

Java7


TimeZone tz = TimeZone.getTimeZone("Asia/Tokyo");
Calendar cal = Calendar.getInstance(tz);

Java8


ZonedDateTime now =
        ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));

日付・時刻・タイムゾーンが一体として扱える。


■ 結局、何が変わったのか

  • mutable → immutable
  • 曖昧 → 役割分離
  • 計算ベース → 意味ベースAPI

書きやすくなった。

それだけではない。

バグが入りにくくなった。

地味に見える。

だが実務では、かなり大きい進化だ。




2026年2月22日日曜日

【Java進化史 第23回】Java8でBase64は何がどう変わったのか 〜 地味だが確かな進化 〜

風の噂で聞いた。

Java8でBase64が正式に追加されたらしい。

「いや、前からあっただろう?」

そう思った人も多いはずだ。

確かに、Java7以前でもBase64は使えていた。


// Java7以前(内部API)
import sun.misc.BASE64Encoder;
import sun.misc.BASE64Decoder;

// 内部実装。公式仕様ではない
BASE64Encoder encoder = new BASE64Encoder();
String encoded = encoder.encode("Hello".getBytes());

使えてはいた。

しかし、sun.* は内部API。

将来の保証はない。

そしてJava8で、正式APIが追加された。

■ Java8で追加されたBase64


import java.util.Base64;

String original = "Hello Java8";

// 通常のBase64エンコード
String encoded = Base64.getEncoder()
                       .encodeToString(original.getBytes());

// デコード
String decoded = new String(
        Base64.getDecoder().decode(encoded)
);

java.util.Base64。

内部APIから、正式仕様へ。

これが最大の変化だ。

■ 何がどう変わったのか

① 内部API → 標準APIへ

  • Java7以前:sun.misc(内部API)
  • Java8以降:java.util.Base64(正式サポート)

安心して使える。

地味だが、これは大きい。

② 用途別に整理された


// 改行なし(一般的な用途)
Base64.getEncoder();

// URL安全(+ と / を置換)
Base64.getUrlEncoder();

// MIME形式(76文字ごとに改行)
Base64.getMimeEncoder();

以前は用途の区別が曖昧だった。

Java8では、意図がAPIに現れている。

③ 改行仕様が明確になった

  • 通常Encoder:改行しない
  • MIME Encoder:76文字ごとにCRLF改行

メール添付や一部仕様では、 この違いが重要になる。

④ 例外仕様が整理された


try {
    Base64.getDecoder().decode("不正な文字列");
} catch (IllegalArgumentException e) {
    // 不正なBase64文字列
}

decode時の挙動も明確だ。

■ 地味だが、いい進化

Java8はラムダやStreamが目立つ。

しかしその裏で、 こうした基盤の整備も行われていた。

派手ではない。

だが確実に実務を支える変更だ。

地味だが、いい進化だなぁと思う。




【Java進化史 第22回 後編】defaultメソッドの注意点 〜 どこまで“クラス化”したのか 〜

前回、インターフェースに実装を書くという事実に、 正直な違和感を覚えた。

今回は、少し冷静に整理してみる。

defaultメソッドは、 どこまで許されているのか。

■ defaultメソッドは何ができるのか

まずは単純な例から。


public interface Sample {

    int VALUE = 10; // 定数は持てる(public static final)

    int calc(int x);

    default int doubleCalc(int x) {
        return calc(x) * VALUE;
    }
}

ここで注目すべき点は2つある。

  • インターフェースは定数(public static final)しか持てない
  • defaultメソッドは他の抽象メソッドを呼び出せる

つまり、 defaultメソッドは「振る舞いの共通化」はできる。

しかし、 状態は持てない。

インスタンスフィールドは宣言できない。 コンストラクタもない。

ここは、クラスとは決定的に違う。

defaultメソッドは、 クラスの代わりではない。

あくまで、 契約の延長線上にある“補助的な実装”だ。

■ 多重継承はどうなるのか

では、インターフェースが複数あったらどうなるのか。


interface A {
    default void hello() {
        System.out.println("A");
    }
}

interface B {
    default void hello() {
        System.out.println("B");
    }
}

class C implements A, B {
}

これはコンパイルエラーになる。

どちらのhelloを使うのか、 曖昧だからだ。

Javaは、この曖昧さを許さない。

必ず、実装クラス側で明示的に解決する必要がある。


class C implements A, B {

    @Override
    public void hello() {
        A.super.hello();
    }
}

このように、 どのインターフェースの実装を使うかを 明示的に指定する。

「なんとなく動く」は許されない。

■ 原則は壊れていない

インターフェースは、今でも状態を持たない。

完全なクラスになったわけではない。

defaultメソッドは、 無制限の自由ではない。

制御された拡張である。

だからこそ、 長年守られてきた設計思想は、 完全に崩れたわけではない。

むしろ、 後方互換性を守りながら進化するための、 現実的な選択だったと言える。

違和感はある。

だが、無秩序ではない。

そこが、Javaらしさなのかもしれない。




【Java進化史 第22回 前編】Java8 〜 インターフェースに実装を書く?defaultメソッドはアリなのか 〜

若手のコードを見て、手が止まった。


public interface Calculator {

    int add(int a, int b);

    default int subtract(int a, int b) {
        return a - b;
    }
}

class SimpleCalculator implements Calculator {

    @Override
    public int add(int a, int b) {
        return a + b;
    }
}

ここでの読み方はこうだ。

add は従来どおり「実装必須」の抽象メソッド。

一方、default が付いた subtract は、 インターフェース側にあらかじめ実装が用意されている。

実装クラスが独自に書かなければ、 そのままこの処理が使われる。

つまり、インターフェースが 「振る舞いの雛形」を持っている、ということになる。

あれ?

実装していいの?

インターフェースに、処理が書いてある。

しかも、実装クラスでは add しか書いていない。 subtract は自動的に使えてしまう。

正直に言えば、違和感しかなかった。

インターフェースとは「契約」だ。

何をするかを定義する場所であって、 どうやるかを書く場所ではない。

少なくとも、私たちはそう教わってきた。

■ なぜ実装を書かなかったのか

理由は単純だ。

責務を分けるためである。

  • インターフェース = 振る舞いの定義(契約)
  • クラス = 具体的な実装

役割を分離することで、 設計は明確になり、 依存関係も整理される。

そして何より、 カプセル化が守られる。

■ カプセル化と保守性

実装の詳細はクラス内部に隠される。

外部から見えるのは、 メソッドのシグネチャだけ。

だから、保守性と利用者視点の可読性が守られていた。 実装が隠蔽されていることで、変更の影響範囲は実装クラス内部に閉じ込められる。 利用者は契約(インターフェース)だけを読めばよく、それが長年Javaの安定性を支えてきた。

ただし、これは「半分は真実」である。

確かに利用者にとっては読みやすい。 しかし、実装を探すためにクラスを辿らなければならない場面も多かった。

設計が崩れれば、 インターフェースはただの形式だけになってしまう。

■ それでも守られてきた原則

それでも、 「インターフェースに実装を書かない」 という原則は長く守られてきた。

それは単なる文法上の制約ではない。

Javaという言語が大規模開発で信頼されてきた、 ひとつの思想だった。




2026年2月21日土曜日

【Java進化史 番外編】Java8で「符号なし」が追加?本当か?

そういえば。

Java8で「符号なし」が導入された、と どこかで聞いたことがある。

……え?

Javaって、unsigned無かったよね?

なんで今頃?

ちょっと気になったので、調べてみた。


■ きっかけは、IPアドレス

昔、こんなコードを書いたことがある。


InetAddress addr = InetAddress.getLocalHost();
byte[] ip = addr.getAddress();

System.out.println(ip[0]);

出力:


-64

……は?

IPアドレスって、192とか168じゃなかったか?

なぜマイナスになる?


■ 192って何なのか

例として、


192.168.0.1

この「192」は、 IPアドレスの最初の1バイト(8ビット)である。

IPv4は32ビット。


8ビット × 4 = 32ビット

つまり、IPアドレスは4つのbyteでできている。

192を2進数にすると:


11000000

これが1バイトの正体だ。


■ なぜ -64 になるのか

Javaのbyteは符号付き。


-128 ~ 127

11000000を符号付きとして解釈すると、 それは -64 になる。

それだけの話である。


■ Java8ならどう書く?

IP取得方法は変わっていない。 InetAddressも昔のままだ。

変わったのは、読み方である。


InetAddress addr = InetAddress.getLocalHost();  
byte[] ip = addr.getAddress();                  

int first = Byte.toUnsignedInt(ip[0]);

System.out.println(first);

Java8で追加された Byte.toUnsignedInt() によって、 符号なしとして読むことができるようになった。


■ 4つ並べると


for (byte b : ip) {
    System.out.print(b + ".");
}

-64.-88.0.1.

Java8


for (byte b : ip) {
    int value = Byte.toUnsignedInt(b);
    System.out.print(value + ".");
}

192.168.0.1.

■ で、結局なにが起きたのか

Java8は、unsigned型を追加したわけではない。

unsignedとして「読む方法」を追加した。

byteの仕様は変わっていない。 intの仕様も変わっていない。

もちろん、boxingやラッパークラスの挙動もそのままである。

既存のコードを一切壊さない。

その範囲でできることを追加した。

値そのものは昔から同じ。

型は増やさず、 解釈の手段だけを加えた。


■ なぜ今頃?(少しだけ補足)

Javaは当初、型を増やさない設計を選んだ。

だからunsignedは入らなかった。

だが、 ネットワークやビット演算の現場では、 0~255として扱いたい場面が多かった。

そこでJava8で、 型は増やさず、解釈メソッドを追加した。

いかにもJavaらしい、 後方互換を守る現実解である。



【Java進化史 第21回 】Java8 〜 ::(メソッド参照)とは何か。どう読めばいいのか〜

若手のコードを見た。

list.forEach(System.out::println);

「:」なら知っている。

でも「::」は知らない。

Javaに、こんな記号あったか?

どこが引数で、どこが処理なのか。

まずは、一度戻る。



■ 昔のJavaに戻る

for (String s : list) {
    System.out.println(s);
}

これなら分かる。


■ 次にラムダに戻る

list.forEach(s -> System.out.println(s));
s -> System.out.println(s)

左側が「受け取る引数」。

右側が「実行する処理」。

s を受け取り、println(s) を実行する。


■ 何が省略されたのか

System.out.println(s)

受け取った s を、そのまま println に渡しているだけ。

だから省略できる。


■ ここで :: が出てくる

System.out::println

これは、

s -> System.out.println(s)

を短く書いただけ。

まだ実行していない。

メソッドそのものを渡している。


■ A::B の読み方

A::B

「A の B というメソッドを使う」

A.B() ではない。

呼び出しではなく、 メソッドそのものを渡している。


■ 他のパターンも同じ

① 静的メソッド

list.stream()
    .map(s -> Integer.parseInt(s));
list.stream()
    .map(Integer::parseInt);

Integer クラスの parseInt メソッドを使う、 という意味。


② 特定オブジェクトのメソッド

Supplier<String> supplier =
    () -> user.getName();
Supplier<String> supplier =
    user::getName;

user の getName メソッドを使う。


③ コンストラクタ参照

Supplier<List<String>> supplier =
    () -> new ArrayList<>();
Supplier<List<String>> supplier =
    ArrayList::new;

ArrayList のコンストラクタを使う、 という意味。

これも同じ。

「new を呼び出している」のではなく、

「このコンストラクタを使ってね」と渡している。


■ もう一度まとめる

昔のJava。

for (String s : list) {
    System.out.println(s);
}

ラムダ。

list.forEach(s -> System.out.println(s));

メソッド参照。

list.forEach(System.out::println);

意味は同じ。

段階的に省略されているだけ。


■ 何が変わったのか

昔のJavaは、 「全部書け」だった。

Java8からは、 「同じ意味なら短くしていい」に少し進んだ。

:: は、その象徴。

でも中身は昔と変わらない。



2026年2月19日木曜日

【Java進化史 第20回 】Java8 〜Optionalとは何か。どう読めばいいのか〜

昔は、こういうコードを何度も書いた。

 User user = findUser(id); if (user != null) { String name = user.getName(); if (name != null) { System.out.println(name); } } 

null を確認する。

また null を確認する。

チェックを忘れれば、 NullPointerException。

仕方なかった。

当時のメソッドは、こうだったからだ。

 User findUser(int id); 

見つからなければ、null を返す。

だから、呼び出す側で毎回確認していた。


■ Java8では、こう書ける

 Optional<User> findUser(int id); 

戻り値が変わった。

null を直接返さない。

値が無い場合は、 「空の Optional」を返す。

読むときは、こう読む。

「User が入っているかもしれない箱」


■ Optionalとは何か

まずは宣言。

 Optional<String> name; 

意味は、

「String が入るかもしれない箱」。

値がある場合。

 Optional<String> name = Optional.of("Taro"); 

中身あり。

値が無い場合。

 Optional<String> name = Optional.empty(); 

中身なし。

これは true / false ではない。

「空の箱」を作っているだけだ。

null かもしれない値を入れる場合。

 Optional<String> name = Optional.ofNullable(value); 

value が null なら empty。

値があれば、中身あり。


■ どう使うのか

中身があるか確認する。

 if (name.isPresent()) { System.out.println(name.get()); } 

isPresent() は boolean を返す。

get() は中身を取り出す。

※ 中身が無いと例外になる。

デフォルト値を使う場合。

 String result = name.orElse("default"); 

中身があればその値。

無ければ "default"。

中身があるときだけ処理する。

 name.ifPresent(n -> System.out.println(n)); 

昔の

 if (user != null) 

に近い。


■ NullPointerExceptionは減るのか

昔はこうだった。

 User user = findUser(id); System.out.println(user.getName()); // NPEの可能性 

Optional では、

 Optional<User> user = findUser(id); 

そのまま user.getName() は書けない。

必ず、

isPresent()、orElse()、ifPresent()

などを通す。

だから、 nullチェック忘れの事故は減る。

ただし、 get() を乱用すれば例外は出る。


■ どこで使うのか

基本は、戻り値。

 Optional<User> findUserById(int id); 

見つからない可能性があるメソッド。

フィールドや引数では、あまり使わない。


■ 何が良くなったのか

「nullを返す」から、

「値が無い可能性があると宣言する」へ変わった。

その結果、

nullチェック忘れの事故が減り、 コードの意図が少し読みやすくなった。




2026年2月16日月曜日

【Java進化史 第19回 】Java8 〜interfaceが壊れた日(@の正体)〜

若手のコードを見て、固まった。

interface に @ が付いている。

しかも、その中に実装らしきものがある。

例えば、こんなコードだ。


@FunctionalInterface
public interface Task {

    void execute();

    default void log() {
        System.out.println("log");
    }
}

@ が付いている。

しかも、実装まである。

え?
interface って、実装書けたっけ?

何が起きているのか、分からなくなった。


■ 混乱は、2つあった

落ち着いて見ると、
混乱は2つあった。

1つ目。
上に付いている「@」。

2つ目。
interface の中にある「実装」。

一気に理解しようとして、止まった。

だから今回は、
まず「@」のほうだけ整理する。


■ @FunctionalInterface が出てきたら

正直、「関数型」という言葉は難しい。

だから私は、こう読むことにした。

「この interface は、メソッドが1つだけです」

それだけだ。


@FunctionalInterface
public interface Converter<T, R> {
    R convert(T input);
}

見るべきなのは、ここ。

抽象メソッドが1つだけ。

もし2つに増やすと、
コンパイルエラーになる。

つまりこれは、

「1メソッドで使います」

という宣言だ。


■ なぜ、わざわざ宣言するのか

では、なぜそんな宣言が必要なのか。

理由はシンプルだ。

うっかり2つ目の抽象メソッドを 追加してしまうのを防ぐため。

もし増やせば、コンパイルエラーになる。

つまりこれは、 設計のブレを防ぐための安全装置だ。

特別な機能を追加するものではない。

設計の意図を、はっきりさせるための印だ。


■ interface の形は変わっていない

public interface。

メソッド定義。

書き方は昔と同じだ。

@ が付いただけ。

interface 自体が 別物になったわけではない。


■ もう1つの混乱

だが、まだ疑問は残る。

なぜ interface の中に、 実装があるのか。

それは、@ とは別の進化だ。




【Java進化史 第18回】Java8 〜「@」も読めなかった日

若手のコードを見たとき、
固まったことがある。

-> が読めない。
Streamも追えない。

そして、やたらと出てくる「@」。

なんじゃこれ?


■ @ は何者か

調べてみると、
これは「アノテーション」という仕組みだった。

コードに“意味”を追加するための印。
処理そのものを書くものではない。


@Override
public String toString() {
    return name;
}

「ちゃんとオーバーライドしていますよ」
という宣言だ。

もしメソッド名を間違えれば、
「それ、オーバーライドになっていませんよ」と
コンパイル時に教えてくれる。

人間のミスを減らすための仕組みだ。


■ いつからあったのか

アノテーションは、Java5から存在している。

つまり、
私が気づかなかっただけだ。

だがJava8で、
その世界は少し広がった。


■ Javaで使える主なアノテーション一覧(〜Java8)

アノテーション 用途 利用可能バージョン
@Override 正しくオーバーライドしていることを示す Java5〜
(※Java6からインターフェースにも適用可)
@Deprecated 非推奨であることを示す Java5〜
@SuppressWarnings コンパイラ警告を抑制する Java5〜
@SafeVarargs 可変長引数の安全性を保証する Java7〜
@FunctionalInterface 関数型インターフェースであることを示す Java8〜
型アノテーション
(@Target(TYPE_USE))
型そのものにアノテーションを付けられる Java8〜

こうして並べてみると、
Java8で増えたものがある。

@FunctionalInterface と、
型アノテーションだ。


■ 型にも付くようになった

Java8から、
アノテーションは「型」にも付けられるようになった。


List<@NotNull String> names;

これまでは


@NotNull List<String> names;

こうだった。

前者は「namesがnullではない」。
後者は「リストの中のStringがnullではない」。

守れる範囲が、少し細かくなった。


■ 読めなかった理由

若手は、これを普通に読む。

私は止まった。

-> と同じくらい、
@ も読めなかった。

だが分かってしまえば、
やっていることはシンプルだ。

コードに意味を足しているだけ。

Java8はラムダだけではない。
@ の世界も、静かに広がっていた。


■ 次回へ

Java8で追加された @FunctionalInterface。
これはラムダと深く関係している。

Javaはどこまで「関数型」に寄ったのか。
次回、そこを見ていく。





2026年2月15日日曜日

【Java進化史 第17回】Java8 〜データが壊れた日。forEachが危ない理由〜

昔、データが壊れたことがある。

テストでは動いていた。
だが本番で崩れた。

件数が合わない。
値が欠ける。
ときどき例外も出る。

原因は、スレッドセーフではないクラスだった。


■ スレッドセーフとは

複数のスレッドから同時に触っても、 データが壊れないこと。

逆に言えば、 同時に触ると壊れるクラスもある。

そして怖いのは、

テストでは再現しないことがある。

タイミング次第だからだ。


■ 並列処理は、昔は怖かった

Java7まで、 並列処理を書くには自分で守る必要があった。

synchronizedを付ける。
Concurrent系を使う。
共有変数に気を付ける。

だから私は、 並列処理が少し怖かった。


■ Streamでも同じことが起きる

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

list.parallelStream()
    .map(Employee::getName)
    .forEach(result::add);   // これ

parallelStreamにすると、 複数スレッドが同時にaddする可能性がある。

ArrayListはスレッドセーフではない。

つまり、 また壊れる可能性がある。


■ ああ、だからforEachは危ないのか

forEachは、 外の変数を触る。

外を触るということは、 共有するということだ。

共有は、並列では危ない。


■ collectという書き方

List<String> result =
    list.parallelStream()
        .map(Employee::getName)
        .collect(Collectors.toList());

collectは、 外の変数を使わない。

だからparallelでも、 安心して書ける。

仕組みの詳しい話までは知らなくていい。

並列にするなら、collectを書く。


■ Stream編、ここまで

最初は、 若手の1行が読めなかった。

そこから、

  • 左から読む
  • 何をやるかを書く
  • forEachは危ないことがある

ここまで来た。

もう、Streamは怖くない。

Java8は、 少しだけ味方になった。




【Java進化史 第16回】Java8 〜Streamは「どうやるか」ではなく「何をやるか」〜

前回は、Streamを「左から読む」練習をしました。

filter → map → forEach。

左から順番に読めばいい。

それだけで、若手の1行が読めるようになる。



でも、まだモヤモヤが残る

なぜ、こう書くのか。

Java6までの自分には、 for文のほうが自然でした。

for (Employee e : list) {
    if (e.isActive()) {
        System.out.println(e.getName());
    }
}

順番も、処理も、全部自分で書いている。

安心感があります。


Streamは「何をやるか」だけを書く

list.stream()
    .filter(e -> e.isActive())
    .map(e -> e.getName())
    .forEach(System.out::println);

このコードをよく見ると、 回し方は書いていません。

  • どこから回すか
  • どうやって回すか
  • 順番をどう制御するか

何も書いていない。

書いているのは、

  • 条件で絞る
  • 名前に変換する
  • 出力する

「何をするか」だけです。


動きは1件ずつ“かもしれない”

Streamは内部で、

要素1 → filter → map → forEach
要素2 → filter → map → forEach
...

のように流れていきます。

でも、それを意識しなくていい。

1件ずつ処理するかもしれないし、 並列になるかもしれない。

自分は「どうやるか」を考えなくていい。

これが、Streamの価値です。


オブジェクト指向の延長だった

Javaは昔から、 実装を隠してきました。

Collectionもそう。

インターフェースで定義し、 中身は隠す。

Streamも同じです。

処理のやり方は任せる。

自分は、やりたいことを書く。

そう考えると、 少しだけ腹に落ちました。


次回予告

次回は、collectとforEachの違い。

「結果を返す」のはどちらなのか。

ここが分かると、Streamは完成します。




【Java進化史 第15回】Java8 〜Streamを読む練習。「左から読む」だけ〜

■ この記事で分かること

  • Streamを読んで「何をしているか」説明できるようになる
  • Java6以前でどう書くかが分かる
  • Streamの内部の動きと価値が分かる


■ まずは読む練習

Streamが怖いのは、何をしているのか分からないからだ。

今日は、たった1つのコードを読む。


names.stream()
     .filter(n -> n.startsWith("S"))
     .map(String::toUpperCase)
     .sorted()
     .forEach(System.out::println);

さて、これは何をしているだろう。

読むポイントはひとつ。

左から順番に読むだけ。


■ 何をしている?

左から順に、日本語にしていく。

  1. names を stream にする
  2. Sで始まるものだけ残す
  3. 大文字に変換する
  4. 並べ替える
  5. 表示する

つまり、

「Sで始まる名前を大文字にして、並べ替えて表示している」

まずは、これが言えればいい。


■ Java6以前ならどう書くか

同じことを、Java6までの書き方で書いてみる。


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

for (String name : names) {
    if (name.startsWith("S")) {
        result.add(name.toUpperCase());
    }
}

Collections.sort(result);

for (String name : result) {
    System.out.println(name);
}

やっていることは同じだ。

  • 回す
  • 条件を確認する
  • 変換する
  • 並べ替える
  • 出力する

Streamは、これを一本の流れで書いているだけだ。

まずはここまで理解できれば十分だ。


■ では内部ではどう動くのか

Java6までなら、基本はこうだ。


for (...) {
    // 1件ずつ順番に処理
}

もし並列化したければ、

  • スレッドを作る
  • 処理を分割する
  • 同期を取る
  • 競合を防ぐ

あの、バグを生みやすい並列処理を、自分で書く必要があった。


Streamは違う。


names.parallelStream()
     .filter(...)
     .map(...)
     .forEach(...);

これだけで、内部で自動的に分割され、 並列処理される。

開発者は並列化のコードを書かなくていい。

回す責任だけでなく、 並列化の責任もライブラリに渡した。


通常の stream() の場合は順番に処理される。

だが重要なのは、

Streamは1件ずつかもしれないし、並列かもしれない。 それを意識しなくていい設計が価値だ。

「どう回すか」ではなく、 「何をするか」だけを書く。

それがJava8の思想である。


■ まとめ

  • まずは左から順番に読む
  • 日本語にできれば理解できている
  • Java6で書き直せるなら、もう怖くない
  • 並列化を自分で書かなくていいのが本質

■ 次回予告

では、実際にfor文からStreamへ書き換えてみる。

次回、「Streamを書いてみる」。



【Java進化史 第14回】:Java8 〜若手のこの1行が読めない。Streamの衝撃〜(理屈編)

■ この記事で分かること

  • なぜStreamが読みにくく感じるのか
  • なぜfor文が見えなくなったのか
  • Streamが自動でやってくれていること
  • 「どうやるか」から「何をするか」への変化

■ 若手のこの1行で、私は止まった


names.stream()
     .filter(n -> n.startsWith("S"))
     .map(String::toUpperCase)
     .sorted()
     .forEach(System.out::println);

どうも、Sで始まる名前を集めて、
大文字にして、並べ替えているらしい。

……らしい。

しかし、どこで回しているのかが見えない。

なんじゃこりゃ。


■ 私が知っているJava(Java7まで)


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

for (String name : names) {
    if (name.startsWith("S")) {
        result.add(name.toUpperCase());
    }
}

Collections.sort(result);

for (String name : result) {
    System.out.println(name);
}

こちらは安心する。
回して、条件を見て、入れて、並べ替えて、出力する。
全部、自分でやっている。


■ Streamの見た目の流れ


names
  ↓
stream()
  ↓
filter
  ↓
map
  ↓
sorted
  ↓
forEach

左から順番に実行されているように見える。


■ しかし実際はこう動く


① パイプライン(処理の設計図)を作る
   ├─ filter
   ├─ map
   └─ sorted

② 終端操作(forEach)が呼ばれた瞬間

③ まとめて一気に実行

filterやmapは、書いた瞬間には動いていない。

動くのは、最後のforEachが呼ばれたときだ。


■ Streamはforの書き換えなのか?

Streamは、forを書かなくていい仕組みだ。

でも、回っていないわけではない。
見えないところで、ちゃんと回っている。

forが消えたのではない。
自分で書かなくてよくなっただけだ。


■ さらに違うところ


names.parallelStream()

これだけで並列化できる。

Java7でも並列処理は書けた。
しかし、スレッド管理や同期を自分で制御する必要があった。

Streamは、そこまで抽象化している。


■ 私の誤解

私は最初、「短く書ける便利機能」だと思った。

しかし違った。

これは、ループを書くという作業を、
Javaに預ける仕組みだった。

楽をしているのではない。
抽象度を一段、上げただけだった。


■ まとめ

  • Streamはforを消していない
  • 回す仕組みを内部に隠した
  • 並列処理を簡単に扱えるようにした
  • 「やり方」よりも「意図」を書くようになった

■ 次回予告

Streamは、forの書き換えに見える。
私も最初はそう思った。

回すことを、少し楽にしただけの機能だと。

だが、どうもそれだけではない。

なぜなら、filterやmapは、
書いた瞬間にはまだ動いていないからだ。

では、いつ動いているのか。

次回、この1行を分解してみる。

2026年2月14日土曜日

【Java進化史 第13回】:エンジニアとしての矜持が許さない 〜ラムダはいつ使う?〜

■ それでも、まだやれるはずだ

最近、コードを書く機会が減った。

若手が書く。

最近はAIも書く。

設計はする。レビューもする。

だが、自分の手でラムダを書くことは、確実に減っている。

読めるようにはなった。

→ を見ても、もう止まらない。

だが、それで満足していいのか。

エンジニアとしての矜持が、それを許さない。

読めるだけでは足りない。

自分の手で書ける。

迷わず書ける。

若手と同じスピードで、自然に書ける。

俺はまだ、やれるはずだ。


■ なんでもラムダにできるわけではない

ここで一度、冷静になろう。

ラムダは魔法ではない。

「関数型インターフェース」の実装として使えるだけだ。

つまり、

  • 抽象メソッドが1つだけ
  • 処理をその場で渡したいとき

こういうときに使う。

■ 典型例


list.forEach(e -> System.out.println(e));

これは Consumer<T> という関数型インターフェースを ラムダで実装している。


■ では、こういう場合は?


public void execute() {
    int count = 0;
    list.forEach(e -> {
        count++;  // コンパイルエラー
    });
}

ローカル変数 count を変更しようとするとエラーになる。

なぜか?

ラムダ内では、ローカル変数は実質 final でなければならない。

つまり、

「外の状態を自由に変更する道具ではない」

ここが重要だ。


■ ここでラムダだ!と言える瞬間

ラムダが最もかっこいい瞬間は、

「処理を渡す」ときだ。


Collections.sort(list, (a, b) -> a.compareTo(b));

昔なら、こうだった。


Collections.sort(list, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.compareTo(b);
    }
});

長い。

本質は compare の1行だけなのに。

その「本質だけ」を書ける。

これがラムダの美しさだ。


■ 今日の練習問題(1問)

問題:

次の匿名クラスを、ラムダ式に書き換えよ。


Runnable r = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello Lambda");
    }
};

■ 解答


Runnable r = () -> System.out.println("Hello Lambda");

Runnable は抽象メソッドが1つ(run)だけ。

だからラムダにできる。

これが判断基準だ。


■ まとめ

  • ラムダは「関数型インターフェース」の実装
  • ローカル変数は実質 final
  • 処理を渡すときに最も美しい

読めるようになった。

次は、書けるようになる。

エンジニアとしての矜持を、取り戻す。


■ 次回予告

ラムダは、ここで一区切り。

読めるようになった。 書けるようにもなった。

だが――

若手のコードには、まだ壁がある。


list.stream()
    .filter(e -> e.startsWith("A"))
    .map(String::toUpperCase)
    .collect(Collectors.toList());

……長い。

何が返っているのか、一瞬止まる。

次は、ここだ。

Streamを、分解する。




【Java進化史 第12回】Java8 〜ラムダ式を分解して読む〜(読み方編)

■ この記事で分かること

  • ラムダ式をどう頭の中で変換すれば読めるか
  • → の意味をシンプルに理解する方法
  • 若手のコードを怖がらずに読むコツ

■ 50代後半になると、脳は少し保守的になる

正直に言う。

50も後半になると、 新しい文法を見るだけで、少し身構える。

また覚え直すのか?

若い頃のように、 何でも吸収できる感覚は、もうない。

そして、→ を見る。


(x, y) -> x + y

……やめてくれ、と思った。

だが、あることに気づいた。

これは新しい知識ではない。

昔の知識に変換すればいいだけだ。


■ → はたった2つの意味しかない

難しく見えるが、本質は単純だ。

  • 左側: メソッドの引数
  • 右側: メソッドの処理本体

それだけだ。

→ を見たら、心の中でこう唱える。

「左が引数。右がメソッド本体。」


■ 匿名クラスに戻して読む

たとえば次のコード。


list.forEach(x -> System.out.println(x));

これは何をしているか?

落ち着いて、Java7に戻してみる。


list.forEach(new Consumer<String>() {
    @Override
    public void accept(String x) {
        System.out.println(x);
    }
});

つまり、

  • x は accept メソッドの引数
  • 右側は accept の中身

匿名クラスを圧縮しているだけ。

あなたはこの書き方を、何年もしてきた。

知らないのではない。

形が変わっただけだ。


■ 型は消えていない


Calculator add = (x, y) -> x + y;

なぜ型を書かなくていいのか?

答えは単純だ。

左側の型が、メソッドの形を決めているから。

Javaは動的言語になっていない。

型は消えていない。

見えなくなっただけだ。


■ 練習問題①

次のラムダ式を、Java7風に書き直してみよう。


Comparator<String> comp = (a, b) -> a.length() - b.length();

答え:


Comparator<String> comp = new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
};

■ 練習問題②

次のコードは何をしているか、匿名クラスに戻してみよう。


Runnable r = () -> System.out.println("Hello");

答え:


Runnable r = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello");
    }
};

→ の左が空なのは、引数がないからだ。


■ まとめ

  • → は「左が引数」「右が処理」
  • 匿名クラスに戻せば読める
  • 新しいことを覚える必要はない

新しい文法に怯える必要はない。

あなたはすでに、知っている。

ラムダ式は、 長年書いてきたJavaの圧縮形にすぎない。

展開すれば、見慣れた景色になる。


■ 次回予告

【Java進化史 第13回】
〜ラムダ式はどう書くのか〜(実践編)

読む側から、書く側へ。




【Java進化史 第11回】Java8 〜なんだこの→は?ラムダ式の必要性〜(理論編)

■ この記事で分かること

  • 若手のラムダ式がなぜ読めなかったのか
  • ラムダ式で「何が消えて」「何が残ったのか」
  • なぜInterfaceは残るのか
  • それでもJavaらしさが失われていない理由

■ 若手のコードで、私は固まった

コードレビューで見た一行。


Calculator add = (x, y) -> x + y;
System.out.println(add.calc(3, 5));

……?

矢印?

これがJava?

私の知っているJavaは、 もっと重厚で、もっと“クラス中心”だった。


■ 私が知っているJava(Java7まで)

同じ処理を、Java7で書くとこうなる。


public class Main {

    interface Calculator {
        int calc(int x, int y);
    }

    public static void main(String[] args) {

        Calculator add = new Calculator() {
            @Override
            public int calc(int x, int y) {
                return x + y;
            }
        };

        System.out.println(add.calc(3, 5));
    }
}

やりたいことは単純だ。


x + y

しかし実際には、

  • Interfaceを書く
  • 匿名クラスを書く
  • メソッドを実装する

「計算」よりも、「入れ物」の方が目立っている。


■ Java8ではどうなったか


public class Main {

    interface Calculator {
        int calc(int x, int y);
    }

    public static void main(String[] args) {

        Calculator add = (x, y) -> x + y;

        System.out.println(add.calc(3, 5));
    }
}

出力は同じ。


8

消えたのは、これだ。


new Calculator() {
    @Override
    public int calc(int x, int y) {
        return x + y;
    }
}

■ 何が消えて、何が残ったのか

消えたもの:

  • 匿名クラス
  • ボイラープレートコード

残ったもの:

  • Interface

ここが重要だ。

ラムダ式は、Interfaceを消していない。

Javaはあくまで静的型付け言語。 型がなければ成立しない。


Calculator add = (x, y) -> x + y;

この左側の Calculator があるから、 ラムダは意味を持つ。


■ Interfaceを残すのが、Javaらしい

Java8は変わった。

しかし、捨てなかった。

Interfaceという「契約」は残した。

これはオブジェクト指向の王道だ。

ラムダは“関数”ではない。

Interfaceの実装を、簡潔に書ける仕組みだ。

だからJavaは、 動的言語になったわけではない。

あくまで、 型と契約の上に立った進化だった。


■ まとめ

  • ラムダ式はInterfaceを消さない
  • 消えたのは匿名クラスの冗長さ
  • Javaらしさは残っている

→ は革命ではない。

しかし、 Javaを書く感覚を確実に軽くした。


■ 次回予告

【Java進化史 第12回】
〜ラムダ式を分解して読む〜

  • なぜ型を書かなくていい?
  • どこで型推論している?
  • 「関数型インターフェース」とは何か?

→ の裏側を、技術的に解体する。




2026年2月11日水曜日

【Java進化史 第10回】Java8 〜あのPermGenはどこへ消えた〜

昔、意味も分からず書いていたオプションがある。


-XX:MaxPermSize=256m

足りなければ増やす。
512m。
1024m。

それでも、死ぬ。


java.lang.OutOfMemoryError: PermGen space

あの文字列を、何度見ただろう。

■ PermGenとは何だったのか

Java7まで、JVMのメモリは大きくこう分かれていた。


Heap
 ├─ Young領域
 ├─ Old領域
 └─ PermGen(Permanent Generation)

PermGenはヒープの一部で、主に

  • クラスのメタ情報
  • メソッド情報
  • 静的変数
  • (Java6までは)文字列定数プール

を保持していた。

そして最大の問題は、 サイズが固定だったことだ。

ヒープのように自動で伸びない。
だから溢れる。

■ Stringを多用するな、と言われた時代

あの頃、よく言われた。

「Stringを多用するな」

理由は曖昧だった。
だが確かにサーバはよく死んだ。

これは半分本当だ。

Java6までは、文字列定数プールがPermGenにあった。

大量の文字列リテラル、internの多用、
フレームワークが内部で生成する文字列。

それらがPermGenを圧迫することがあった。

だから、

「Stringがヤバい」

という空気は、間違いではなかった。

■ でも本質はStringそのものではない

問題は設計だった。

固定サイズのPermGenに、 クラス情報と文字列プールを押し込んでいたこと。

さらに、Stringは不変(immutable)。


String s = "";
for (int i = 0; i < 10000; i++) {
    s += i;
}

このコードは、大量の中間Stringを生む。

だから我々は学んだ。

「StringBuilderを使え」

間違ってはいない。
だが根本原因はJVM側の設計にもあった。

■ GCパラメータ職人の時代

最後は、GCと戦っていた。

レスポンスが急に悪くなる。
CPUが跳ね上がる。
サーバが固まる。

Full GC。

Stop The World。

ログを吐かせる。


-XX:+PrintGCDetails
-XX:+UseConcMarkSweepGC
-XX:NewRatio=2
-XX:SurvivorRatio=8

意味を完全に理解していたか?

正直、怪しい。

だが、体で覚えていた。

パラメータを調整し、 再起動し、 負荷試験をし、 延命する。

今思えば、 あれは完全に職人技だった。

■ Java7で起きた静かな改善

Java7で文字列プールはヒープへ移動した。

PermGenに直接効くString問題は緩和された。

だが、PermGenそのものはまだ存在していた。

■ Java8でPermGenは消えた

Java8でPermGenは廃止された。

代わりに導入されたのが Metaspace

  • ヒープ外(ネイティブメモリ)を使用
  • 原則、動的に拡張

固定枠で突然死ぬ設計ではなくなった。

さらにGCも成熟した。

「あれ?最近安定してない?」

それは気のせいではない。

  • PermGen廃止
  • Metaspaceの動的拡張
  • GCアルゴリズムの成熟

足元が、ちゃんと進化していた。

■ あの頃の自分へ

-XX:MaxPermSizeを書き、
Stringを疑い、
GCログを読み、
サーバの負荷に怯えていた自分へ。

PermGenは、もういない。

Java8は文法革命だけではない。

JVMの土台も、ちゃんと進化していた。

少しだけ、戦わなくてよくなった。

ほんと、助かるよ。





【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進化史 第4回】Java7 - switch文と設計思想 ― 分岐の向こう側にあるもの

そういえば、最近あまりswitch文を書いていない。

書けなくなったわけではない。 むしろ、書こうと思えばすぐ書ける。

だが設計を考え始めると、 どこかで立ち止まる自分がいる。


■ 手続き的思考の安心感

switchは安心だ。

条件を並べれば、 処理の流れは一目で追える。

どこで何が起きるのか、 すべてが自分の視界の中にある。

長く手続き的なコードを書いてきた身には、 この感覚は心地よい。

分岐を書く。 処理を書く。 上から順に読む。

世界が、整然と並んでいる。


■ ふと浮かぶ違和感

だがあるとき、ふと思った。

オブジェクト指向言語で、 switchをあまり見ないのはなぜだろう。

書けるのに、 あまり使われない。

それは構文の問題ではない。 設計思想の違いだ。


■ 責務という考え方

オブジェクト指向では、 振る舞いはクラスに持たせる。

条件で分けるのではなく、 責務で分ける。

「どの種類か」で分岐するのではなく、 「誰が責任を持つのか」を考える。

その結果、 分岐はオブジェクトの内部に吸収される。

外側から見えるのは、 ただの振る舞いだけになる。


■ 変更点の局所化

switchを書き始めると、 同じ条件分岐が別の場所にも現れることがある。

仕様変更が入るたびに、 その条件を探して回る。

変更点が、散らばる。

一方で、責務を適切に分けた設計では、 変更は一か所に閉じ込められる。

変更点の局所化。

それは地味だが、 長く保守する現場では大きな意味を持つ。


■ 拡張に開き、修正に閉じる

新しい種類が増えたとき、 switchはcaseを増やす。

既存のコードを修正する。

だが理想とされる設計は、 拡張に開き、修正に閉じる。

既存部分に手を入れず、 新しい要素を追加することで対応する。

それがOCP(Open/Closed Principle)と呼ばれる考え方だ。

理屈は理解している。

だが、実践は簡単ではない。


■ 設計の重さ

責務を分ける。

将来の変更を想像する。

インターフェースを定義する。

switch一つで済む場面でも、 設計は少し重くなる。

正直に言えば、 急いでいるときほど、 switchの安心感に戻りたくなる。

条件を書けば、すぐに動く。

目の前の問題は、すぐ解決する。


■ 未来の自分への負債

だが安易な分岐は、 未来の自分への負債になることがある。

仕様が増え、 条件が増え、 分岐が増え、 修正箇所が増える。

そのとき向き合うのは、 過去の自分の判断だ。

設計とは、 未来の自分との対話なのかもしれない。


■ 分岐の向こう側

switchを書かなくなったのは、 構文の問題ではない。

分岐よりも、 責務を考える時間が増えただけだ。

それが成長なのか、 ただ慎重になっただけなのかはわからない。

だが少なくとも、 設計という重みを意識するようにはなった。

分岐の向こう側にあるもの。

それを考えることが、 オブジェクト指向という思想なのだと思う。





【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実行環境】ブラウザだけでJavaは動くのか?paiza.ioを試してみた

前回、MacにJDK21をインストールした。

Homebrewで入れて、HelloWorldまで動かした。

「よし、これで本格的にやるぞ」

…と思ったが、ふと考えた。

今って、ブラウザだけでJava動かせないのか?


■ 若い頃は、こんなサービスなかった

私が若い頃。

Javaを動かすには、必ず環境構築が必要だった。

  • JDKをダウンロード
  • パスを通す
  • コンパイルする

環境が壊れたら、半日つぶれる。

それが普通だった。

でも今は違う。

ブラウザを開くだけで、Javaが書ける。

いい時代になった。


■ paiza.ioを使ってみる(無料)

今回は、以前少し触ったことのある paiza.io を使ってみる。

https://paiza.io/ja/projects/new

無料で使えるオンライン実行環境だ。

特別なインストールは不要。

ブラウザでアクセスするだけ。


■ 使い方(超かんたん)

上のURLにアクセスすると、画面左上に緑色のボタンがある。

そこで言語を Java に変更する。

すると、次のようなテンプレートが表示される。


import java.util.*;

public class Main {
    public static void main(String[] args) throws Exception {
        // Your code here!
        
        System.out.println("XXXXXXXX");
    }
}

この


System.out.println("XXXXXXXX");

を、次のように書き換える。


System.out.println(System.getProperty("java.version"));

そして、左中ほどにある緑色の「実行」ボタンを押す。

すると、下の実行結果エリアに、Javaのバージョンが表示される。

(今日時点では)


18.0.2

と表示された。

ローカルに何も入れていなくても、Javaが動く。

正直、ちょっと感動する。


■ 昔のJavaでも動くのか?

ちなみに、


System.getProperty("java.version")

は、かなり昔のJavaから使えるメソッドだ。

Java6でも問題なく動く。

こういう「昔からあるAPI」を使うと、世代を超えて検証できるのが面白い。


■ 実際に使ってみた感想

いいところ:

  • すぐ試せる
  • 環境構築が不要
  • 無料で使える
  • ちょっとした検証には最適

気になるところ:

  • 実行がやや遅い
  • ブラウザなので入力スピードが出ない
  • コード補完などの支援は弱い

本気で開発するには少し厳しい。

でも、

「ちょっと試したい」
「若手のコードの一部を検証したい」

そんな用途には、十分すぎる。


■ ローカル環境との違い

前回、MacにJDK21をインストールした。

ローカル環境では:

  • 高速に実行できる
  • 複数バージョンを切り替えられる
  • 本格的な開発ができる

一方、ブラウザ環境は:

  • とにかく手軽
  • 準備ゼロ
  • すぐ試せる

用途が違う。

どちらが正しい、ではない。


■ まとめ

Javaは、昔よりもずっと始めやすくなっている。

環境構築で挫折する時代ではない。

でもやはり、本格的にやるならローカル環境は必要だ。




【Java環境構築】MacにHomebrewでJava21をインストールしてHello Worldまで動かす

若手のコードが読めなかったあの日から、再学習を始めた。

まずは環境を整えるところからやる。


■ 前提環境

  • Mac OS 15.6.1
  • Homebrew 5.0.13
  • Java未インストール状態から開始

Homebrewのバージョン確認:

% brew --version
Homebrew 5.0.13

今回はJava21を使用する。

最新を追いかけるのではなく、LTSであり、現場利用も多い安定バージョンから再出発する。


■ Javaが入っていないことを確認

javac --version

未インストールの場合:


The operation couldn’t be completed. Unable to locate a Java Runtime.
Please visit http://www.java.com for information on installing Java.

よし。まっさらだ。


■ HomebrewでJava21をインストール

brew install --cask temurin@21

インストール後の確認:

javac --version

出力:


javac 21.0.10

21が入った。

数字を見ると、少し安心する。


■ JAVA_HOMEを設定する

~/.zshrc に追加する。


export JAVA_HOME=$(/usr/libexec/java_home -v 21)

反映:

source ~/.zshrc

■ Hello Worldで動作確認

Hello.java を作成:


public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, Java21!");
    }
}

コンパイル:

javac Hello.java

実行:

java Hello

出力:


Hello, Java21!

ようやく戻ってきた感じがした。


■ 実は一度つまずいた

javac Hello

エラー:


エラー: クラス名'Hello'が受け入れられるのは、注釈処理が明示的にリクエストされた場合のみです
エラー1個

.java を付け忘れていただけだった。

こういうところで止まるのが、今の自分だ。


■ インストール済みJDK確認

/usr/libexec/java_home -V

複数バージョンを入れるときに使う。


■ まとめ

Java21(21.0.10)の導入と動作確認が完了。

小さな一歩だが、確実に前進している。





2026年2月7日土曜日

若手に聞かれて固まった。「JavaのLTS」って何だ?JDKの違いも整理してみた

若手が言っていた「JavaのLTS」って何?そしてJDKの違いも整理してみた

ある日、若手との会話でこんな言葉が出てきた。

「今は21がLTSですよね?」

……LTS?

聞いたことはある。でも説明できない。

正直、頭に浮かんだのはこれだった。

「LTEなら昔聞いたことがあるけど、そのこと?」

スマホの通信規格じゃない。それは違う。

でも、そのくらい曖昧だった。


① Javaのバージョンはどう変わったのか

私の感覚はこうだった。

Java5 → Java6 → Java7

数年おきに大きく変わる。

そんなペースだったはず。

しかしJava9以降、リリース方式が変わった。

半年ごとに新バージョンが出る。

9、10、11、12……

数字はどんどん進む。

気づけば25。

「そんなに出ているのか?」

正直、追えていなかった。


② LTSとは何か

LTSとは Long Term Support の略。

長期サポート版、という意味らしい。

半年ごとに出る通常版とは違い、
長期間サポートされる安定版 がLTSだ。

企業で採用されやすいのは、このLTS。

最近のLTSは次の通り。

  • Java8
  • Java11
  • Java17
  • Java21

なるほど。


だから若手は普通に「LTS」という言葉を使っていたのか。


③ OpenJDKとOracle JDKの違いは?

さらに混乱したのが、JDKの種類だ。

  • Oracle JDK
  • OpenJDK
  • Eclipse Temurin
  • Amazon Corretto

何が違うのか。

■ 機能は同じなのか?

同じバージョンであれば、基本的な機能は同じ。

Java21なら、言語仕様や標準APIは基本的に共通。

コードの動作そのものに大きな差はない。

■ 違いは何か?

主な違いは次の3つ。

  • ライセンス
  • サポート体制
  • 提供元

OpenJDKはオープンソースで基本無料。

Oracle JDKはOracleが提供するJDK。
現在は多くの場合無料で利用できるが、
企業向けには有料サポート契約がある。

つまり、機能差というより


サポートと契約の違いが本質らしい。


④ 企業ではどちらが使われているのか

最近の企業はどちらを使っているのか?

答えは――

混在している。

一時期、Oracle JDKのライセンス変更が話題になった。

それをきっかけに、OpenJDK系へ移行する企業が増えた。

クラウド環境では、

  • Amazon Corretto
  • Eclipse Temurin

などを採用する例も多い。

一方で、既存システムではOracle JDKが使われ続けていることもある。

現場は統一されているわけではなく、


移行中・混在状態というのが実情らしい。


⑤ なぜ今回はJava21でいくのか

最初は最新バージョンで始めようかとも思った。

しかし、やり直しの目的を考えた。

  • 現場で通用すること
  • 長く使われるバージョンであること
  • 情報が安定していること

それを考えると、


Java21(LTS)がちょうどいい。

いきなり最先端を追いかけるのではなく、
まずは足場を固める。

今回はJava21 + Oracle JDKで環境を作る。


まとめ

LTS。

聞いたことはあった。

でも、説明できなかった。

JDKの違いも、なんとなくで流していた。

知らないのが怖いのではない。

曖昧なままにしていることが怖い。

次回は、MacにJava21(Oracle JDK)をインストールする。

まずは動かす。




若手のコードが読めなかった日

若手のコードが読めなかった日

ある日、若手が書いたJavaコードをレビューすることになった。

画面を開いた瞬間、違和感があった。

見慣れない書き方。

  • ラムダ式
  • Stream
  • 知らないアノテーション

class には、見覚えのない識別子まで付いている。

そして、あるメソッドで完全に止まった。

「このメソッド何やってるの?全く分からん。」

本気でそう思った。


Javaは長年やってきた。
業務でも使ってきた。

分かっているつもりだった。

でも、それはJava6までの世界だった。

Java7以降の進化を、私はきちんと追っていなかった。


若手は普通に読んでいる。
普通にレビューしている。

自分だけが取り残されている感覚。

「これ、本当にJavaか?」

一瞬、別の言語に見えた。


焦った。

エンジニアとして終わったのではないか。
今さら基礎からやり直すのか。

50代で。


その後、本屋やネットで最近のJava本を探した。

でも多くはJava17や最新前提。

昔の知識とうまくつながらない。

「何が、いつ、どう変わったのか」

それが整理されていない。

断片的な情報ばかり増えて、頭の中は混乱した。


だったら、自分で整理しようと思った。

Java7から順番に。

  • 何が変わったのか
  • なぜ変わったのか
  • どこでつまずくのか

そして、若手のコードを普通に読める自分になる。


これは、焦りから始めた再学習の記録です。

同じように、

「分かっているつもりだった」と思った人がいたら、

一緒にやり直しませんか。





Java7、Java8、Java17・・・・と進化したJavaを、基礎から整理し直していく。


【Java進化史 第26回】Java9でListは何がどう変わったのか 〜 List.ofってなんじゃこりゃ? 〜

若手のコードをレビューしていて、手が止まった。 List<String> names = List.of("Tanaka", "Sato", "Suzuki"); ……なんじゃこりゃ? ...