Блок Try-finally предотвращает StackruError

Взгляните на следующие два метода:

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

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

Бег bar() явно приводит к StackruError, но работает foo() нет (программа, кажется, работает бесконечно). Это почему?

6 ответов

Решение

Это не вечно. При каждом переполнении стека код перемещается в блок 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 и более. Если вы можете делать 10000000 вызовов в секунду, это займет 10^3003 секунды или дольше, чем возраст вселенной.

Когда вы получаете исключение из вызова foo() внутри try, ты звонишь foo() от finally и начать возвращаться снова. Когда это вызывает другое исключение, вы позвоните foo() из другого внутреннего finally()и так далее до бесконечности.

Попробуйте запустить следующий код:

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

Вы обнаружите, что блок finally выполняется, прежде чем выбросить исключение на уровень выше него. (Выход:

в заключение

Исключение в теме "основной" java.lang. Исключение: ТЕСТ! в test.main(test.java:6)

Это имеет смысл, поскольку, наконец, вызывается перед выходом из метода. Это означает, однако, что, как только вы получите это первым StackruError, он попытается выбросить его, но, наконец, должен сначала выполнить, поэтому он работает foo() снова, который получает другое переполнение стека, и, как таковой, выполняется, наконец, снова. Это происходит вечно, поэтому исключение никогда не печатается.

Однако в вашем методе bar, как только возникает исключение, оно просто выбрасывается прямо на уровень выше и будет напечатано

В попытке предоставить разумные доказательства того, что это в конечном итоге прекратится, я предлагаю следующий довольно бессмысленный код. Примечание: 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("Stackru@try("+n+")");
        }
        finally
        {
            System.out.println(n + " - Finally");
            if (n < limit)
                foo(n+1,limit);
            else
                throw new Exception("Stackru@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: Stackru@finally(5)

Научитесь отслеживать свою программу:

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 создается на нескольких уровнях выше, поэтому вы можете выполнять дополнительные шаги рекурсии, пока не достигнете другого исключения, и так далее. Это бесконечная "петля".

Программа, кажется, работает вечно; это фактически завершается, но это занимает экспоненциально больше времени, чем больше у вас стекового пространства. Чтобы доказать, что это заканчивается, я написал программу, которая сначала истощает большую часть доступного пространства стека, а затем вызывает 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.StackruError
    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 StackruError 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 (StackruError e) {
          fooKiller = e;
          throw done; //not enough stack space to construct a new exception
        }
      }
    } catch (StackruError 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 больше или меньше, чем другие)

Другие вопросы по тегам