Try - finalmente bloquear evita StackOverflowError


Echa un vistazo a los siguientes dos métodos:

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

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

Ejecutar bar() resulta claramente en un StackOverflowError, pero ejecutar foo() no lo hace (el programa simplemente parece ejecutarse indefinidamente). ¿Por qué es eso?

Author: RPichioli, 2012-09-15

6 answers

No se ejecuta para siempre. Cada desbordamiento de pila hace que el código se mueva al bloque finally. El problema es que tomará mucho, mucho tiempo. El orden del tiempo es O (2^N) donde N es la profundidad máxima de la pila.

Imagine que la profundidad máxima es 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()

Para trabajar cada nivel en el bloque final tomar el doble de tiempo una profundidad de la pila podría ser 10,000 o más. Si puede hacer 10,000,000 llamadas por segundo, esto tomará 10^3003 segundos o más que la edad de la universo.

 326
Author: Peter Lawrey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-09-15 17:17:52

Cuando obtiene una excepción de la invocación de foo() dentro de try, llama a foo() desde finally y comienza a repetir de nuevo. Cuando eso causa otra excepción, llamarás foo() desde otro finally() interior, y así sucesivamente casi ad infinitum.

 40
Author: ninjalj,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-09-15 17:02:32

Intente ejecutar el siguiente código:

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

Encontrará que el bloque finally se ejecuta antes de lanzar una Excepción al nivel superior. (Salida:

Finalmente

Excepción en el hilo "main" java.lang.Excepción: PRUEBA! en la prueba.principal (prueba.java:6)

Esto tiene sentido, ya que finalmente se llama justo antes de salir del método. Esto significa, sin embargo, que una vez que obtenga ese primer StackOverflowError, intentará lanzarlo, pero finalmente debe ejecutarse primero, por lo que se ejecuta foo() de nuevo, que obtiene otro desbordamiento de pila, y como tal se ejecuta finalmente de nuevo. Esto sigue sucediendo para siempre, por lo que la excepción nunca se imprime realmente.

En su método bar, sin embargo, tan pronto como se produce la excepción, se lanza directamente al nivel anterior, y se imprimirá

 37
Author: Alex Coleman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-09-15 15:54:39

En un esfuerzo por proporcionar evidencia razonable de que esto terminará eventualmente, ofrezco el siguiente código bastante sin sentido. Nota: Java NO es mi lenguaje, por cualquier tramo de la imaginación más vívida. Ofrezco esto solo para apoyar la respuesta de Pedro, que es la respuesta correcta a la pregunta.

Esto intenta simular las condiciones de lo que sucede cuando una invocación NO puede suceder porque introduciría un desbordamiento de pila. Me parece lo más difícil gente están fallando en comprender que la invocación no sucede cuando no puede suceder.

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+")");
        }
    }
}

La salida de esta pequeña pila sin sentido de goo es la siguiente, y la excepción real atrapada puede ser una sorpresa; Oh, y 32 try-calls (2^5), que se espera por completo:

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)
 25
Author: WhozCraig,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-09-18 22:08:17

Aprenda a rastrear su programa:

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

Esta es la salida que veo:

[...]
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
[...]

Como puede ver, el StackOverFlow se lanza a algunas capas de arriba, por lo que puede hacer pasos de recursión adicionales hasta que llegue a otra excepción, y así sucesivamente. Este es un "bucle" infinito.

 23
Author: Karoly Horvath,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-09-15 16:14:31

El programa simplemente parece ejecutarse para siempre; en realidad termina, pero toma exponencialmente más tiempo cuanto más espacio de pila tenga. Para probar que termina, escribí un programa que primero agota la mayor parte del espacio disponible de la pila, y luego llama foo, y finalmente escribe un rastro de lo que sucedió:

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)
    ...

El código:

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);
      }
    });
  }
}

Puedes probarlo en línea! (Algunas ejecuciones pueden llamar a foo más o menos veces que otras)

 0
Author: Vitruvius,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-04-11 00:08:28