看看下面两种方法:

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

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

运行bar()显然会导致StackOverflowError,但运行foo()不会(程序似乎无限期地运行)。为什么呢?


当前回答

学习跟踪你的程序:

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在上面的一些层上抛出,因此您可以执行额外的递归步骤,直到遇到另一个异常,等等。这是一个无限“循环”。

其他回答

当你在try中从foo()调用中获得异常时,你从finally调用foo()并再次开始递归。当这导致另一个异常时,您将从另一个内部finally()调用foo(),以此类推,直到无穷无尽。

试着运行下面的代码:

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

你会发现finally块在抛出一个异常到它上面的级别之前执行。(输出:

最后 java.lang.Exception: TEST! test.main (test.java: 6)

这是有意义的,因为finally是在退出方法之前调用的。然而,这意味着一旦您获得第一个StackOverflowError,它将尝试抛出它,但finally必须首先执行,因此它再次运行foo(),这将获得另一个堆栈溢出,并最终再次运行。这种情况会一直发生,所以异常实际上不会被打印出来。

然而,在bar方法中,一旦异常发生,它就会直接抛出到上面的级别,并将被打印

它不会永远运行。每次堆栈溢出都会导致代码移动到最后一个块。问题是这需要非常非常长的时间。时间的顺序是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()

将每一层工作到最后一个块需要两倍于堆栈深度的时间 一万或更多。如果你能每秒拨打10,000,000个电话,这将需要10^3003秒,甚至比宇宙的年龄还要长。

程序似乎永远在运行;它实际上会终止,但是你拥有的堆栈空间越大,它所花费的时间就会呈指数级增长。为了证明它完成了,我写了一个程序,首先耗尽了大部分可用的堆栈空间,然后调用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的次数更多或更少)

为了提供合理的证据证明这种情况最终会终止,我提供了以下相当无意义的代码。注意:无论如何,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调用(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)