Javaで「再帰が止まらない!」StackOverflowErrorから学ぶ、スタックと設計の話

Java入門・実践
スポンサーリンク

再帰が止まらない!StackOverflowErrorとの出会い――

StackOverflowErrorに初めて出会ったとき、私はこう思いました。

「え、スタック?オーバーフロー??なんか強そうな名前来た…」

しかもJavaはものすごく冷静に、こんなメッセージを出してくるのです。

Exception in thread "main" java.lang.StackOverflowError

なんか…落ちたっぽいぞ。 でも何が起きたのかはまったく分からない。

「再帰って、呼び出し合う関数でしょ?よし、使ってみよう!」と思って書いたコードが、気づいたら自分で自分を無限に呼び続けていたのです。

そう、これは再帰の“出口がなかった”ことで起こるクラッシュ。

「スタックって何?」「なんで溢れるの?」という謎に、一歩ずつ向き合ってみると、 ただのエラーじゃなくて“設計のヒント”が隠れていました。

本記事では、StackOverflowErrorの原因・よくある再帰のやらかし・設計で防ぐ考え方まで、初心者の方にもわかりやすく解説していきます。

スポンサーリンク
  1. StackOverflowErrorとは?
    1. スタックってどこ?なに?どうしてオーバーフロー?
    2. たとえるなら…スタックは「お弁当箱の積み重ね」
    3. オーバーフローの原因は「呼びすぎ・片付けなさすぎ」
    4. StackOverflowErrorの特徴と見分け方
    5. この章のまとめ
  2. 再帰の落とし穴
    1. 呼び出したのは私。でも止め方を知らなかった
    2. 実は「出口がない」=永遠ループ
    3. よくあるやらかしパターン
      1. その①:終了条件の書き忘れ
      2. その②:逆方向に進んでいる
      3. その③:相互再帰(お互いに呼び合ってる)
    4. ベースケースの大切さ
    5. どこで止まる?を先に考える癖を
    6. この章のまとめ
  3. スタックの仕組み
    1. スタックメモリって、つまりは「お弁当の重ね置き」
    2. スタックメモリの動き方
    3. 再帰がスタックをパンパンにする仕組み
    4. スタックサイズって決まってるの?
    5. エラーのスタックトレースを見るコツ
    6. この章のまとめ
  4. 回避方法と再帰の安全設計
    1. まずは出口をつけよう ― ベースケースの重要性
    2. 再帰の“方向”を見失わないで
    3. 再帰じゃなくても書けるなら、それも“優しさ”
    4. 安全にするためのチェックリスト
    5. この章のまとめ
  5. 設計の気づき
    1. 再帰は「呼ぶ力」よりも「戻る力」が大事だった
    2. 設計で“呼びすぎない”コードに
    3. 呼び出し構造を設計の言語にする
    4. 呼び出しとスタックの設計は「安心を渡す仕組み」
    5. この章のまとめ
  6. まとめ
    1. 呼びすぎたから落ちた。でも、それだけじゃなかった
    2. 限界を知ることで、設計はやさしくなる
    3. エラーは、やさしい設計へのヒントだった
    4. そしてこれからも、帰れるコードを書こう

StackOverflowErrorとは?

――呼びすぎて“積み重ね”が限界に

スタックってどこ?なに?どうしてオーバーフロー?

StackOverflowError を初めて見たとき、私はこう思いました。

「スタックって、あの質問サイトの名前じゃなかったっけ…?」

そのくらい、「スタック」も「オーバーフロー」も聞き慣れない単語でした。

でもこのエラーは、Javaの“呼び出しメモリ”がパンクしたサインなんです。

たとえるなら…スタックは「お弁当箱の積み重ね」

スタックとは、Javaがメソッド(関数)の呼び出しを管理するための領域のこと。

たとえば、AさんがBさんに「ちょっとこれお願い」と頼み、BさんがCさんに…と続くイメージです。

プログラム上ではこれが以下のように積み重なります。

  • main()が foo() を呼ぶ
  • foo() が bar() を呼ぶ
  • bar() が baz() を呼ぶ…

