Java | 再帰とループの最適な使い分け

Java Java
スポンサーリンク

Javaで学ぶ「再帰とループの最適な使い分け」(前半)

Javaを学び始めると、多くの読者が「再帰とループ、どっちを使えばいいのか?」という疑問にぶつかります。 どちらも「繰り返し」を表現する手段ですが、得意分野も弱点も違います。 前半では、まず両者の性格を丁寧に整理しながら、具体的なコード例を通して「考え方の軸」を作っていきます。 後半では、より実務寄りの判断基準やセキュリティ・パフォーマンスの観点まで踏み込みます。

再帰とループは何が違うのか(考え方の軸を作る)

再帰もループも「同じ処理を何度も繰り返す」ための仕組みです。 しかし、頭の中でのイメージはかなり違います。

再帰は「自分自身を呼び出しながら、問題を少しずつ小さくしていく」スタイルです。 ループは「変数を変化させながら、同じブロックを何度も回す」スタイルです。

この違いを、まずはシンプルな例で体感してみましょう。

同じ処理を再帰とループで書き比べる(合計値の計算)

例として「1からnまでの合計を求める」処理を、再帰とループで書き比べてみます。

再帰で書く合計計算

public static int sumRecursive(int n) {
    if (n == 0) {
        return 0; // ベースケース
    }
    return n + sumRecursive(n - 1); // 再帰ステップ
}

public static void main(String[] args) {
    System.out.println(sumRecursive(5)); // 15
}
Java

このコードは「n を足して、残りは自分自身に任せる」という考え方です。 sumRecursive(5) は内部で sumRecursive(4) を呼び出し、さらに sumRecursive(3)…と続き、 最後に sumRecursive(0) で止まります。

ここで重要なのは、再帰が「問題を小さくしながら、自分自身に仕事を渡していく」スタイルだという点です。

ループで書く合計計算

同じ処理をループで書くと、次のようになります。

public static int sumLoop(int n) {
    int result = 0;
    for (int i = 1; i <= n; i++) {
        result += i;
    }
    return result;
}

public static void main(String[] args) {
    System.out.println(sumLoop(5)); // 15
}
Java

こちらは「i を 1 から n まで動かしながら、result に足していく」というスタイルです。 ループは「変数を動かしながら、同じブロックを回す」イメージが強く、 頭の中での流れを追いやすいという特徴があります。

再帰の強み:構造が「入れ子」になっている問題に強い

再帰が本領を発揮するのは、問題の構造そのものが「入れ子」になっているときです。 例えば、ツリー構造・フォルダ階層・JSON・DOMなどです。

ツリー構造の例

class Node {
    String name;
    List<Node> children;

    Node(String name) {
        this.name = name;
        this.children = new ArrayList<>();
    }
}
Java

このツリーを「すべてのノード名を表示する」処理は、再帰で書くと非常に自然です。

public static void traverse(Node node) {
    System.out.println(node.name);

    if (node.children == null || node.children.isEmpty()) {
        return; // ベースケース
    }

    for (Node child : node.children) {
        traverse(child); // 再帰ステップ
    }
}
Java

ここで深掘りしたいポイントは、「コードが構造そのものを表現している」ということです。 ツリー構造は「ノードが子を持ち、その子もまたノードである」という自己相似的な構造を持っています。 再帰は、この「自分と同じ形のものが入れ子になっている」構造を、そのままコードに落とし込めるのです。

ループの強み:線形な問題に強く、パフォーマンスも安定している

一方で、ループが得意なのは「1からnまで」「配列の先頭から末尾まで」といった、 一直線に並んだデータを処理する場面です。

配列の合計をループで計算する

public static int sumArray(int[] arr) {
    int result = 0;
    for (int value : arr) {
        result += value;
    }
    return result;
}
Java

このような「線形な構造」を扱う場合、ループはシンプルで高速です。 再帰で同じことを書けなくはありませんが、 わざわざスタックを使うメリットがほとんどありません。

再帰とループの「読みやすさ」の違いを意識する

初心者にとって重要なのは、「どちらが読みやすいか」という視点です。 読みやすさは、バグの少なさや保守性に直結します。

再帰は、慣れていないと「頭の中で追いにくい」ことがあります。 特に、呼び出しが何段階も重なると、 「今どのレベルの呼び出しを見ているのか」が分からなくなりがちです。

ループは、「上から下へ順番に処理が進む」ため、 初心者でも流れを追いやすいという利点があります。

ただし、ツリー構造のような入れ子の問題では、 ループで書こうとすると逆に読みづらくなることが多いです。 この「読みやすさのトレードオフ」を意識することが、使い分けの第一歩になります。

再帰とループの「安全性」の違い(スタックオーバーフロー)

セキュリティや安定性の観点から見ると、 再帰には特有のリスクがあります。

Javaでは、メソッドを呼び出すたびに「スタックフレーム」が積み上がります。 再帰が深くなりすぎると、このスタックが限界を超え、 StackOverflowError が発生します。

危険な再帰の例

public static void bad(int n) {
    bad(n + 1); // 終了条件がない
}
Java

このようなコードは、意図せず「無限に深く潜る」ことになり、 プログラムがクラッシュします。

ループはスタックを増やさないため、 深さが大きくなっても StackOverflowError は起きません。 その意味で、ループは「安全性が高い」構造と言えます。

前半のまとめ:使い分けの基本方針をざっくり掴む

ここまでの内容を、前半の結論として整理すると次のようになります。

再帰は「構造が入れ子になっている問題」に強く、 コードがその構造を自然に表現できるという大きなメリットがあります。 一方で、深さが増えるとスタックオーバーフローのリスクがあり、 初心者には少し難しく感じられることもあります。

ループは「線形な問題」に強く、 パフォーマンスも安定していて、安全性も高いです。 ただし、ツリー構造などでは、無理にループで書くと逆に読みづらくなることがあります。

後半では、この基本方針をさらに具体化し、

  • 実務での「再帰 vs ループ」の判断パターン
  • 再帰からループへの書き換え例
  • セキュリティ・パフォーマンスを踏まえた選び方
  • 練習問題レベルの具体的な「どっちで書くべきか」ケース集

などを、コードとともに深掘りしていきます。

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