335

次の 2 つの方法を見てください。

public static void foo() {
    try {
        foo();
    } finally {
        foo();
    }
}

public static void bar() {
    bar();
}

実行bar()すると明らかに が発生しますStackOverflowErrorが、実行foo()はそうではありません (プログラムは無期限に実行されているように見えます)。何故ですか?

4

6 に答える 6

333

それは永遠に実行されません。各スタック オーバーフローにより、コードは finally ブロックに移動します。問題は、非常に長い時間がかかることです。時間の順序は O(2^N) で、N はスタックの最大深度です。

最大深度が 5 であると想像してください

foo() calls
    foo() calls
       foo() calls
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()
       finally
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()
    finally calls
       foo() calls
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()
       finally
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()
finally calls
    foo() calls
       foo() calls
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()
       finally
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()
    finally calls
       foo() calls
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()
       finally
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()

各レベルを finally ブロックに入れるには、スタックの深さが 10,000 以上になる可能性があるため、2 倍の時間がかかります。1 秒あたり 10,000,000 回の呼び出しを行うことができる場合、これには 10^3003 秒または宇宙の年齢よりも長くかかります。

于 2012-09-15T16:25:23.507 に答える
40

foo()内部の呼び出しから例外を受け取ったtry場合は、から呼び出して、再度繰り返しを開始します。それが別の例外を引き起こすとき、あなたは別の内部から呼び出すでしょう、等々はほとんど無限に。foo()finallyfoo()finally()

于 2012-09-15T15:53:18.617 に答える
38

次のコードを実行してみてください。

    try {
        throw new Exception("TEST!");
    } finally {
        System.out.println("Finally");
    }