こうして、呼び出し元→呼び出された先の順にスタックメモリに積まれていきます。

そして、処理が終わると“上から順に片付けて”戻ってくる。

イメージ:スタック = お弁当箱のタワー。 一番上からしか出せないので、取り出すにも「順番」がある。

オーバーフローの原因は「呼びすぎ・片付けなさすぎ」

StackOverflowError が起きるとき、ほとんどの場合は再帰処理の暴走が原因です。

Java
public void callMe() {
    callMe(); // ❌ 永遠に自分を呼び続ける
}

このコード、呼ばれるたびにスタックにメソッドが積まれていきますが、 終了条件がないので “片付けられないまま積み上がってしまう”んです。

スタックメモリには上限があるため、それを超えると……

Exception in thread "main" java.lang.StackOverflowError

→ メモリ限界でクラッシュ!

StackOverflowErrorの特徴と見分け方

特徴説明
エラークラスは Error Exception ではなく Error(キャッチしても治らない)
主な原因は“再帰の出口なし”再帰関数に終了条件がない or 失敗している
スタックトレースが異常に長い 同じ行が延々と繰り返されているのが特徴

たとえば

at MyClass.callMe(MyClass.java:5)  
at MyClass.callMe(MyClass.java:5)  
at MyClass.callMe(MyClass.java:5)  
...(これが数百回)

→ 無限ループで積み重なったことが丸わかり!

この章のまとめ

観点要点
Stackとは?メソッド呼び出しを積み重ねる場所
Overflowとは?スタックの積みすぎで容量オーバー
よくある原因終了条件なしの再帰処理
見分け方同じメソッドのスタックトレースが連続して表示される

次章では、実際によくある“再帰のやらかし例”を見ながら、どうして止まらなくなったのか? という再帰処理の注意ポイントを探っていきます。

再帰の落とし穴

――終わらない呼び出しは事故のもと

呼び出したのは私。でも止め方を知らなかった

再帰って、かっこよくないですか? 関数が自分自身を呼び出すって、なんだか数学っぽいし、ロジックもスマートに見える。

私もはじめて再帰を書いたとき、ちょっとテンション上がっていました。

Java
public void callMe() {
    callMe(); // なんか“再帰っぽい”から書いてみた
}

…で、実行してみると。 はい、落ちました。あっさりと StackOverflowError。

実は「出口がない」=永遠ループ

再帰は「入り口」と「出口」がセットになってこそ成り立つもの。

でも、出口(終了条件)を忘れると、関数は永遠に自分を呼び続けてしまいます。

Java
public int count(int n) {
    return count(n + 1); // ❌ 終了条件がない → 無限呼び出し
}

このコード、count(0) を呼んだら count(1) → count(2) → count(3)… と永遠に進み続けます。

そしてスタックメモリがいっぱいになり…ドカン。

よくあるやらかしパターン

その①:終了条件の書き忘れ

Java
public void recurse() {
    recurse(); // ❌ どこまで呼ぶつもり?
}

→ 忘れたというより、「必要だと気づいてない」が原因

その②:逆方向に進んでいる

Java
public void countDown(int n) {
    if (n == 0) return;
    countDown(n + 1); // ❌ 本当は n - 1 のはず…
}

→ 呼び出すたびにスタックが増えて、しかも終了条件に永遠に届かない!

その③:相互再帰(お互いに呼び合ってる)

Java
public void foo() {
    bar(); // foo → bar
}
public void bar() {
    foo(); // bar → foo → foo → bar…
}

→ 相互依存で無限ループ状態に。互いに「君が終わるまで待つよ」状態。

ベースケースの大切さ

再帰を書くときに絶対に必要なのが、終了条件(ベースケース)です。

Java
public int factorial(int n) {
    if (n == 0) return 1; // ✅ ここが終了条件
    return n * factorial(n - 1);
}

✅ これがあると、

  • factorial(3) → 3 * factorial(2)
  • 2 * factorial(1) → 1 * factorial(0)
  • factorial(0) → 1(終了!)

