java что - Блок Try-finally предотвращает StackOverflowError




передано ноль (6)

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

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

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

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

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

Запуск bar() явно приводит к возникновению Error , но запуск foo() не работает (программа просто работает бесконечно). Это почему?


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


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


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

Это пытается имитировать условия того, что происходит, когда invoke НЕ может произойти, потому что это приведет к переполнению стека. Мне кажется, что самое трудное, что люди не понимают, что вызов не происходит, когда этого не может случиться.

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("@try("+n+")");
        }
        finally
        {
            System.out.println(n + " - Finally");
            if (n < limit)
                foo(n+1,limit);
            else
                throw new Exception("@finally("+n+")");
        }
    }
}

Результатом этой маленькой бессмысленной кучи goo является следующее, и фактическое исключение может стать неожиданностью; Ох и 32 try-calls (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: @finally(5)

Интересно то же самое. API Assert не очень симметричен; для проверки того, являются ли объекты одинаковыми, он предоставляет assertSame и assertNotSame .

Конечно, писать не так уж долго:

assertFalse(foo.equals(bar));

С таким утверждением единственной информативной частью выхода, к сожалению, является имя тестового метода, поэтому описательное сообщение должно формироваться отдельно:

String msg = "Expected <" + foo + "> to be unequal to <" + bar +">";
assertFalse(msg, foo.equals(bar));

Это, конечно, так утомительно, что лучше сворачивать свой собственный assertNotEqual . К счастью, в будущем это, возможно, будет частью JUnit: JUnit issue 22







java recursion stack-overflow try-finally