2026年2月19日木曜日

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

【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が壊れた日(@の正体)〜

【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 〜「@」も読めなかった日

【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が危ない理由〜

【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行を分解してみる。

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

【Java進化史 第20回 】Java8 〜Optionalとは何か。どう読めばいいのか〜 昔は、こういうコードを何度も書いた。 User user = findUser(id); if (user != null) { String name = user.getNa...