Java | Java 詳細・モダン文法:Stream API 深掘り – imperative vs declarative

Java Java
スポンサーリンク

「命令的」と「宣言的」をざっくり一言でいうと

命令的(imperative)は、「どうやってやるか」を一歩一歩、自分で指示していく書き方です。
宣言的(declarative)は、「何をしたいか」だけを宣言して、細かい手順はライブラリやランタイムに任せる書き方です。

同じ「偶数だけ取り出して合計する」でも、for 文で書くと命令的、Stream で書くと宣言的になります。
Stream API を理解するうえで、この二つの違いを体で覚えることがすごく大事です。


命令的スタイル:for 文で「手順」を全部書く世界

例:偶数だけ取り出して合計する(命令的)

まずはおなじみの for 文から。

import java.util.List;

public class ImperativeExample {
    public static void main(String[] args) {
        List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);

        int sum = 0;
        for (int n : nums) {
            if (n % 2 == 0) {
                sum += n;
            }
        }

        System.out.println(sum); // 12
    }
}
Java

ここでやっていることを言葉にすると、こうです。

変数 sum を 0 で用意して、
リストを先頭から順に回して、
偶数だったら sum に足していって、
最後に sum を表示する。

「どうやってやるか」を、順番に、細かく、自分で書いています。
これが命令的スタイルです。

命令的スタイルの特徴

命令的スタイルは、処理の流れを自分で完全にコントロールできます。
一方で、「何をしたいのか」と「どうやっているのか」が混ざりやすい、という弱点があります。

上のコードを初めて見る人は、

「偶数だけを取り出して合計したいんだな」

と読み取るまでに、for 文と if 文と代入を頭の中で追いかける必要があります。


宣言的スタイル:Stream で「やりたいこと」をつなぐ世界

同じ処理を Stream で書く(宣言的)

さっきと同じ「偶数だけ取り出して合計する」を、Stream で書き直してみます。

import java.util.List;

public class DeclarativeExample {
    public static void main(String[] args) {
        List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);

        int sum =
                nums.stream()
                    .filter(n -> n % 2 == 0)
                    .mapToInt(n -> n)
                    .sum();

        System.out.println(sum); // 12
    }
}
Java

今度は、やっていることを上から読むだけで、ほぼそのまま意味になります。

「リストを Stream にして、
偶数だけに絞って、
int にして、
合計する。」

「どうやってループを回すか」「どこで変数を初期化するか」といった細かい手順は、全部 Stream API 側に任せています。
自分は「何をしたいか」だけを宣言している。これが宣言的スタイルです。


命令的 vs 宣言的を、もう少し丁寧に対比する

「状態」と「変化」に注目してみる

命令的スタイルでは、しばしば「状態を持つ変数」が出てきます。
さっきの例だと int sum = 0; がそれです。

ループのたびに sum の中身が変わっていきます。
この「状態が変化していく」ことを頭の中で追いかける必要があるので、処理が複雑になると一気に読みづらくなります。

宣言的スタイルでは、「状態を外に持つ」ことを極力減らします。
Stream の中では、filtermap が「新しい Stream を返す」だけで、外側の変数を書き換えません。

その結果、

「このパイプラインは、元のリストをこう変換して、こう集約している」

と、変化の流れを「線」として捉えやすくなります。

「処理の流れ」と「意図」の分離

命令的スタイルでは、「処理の流れ」と「意図」が同じ場所に書かれます。
for 文の中に if があり、その中に代入があり…という形です。

宣言的スタイルでは、「意図」を表す小さな部品(filter, map, sorted, collect など)を組み合わせて、処理の流れを作ります。
各部品は「何をするか」が名前から分かるので、全体として「意図の列」に見えてきます。

Stream を使うときに大事なのは、

「for 文で書いていた“どうやるか”を、そのまま Stream に持ち込まない」

ことです。
forEach の中に if と代入を詰め込み始めると、一気に命令的な世界に引き戻されます。


Stream API は「宣言的スタイルを支える道具」

中間操作は「宣言」、終端操作は「出口」

Stream の中間操作(filter, map, flatMap, sorted, distinct, …)は、全部「宣言」です。

「こういう条件で絞りたい」
「こういう形に変換したい」
「こういう順番に並べたい」

といった「やりたいこと」を、1 ステップずつ積み重ねていきます。

終端操作(collect, reduce, forEach, sum, max, …)は、「その宣言をどう終わらせるか」を決める出口です。
ここで初めて「実行される」イメージです。

この構造のおかげで、

「まず条件を変えたい」
「次に集め方を変えたい」

といった変更が、パイプラインの一部を書き換えるだけで済むようになります。

命令的な癖が出やすい場所:forEach

宣言的スタイルに慣れていないとき、一番危ないのが forEach です。

forEach の中に if や try-catch や外部変数の更新を書き始めると、そこだけ命令的な世界になります。
それ自体が絶対に悪いわけではありませんが、「本当にそこに書くべきか?」は毎回疑った方がいいです。

「それ、filter / map / collect に出せない?」

と自分に問いかける癖をつけると、宣言的スタイルの筋肉が育っていきます。


どちらが「正しい」ではなく、どこで使い分けるか

命令的が向いている場面もちゃんとある

ここまで宣言的スタイルを推してきましたが、命令的スタイルがダメという話ではありません。

例えば、

複雑な状態遷移を伴うアルゴリズム
ネストしたループで細かく break / continue したい処理
パフォーマンスを極限まで詰める必要がある低レベルな処理

などは、素直に命令的に書いた方が読みやすいことも多いです。

大事なのは、「なんとなく全部 Stream で書く」でも、「全部 for 文で書く」でもなく、

「この処理は“何をしたいか”が主役だから宣言的に書こう」
「この処理は“どうやるか”を細かく制御したいから命令的に書こう」

と、意識して選ぶことです。

自分の中の判断基準を持つ

初心者向けに、ざっくりした判断基準を置くなら、こんな感じです。

「コレクションを変換・フィルタ・集約する」 → Stream(宣言的)で書く
「状態を持ちながら細かく制御する」 → for 文(命令的)で書く

そして、Stream を使うときは、

中間操作で「やりたいこと」を並べる
終端操作で「どう終わらせるか」を決める
forEach にロジックを詰め込みすぎない

この 3 点だけ意識しておけば、かなり「宣言的な書き方」に寄せられます。


まとめ:imperative vs declarative を自分の言葉で言うなら

最後に、あなた自身の言葉で整理してみます。

命令的(imperative)は、「こうして、次にこうして、その次にこうして…」と手順を全部書くスタイル。
宣言的(declarative)は、「こういう条件で、こういう変換をして、こういう形で欲しい」と“何をしたいか”だけを並べるスタイル。

Stream API は、この宣言的スタイルを Java で書きやすくするための道具。
for 文で書いていたロジックを、そのまま Stream に持ち込むのではなく、「どの部分を宣言にできるか?」を意識して分解していくと、コードがどんどんシンプルになっていく。

タイトルとURLをコピーしました