例外をその上のレベルまでスローする前に、finallyブロックが実行されることがわかります。(出力:

ついに

スレッド"main"java.lang.Exceptionの例外:TEST!test.main(test.java:6)で

メソッドを終了する直前にfinallyが呼び出されるため、これは理にかなっています。ただし、これは、最初StackOverflowErrorに取得するとスローしようとしますが、finallyを最初に実行する必要があるため、foo()再度実行され、別のスタックオーバーフローが発生し、finalが再度実行されることを意味します。これは永遠に発生し続けるため、例外が実際に出力されることはありません。

ただし、barメソッドでは、例外が発生するとすぐに、上のレベルまでまっすぐにスローされ、印刷されます。

于 2012-09-15T15:54:39.177 に答える
26

これが最終的に終了するという合理的な証拠を提供するために、次のかなり無意味なコードを提供します。注: Java は私の言語ではありません。これは、質問に対する正しい答えであるピーターの答えをサポートするためだけに提案します。

これは、スタック オーバーフローが発生するために呼び出しが発生しない場合に発生する状況をシミュレートしようとします。人々が把握できていない最も難しいことは、呼び出しが起こらないのに起こらないということです。

public class Main
{
    public static void main(String[] args)
    {
        try
        {   // invoke foo() with a simulated call depth
            Main.foo(1,5);
        }
        catch(Exception ex)
        {
            System.out.println(ex.toString());
        }
    }

    public static void foo(int n, int limit) throws Exception
    {
        try
        {   // simulate a depth limited call stack
            System.out.println(n + " - Try");
            if (n < limit)
                foo(n+1,limit);
            else
                throw new Exception("StackOverflow@try("+n+")");
        }
        finally
        {
            System.out.println(n + " - Finally");
            if (n < limit)
                foo(n+1,limit);
            else
                throw new Exception("StackOverflow@finally("+n+")");
        }
    }
}

この小さな無意味な塊の出力は次のとおりです。実際にキャッチされた例外は驚くべきものです。ああ、そして 32 回の try-call (2^5) です。これは完全に予想されることです。

1 - Try
2 - Try
3 - Try
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
3 - Finally
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
2 - Finally
3 - Try
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
3 - Finally
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
1 - Finally
2 - Try
3 - Try
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
3 - Finally
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
2 - Finally
3 - Try
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
3 - Finally
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
java.lang.Exception: StackOverflow@finally(5)
于 2012-09-16T05:50:24.290 に答える
23

プログラムを追跡する方法を学びます。

public static void foo(int x) {
    System.out.println("foo " + x);
    try {
        foo(x+1);
    } 
    finally {
        System.out.println("Finally " + x);
        foo(x+1);
    }
}

これは私が見る出力です:

[...]
foo 3439
foo 3440
foo 3441
foo 3442
foo 3443
foo 3444
Finally 3443
foo 3444
Finally 3442
foo 3443
foo 3444
Finally 3443
foo 3444
Finally 3441
foo 3442
foo 3443
foo 3444
[...]

ご覧のとおり、StackOverFlowは上のいくつかのレイヤーでスローされるため、別の例外が発生するまで、追加の再帰手順を実行できます。これは無限の「ループ」です。

于 2012-09-15T16:07:30.867 に答える
0

プログラムは永久に実行されているように見えます。実際には終了しますが、スタックスペースが多いほど指数関数的に時間がかかります。終了したことを証明するために、最初に使用可能なスタック領域の大部分を使い果たし、次に を呼び出しfoo、最後に何が起こったかのトレースを書き込むプログラムを作成しました。

foo 1
  foo 2
    foo 3
    Finally 3
  Finally 2
    foo 3
    Finally 3
Finally 1
  foo 2
    foo 3
    Finally 3
  Finally 2
    foo 3
    Finally 3
Exception in thread "main" java.lang.StackOverflowError
    at Main.foo(Main.java:39)
    at Main.foo(Main.java:45)
    at Main.foo(Main.java:45)
    at Main.foo(Main.java:45)
    at Main.consumeAlmostAllStack(Main.java:26)
    at Main.consumeAlmostAllStack(Main.java:21)
    at Main.consumeAlmostAllStack(Main.java:21)
    ...

コード:

import java.util.Arrays;
import java.util.Collections;
public class Main {
  static int[] orderOfOperations = new int[2048];
  static int operationsCount = 0;
  static StackOverflowError fooKiller;
  static Error wontReachHere = new Error("Won't reach here");
  static RuntimeException done = new RuntimeException();
  public static void main(String[] args) {
    try {
      consumeAlmostAllStack();
    } catch (RuntimeException e) {
      if (e != done) throw wontReachHere;
      printResults();
      throw fooKiller;
    }
    throw wontReachHere;
  }
  public static int consumeAlmostAllStack() {
    try {
      int stackDepthRemaining = consumeAlmostAllStack();
      if (stackDepthRemaining < 9) {
        return stackDepthRemaining + 1;
      } else {
        try {
          foo(1);
          throw wontReachHere;
        } catch (StackOverflowError e) {
          fooKiller = e;
          throw done; //not enough stack space to construct a new exception
        }
      }
    } catch (StackOverflowError e) {
      return 0;
    }
  }
  public static void foo(int depth) {
    //System.out.println("foo " + depth); Not enough stack space to do this...
    orderOfOperations[operationsCount++] = depth;
    try {
      foo(depth + 1);
    } finally {
      //System.out.println("Finally " + depth);
      orderOfOperations[operationsCount++] = -depth;
      foo(depth + 1);
    }
    throw wontReachHere;
  }
  public static String indent(int depth) {
    return String.join("", Collections.nCopies(depth, "  "));
  }
  public static void printResults() {
    Arrays.stream(orderOfOperations, 0, operationsCount).forEach(depth -> {
      if (depth > 0) {
        System.out.println(indent(depth - 1) + "foo " + depth);
      } else {
        System.out.println(indent(-depth - 1) + "Finally " + -depth);
      }
    });
  }
}

オンラインでお試しできます!foo(一部の実行では、他の実行よりも呼び出し回数が多い場合と少ない場合があります)

于 2018-04-11T00:08:28.113 に答える