という流れで、ちゃんとスタックが片付きながら戻ってきます。

どこで止まる?を先に考える癖を

再帰を使うときは、かっこよさに惑わされず以下を確認しましょう。

  • 呼び出し条件:何をもって進めるのか?
  • 終了条件:どこで止めるか?
  • スタックの深さ:終わるまでに何段積むのか?

これだけでも StackOverflowError との遭遇率はぐっと下がります。

この章のまとめ

やらかし学び
終了条件を忘れて無限再帰「出口」を絶対に書くこと
引数の変化が逆方向 “どこに向かってるか”をチェック
相互再帰でぐるぐるロジックを一度紙に書き出して整理しよう
再帰の深さを気にしない“何回呼ぶ?”を設計時に考える癖をつける

次章では、再帰でスタックが積み重なる様子をもっと深く掘り下げて、「なぜオーバーフローが起きるのか?」を視覚的&構造的にやさしく解説していきます。

スタックの仕組み

――メソッド呼び出しの積み重ね構造

スタックメモリって、つまりは「お弁当の重ね置き」

プログラムで誰かが誰かを呼び出すとき(つまりメソッドの呼び出し)、その記録は「スタック」という場所に積まれていきます。

この積み方、実はとてもルールがシンプルです。

一番上に乗せたものしか取り出せない。

一番下にあるものは、全部片付けないと出てこない。

まるで お弁当箱を縦に積み重ねた状態。

あとから詰めたやつから順に食べていかないと、おにぎりにはたどり着けないんです。

スタックメモリの動き方

たとえばこんなコード

Java
public void main() {
    sayHello();
}

public void sayHello() {
    greet();
}

public void greet() {
    System.out.println("こんにちは!");
}

この場合のスタックはこんな順番になります

  • main() が呼ばれる → スタックに main が積まれる
  • main から sayHello() → スタックに sayHello が積まれる
  • sayHello から greet() → スタックに greet が積まれる
  • greet の処理が終わる → スタックから greet を片付ける
  • 次に sayHello が終わり → sayHello を片付ける
  • 最後に main が終わる → main を片付ける

スタックは「入れた順」ではなく「出した順」が逆になる、LIFO(Last In, First Out)構造なのです。

再帰がスタックをパンパンにする仕組み

さて、これが再帰処理になるとどうなるでしょう?

Java
public void recurse(int n) {
    if (n == 0) return;
    recurse(n - 1);
}

この場合、recurse(5) を呼び出すと

  • recurse(5)
  • recurse(4)
  • recurse(3)
  • recurse(2)
  • recurse(1)
  • recurse(0)(ここで終了!)

→ それぞれが終わるまで、スタックに積みっぱなしになります。

もし終了条件がなかったら? スタックはどこまでも積み重なっていき、やがて容量オーバー → StackOverflowError に。

スタックサイズって決まってるの?

はい、Javaでは スタック領域のサイズに上限があります。

環境にもよりますが、だいたい「数千〜数万回の呼び出し」で限界が来ることも。

設定の一例:

ShellScript
java -Xss256k MyClass

→ -Xss はスタックサイズを指定するオプション。デフォルトは1MB前後です。

でも、サイズを増やすよりも そもそも無限に呼ばない設計の方が重要です。

エラーのスタックトレースを見るコツ

スタックトレースがずらーっと並んでいたら、見分けポイントはこちら

  • 同じメソッドが何百回も繰り返されている → 再帰の出口がない
  • 行番号がずっと同じ → 特定の行で無限ループしてる
  • 最後に StackOverflowError と出ている → スタックが限界突破!

この情報が、“どこで止まらなかったか”のヒントになります。

この章のまとめ

キーワード内容
スタックメソッド呼び出しを管理する領域
LIFO構造最後に入れたものが最初に出る
再帰との関係呼び出しが深くなるほどスタックが積まれる
エラーの正体スタックの容量を超えると StackOverflowError 発生
見極め方スタックトレースに同じメソッドの繰り返しが出る

次章では、「じゃあどうすればStackOverflowErrorを防げるの?」という視点から、安全な再帰とエラー回避の設計パターンをやさしく整理していきます。

