web-dev-qa-db-ja.com

Try-finallyブロックはStackOverflowErrorを防ぎます

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

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

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

bar()を実行すると、明らかにStackOverflowErrorになりますが、foo()を実行すると、プログラムは無期限に実行されます。 なぜですか?

326
arshajii

それは永遠に実行されません。各スタックオーバーフローにより、コードは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秒以上かかり、宇宙の年齢よりも長くなります。

328
Peter Lawrey

try内でfoo()の呼び出しから例外を取得すると、finallyからfoo()を呼び出して、再度再帰を開始します。それによって別の例外が発生する場合、別の内部foo()からfinally()を呼び出します。ほとんど同様にad infinitumです。

40
ninjalj

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

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

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

最後に

スレッド「メイン」の例外Java.lang.Exception:TEST! test.main(test.Java:6)で

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

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

38
Alex Coleman

これが最終的に終了するという合理的な証拠を提供するために、次のかなり無意味なコードを提供します。注:Javaは私の言語ではありませんが、最も鮮明な想像力が広がっています。これは、ピーターの答えthe質問に対する正解。

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

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回のトライコール(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)
26
WhozCraig

プログラムをトレースすることを学ぶ:

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は上記のいくつかのレイヤーでスローされるため、別の例外に到達するまで追加の再帰手順を実行できます。これは無限の「ループ」です。

23
Karoly Horvath

プログラムは永久に実行されるように見えます。実際には終了しますが、スタックスペースが増えると指数関数的に時間がかかります。それが終了したことを証明するために、最初に利用可能なスタックスペースのほとんどを使い果たし、次に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を呼び出す回数が他の回数より多い場合も少ない場合もあります)

0
Vitruvius