2026年2月15日日曜日

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

【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は「どうやるか」ではなく「何をやるか」〜

【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を読む練習。「左から読む」だけ〜

【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の衝撃〜(理屈編)

【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回】:エンジニアとしての矜持が許さない 〜ラムダはいつ使う?〜

第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 〜ラムダ式を分解して読む〜(読み方編)

【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 〜なんだこの→は?ラムダ式の必要性〜(理論編)

【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回】
〜ラムダ式を分解して読む〜

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

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




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

【Java進化史 第17回】Java8 〜データが壊れた日。forEachが危ない理由〜 昔、データが壊れたことがある。 テストでは動いていた。 だが本番で崩れた。 件数が合わない。 値が欠ける。 ときどき例外も出る。 原因は、スレッドセーフではない...