回避方法と再帰の安全設計

――再帰に“出口”を、設計に“優しさ”を

まずは出口をつけよう ― ベースケースの重要性

再帰で一番大切なのは「止まる理由」を明示しておくこと。

これが ベースケース(終了条件) です。

Java
public int factorial(int n) {
    if (n == 0) return 1;       // ⭕️ 終了条件
    return n * factorial(n - 1); // 再帰の進行
}

このコードの流れ:

  • factorial(3) → 3 * factorial(2)
  • → 2 * factorial(1) → 1 * factorial(0)
  • → factorial(0) で終了 → そこから結果を返しながらスタックが片付いていく!

ベースケースがないと、どこまでも呼び出し続けてしまうので StackOverflowError にまっしぐらです。

再帰の“方向”を見失わないで

再帰では呼び出しごとに値を変化させていきますが、その方向を間違えると終了条件にたどり着きません。

Java
public void countdown(int n) {
    if (n == 0) return;         // ✅ ちゃんと終わる
    countdown(n - 1);           // 👌 減っていく
}

ありがちなミス:

Java
countdown(n + 1); // 増やしてどうするんですか…!永遠に遠ざかる

再帰の設計では、「どこに向かってるか」→「そこにちゃんと届くか」がとても大事です。

再帰じゃなくても書けるなら、それも“優しさ”

場合によっては、再帰よりループ(for/while)で書く方が安全で分かりやすいこともあります。

再帰

Java
public void print(int n) {
    if (n == 0) return;
    System.out.println(n);
    print(n - 1);
}

ループ

Java
for (int i = n; i > 0; i--) {
    System.out.println(i);
}

特に回数が多い・処理が重い・終了条件が複雑な場合は、スタックに依存しないループの方が安心です。

安全にするためのチェックリスト

チェック項目意図
終了条件はありますか?呼び出しの止め時を明示しているか
引数は正しく変化していますか?終了条件に近づいているか
再帰の深さは想定内ですか?スタックが溢れそうな呼び出し数になっていないか
ループで書き換え可能ですか?安全かつ分かりやすくできるかの確認

再帰を書く前にこの4点をチェックするだけでも、StackOverflowError との距離はぐっと遠ざかります。

この章のまとめ

  • 再帰を書くならまず終了条件(ベースケース)をつけよう
  • 引数の変化を見ながら「終点に向かって進んでいるか」を確認しよう
  • 再帰じゃなくてもできることは、ループも検討してみよう
  • 書く前に「呼びすぎてないか?」と優しく疑ってみることが、安心設計の第一歩

次章では、StackOverflowError を通して得られた“設計の気づき”に焦点を当てて、コードとの距離感や呼び出し構造の考え方を深めていきます。

設計の気づき

――呼び出し構造を味方にするコードへ

再帰は「呼ぶ力」よりも「戻る力」が大事だった

再帰って、「自分を呼ぶ」技術のように見えて、実は本質は逆。

“ちゃんと帰ってくること”に意味があるのだと、StackOverflowErrorに出会って初めて気づきました。

再帰処理は、呼び出しの一歩一歩に“積み重ね”があり、それは スタックが肩代わりしてくれる信頼構造。

でも、その信頼を裏切ると、スタックは黙って爆発します(←わりと静かに)。

設計で“呼びすぎない”コードに

StackOverflowErrorを経験してから、コードの書き方が少し変わりました。

  • 「再帰の進行方向」を紙に書き出すようになった
  • 再帰じゃなくてループでもいいんじゃない?と柔軟に考えるようになった
  • 終了条件は、処理の最後じゃなく最初に確認する癖がついた

この変化って、「設計に優しさが生まれた」ってことなのかもしれません。

呼び出し構造を設計の言語にする

複雑な処理を書くとき、再帰に頼りたくなる場面はあります。

でもその時こそ、スタックが積まれていくことの意味をしっかり設計で意識する必要があります。

書き方の違い設計の意味
再帰を書いた「状態の連鎖を成立させたい」
終了条件を先に書いた「戻る力の保証を持たせたい」
再帰を避けてループにした「積みすぎない選択を優先した」

コードはただ動くだけではなく、読み手・実行環境・未来の自分との対話でもあると感じるようになりました。

呼び出しとスタックの設計は「安心を渡す仕組み」

誰かが誰かを呼ぶ。 それがどれだけ深くなっても、帰れる保証があれば怖くない。

再帰って、つまり 「帰り道まで設計して初めて成り立つ冒険」だったんですね。

呼びっぱなしじゃない。 止め方・戻り方・片付け方まで含めてこそ、安心して使える機能だと、StackOverflowErrorが教えてくれました。

この章のまとめ

設計の気づき行動に変えたこと
呼び出し構造に責任を持つ必要性終了条件を意識して書くようになった
再帰の限界とスタックの仕組みループへの置き換えも検討するようになった
エラーは設計のレントゲン “何が足りなかったか”を考える習慣がついた

次章ではいよいよ最終章。StackOverflowErrorとの出会いが、どんな気づきや安心設計のヒントにつながったのか――その振り返りをやさしくお届けします。

まとめ

――StackOverflowErrorが教えてくれたのは“限界の存在”

呼びすぎたから落ちた。でも、それだけじゃなかった

StackOverflowError に初めて出会ったとき、私は「再帰を書いたら落ちた」くらいにしか捉えていませんでした。

でも、そこには思っていたよりも深い“問いかけ”が隠れていました。

「このコード、どこまで進むつもりだったの?」 「止まる保証、つけてる?」 「その呼び出し、本当に戻ってこられる?」

静かに爆発するスタックの中で、私たちは設計の責任を問われていたのかもしれません。

限界を知ることで、設計はやさしくなる

スタックメモリの限界に直面したことで、「コードにも体力がある」という感覚を持つようになりました。 それはつまり、“無限に呼べる”わけではないし、“無限に信用してはいけない”ということ。

  • 呼び出しの深さ
  • 終了条件の有無
  • 再帰が本当に必要かどうか

それぞれが、設計の中で“どこまで責任を持つか”を丁寧に考えるきっかけになったのです。

エラーは、やさしい設計へのヒントだった

私にとって StackOverflowError は、ただの「再帰ミス」ではありませんでした。 それは

  • 呼び出しと戻りの設計
  • 限界を踏まえた責任の分担
  • スタックという“縁の下”との付き合い方

こうした気づきを、あえて“落ちる”という形で教えてくれた小さなきっかけだったのだと思います。

そしてこれからも、帰れるコードを書こう

再帰は今も好きです。 でもそれは、「ちゃんと帰ってこられる設計がある」という安心の上に成り立つもの。

呼んだら戻る。進んだら止まる。 そんな コードとの“約束”があることが、設計のやさしさなのだと思うのです。

この記事が、初めて再帰と向き合う誰かの“つまずきと不安”にそっと寄り添ってくれていたら嬉しいです。

そして「StackOverflowErrorが出た!」と検索した先で、少しだけ安心してもらえるようなページでありますように。

エラーとの出会いは、ちょっと苦くて、でも確かに前に進むヒントでした。

次はどんな“落ち方”と出会えるでしょうか。それもまた、楽しみです。

decopon
decopon

「再帰で書いてみたら落ちました…」という地味なやらかしから始まり、スタックメモリって何?呼びすぎるってどういうこと?と一つずつ向き合っていくうちに、少しずつ設計の視点まで見えてきた気がします。

エラーって、できれば出てほしくないものですよね。 でも実は、“設計を見直すヒント”として顔を出してくれる存在でもあります。

StackOverflowErrorは、無限に呼ぶことの怖さを教えてくれると同時に、 「戻ってこられるコードって、なんだか安心だな」と思わせてくれる学びのきっかけでもありました。

もしこの記事が、あなたの「なんで落ちたんだろう…」に寄り添えたなら、それ以上に嬉しいことはありません。

moco
moco

ただいまって…帰ってこなかったら言えないのです…🐾

コメント

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