El bloque Try-finally evita StackOverflowError

10 minutos de lectura

avatar de usuario
arshajii

Eche un vistazo a los siguientes dos métodos:

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

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

Correr bar() resulta claramente en un StackOverflowErrorpero corriendo foo() no lo hace (el programa parece ejecutarse indefinidamente). ¿Porqué es eso?

  • Formalmente, el programa eventualmente se detendrá debido a errores arrojados durante el procesamiento del finally cláusula se propagará al siguiente nivel. Pero no aguantes la respiración; el número de pasos tomados será de aproximadamente 2 a la (profundidad máxima de pila) y el lanzamiento de excepciones tampoco es exactamente barato.

    – Becarios Donal

    15/09/2012 a las 16:41

  • Sería “correcto” para bar()aunque.

    – dan04

    15 de septiembre de 2012 a las 16:47

  • @ dan04: Java no hace TCO, IIRC para garantizar tener seguimientos de pila completos y para algo relacionado con la reflexión (probablemente también tenga que ver con seguimientos de pila).

    – ninjalj

    15 de septiembre de 2012 a las 16:47

  • Curiosamente, cuando probé esto en .Net (usando Mono), el programa se bloqueó con un error de StackOverflow sin llamar finalmente.

    – Kibbee

    15/09/2012 a las 18:55

  • Esta es la peor pieza de código que he visto 🙂

    – poitroae

    16 de septiembre de 2012 a las 9:49

avatar de usuario
pedro laurey

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

Imagina 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, toma el doble de tiempo y la profundidad de la pila podría ser de 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 del universo.

  • Bien, incluso si trato de hacer la pila lo más pequeña posible a través de -Xssobtengo una profundidad de [150 – 210]entonces 2^n termina siendo un [47 – 65] número de dígitos. No voy a esperar tanto, eso es lo suficientemente cerca del infinito para mí.

    – ninjalj

    15/09/2012 a las 17:05


  • @oldrinb Solo para ti, aumenté la profundidad a 5. 😉

    – Peter Lawrey

    15 de septiembre de 2012 a las 17:18


  • Entonces, al final del día cuando foo finalmente termina, dará como resultado un StackOverflowError?

    – arshajii

    15/09/2012 a las 21:29


  • siguiendo las matemáticas, sí. el último desbordamiento de pila desde el último que finalmente falló en el desbordamiento de pila saldrá con… desbordamiento de pila =P. no pude resistir

    – WhozCraig

    15 de septiembre de 2012 a las 23:13

  • Entonces, ¿esto realmente significa que incluso el código de captura de prueba también debería terminar en un error de desbordamiento de pila?

    – LPD

    4 de enero de 2013 a las 13:33

avatar de usuario
ninjalj

Cuando obtiene una excepción de la invocación de foo() dentro de tryllama foo() de finally y comienza a recurrir de nuevo. Cuando eso provoque otra excepción, llamarás foo() de otro interior finally()y así sucesivamente casi indefinidamente.

  • Presumiblemente, se envía un StackOverflowError (SOE) cuando no hay más espacio en la pila para llamar a nuevos métodos. como puedo foo() ser llamado desde finalmente después una SOE?

    – asilias

    15 de septiembre de 2012 a las 16:02

  • @assylias: si no hay suficiente espacio, volverás de lo último foo() invocación, e invocar foo() en el finally bloque de tu actual foo() invocación.

    – ninjalj

    15/09/2012 a las 16:10

  • +1 a ninjalj. No llamarás a foo desde en cualquier sitio una vez que no puede llamar a foo debido a la condición de desbordamiento. esto incluye desde el bloque finalmente, por lo que eventualmente (la era del universo) terminará.

    – WhozCraig

    16 de septiembre de 2012 a las 6:00

Intenta ejecutar el siguiente código:

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

Encontrará que el bloque finalmente se ejecuta antes de lanzar una excepción hasta el nivel superior. (Producción:

Finalmente

Excepción en el hilo “main” java.lang.Exception: ¡PRUEBA! en prueba.principal(prueba.java:6)

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

Sin embargo, en su método de barra, tan pronto como ocurre la excepción, se lanza directamente al nivel superior y se imprimirá

  • Voto negativo. “Sigue sucediendo para siempre” está mal. Ver otras respuestas.

    – jcsahnwaldt Reincorporar a Monica

    26 de julio de 2018 a las 14:48

avatar de usuario
QuiénesCraig

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

Esto intenta simular las condiciones de lo que sucede cuando una invocación NO puede ocurrir porque introduciría un desbordamiento de pila. Me parece que lo más difícil que la gente está fallando en comprender es que la invocación no ocurre cuando no poder 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("[email protected]("+n+")");
        }
        finally
        {
            System.out.println(n + " - Finally");
            if (n < limit)
                foo(n+1,limit);
            else
                throw new Exception("[email protected]("+n+")");
        }
    }
}

El resultado de este pequeño montón de baba sin sentido es el siguiente, y la excepción real detectada puede ser una sorpresa; Ah, y 32 llamadas de prueba (2 ^ 5), lo cual es completamente esperado:

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: [email protected](5)

avatar de usuario
karoly horvath

Aprende a rastrear tu 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, StackOverFlow se arroja en algunas capas superiores, por lo que puede realizar pasos de recursión adicionales hasta que encuentre otra excepción, y así sucesivamente. Este es un “bucle” infinito.

  • en realidad no es un bucle infinito, si eres lo suficientemente paciente, eventualmente terminará. Sin embargo, no aguantaré la respiración por ello.

    – miente ryan

    15 de septiembre de 2012 a las 17:19


  • Yo diría que es infinito. Cada vez que alcanza la profundidad máxima de la pila, lanza una excepción y desenrolla la pila. Sin embargo, finalmente vuelve a llamar a Foo, lo que hace que vuelva a utilizar el espacio de pila que acaba de recuperar. Avanzará y retrocederá arrojando excepciones y luego retrocederá hasta que vuelva a suceder. Siempre.

    – Kibbee

    15 de septiembre de 2012 a las 18:24


  • Además, querrá que el primer system.out.println esté en la declaración de prueba, de lo contrario, desenrollará el bucle más de lo que debería. posiblemente haciendo que se detenga.

    – Kibbee

    15/09/2012 a las 18:38

  • @Kibbee El problema con su argumento es que cuando llama foo la segunda vez, en el finally bloque, ya no está en un try. Entonces, mientras vuelve a bajar en la pila y crea más desbordamientos de pila una vez, la segunda vez simplemente volverá a generar el error producido por la segunda llamada a fooen lugar de volver a profundizar.

    – amalgama

    20 de agosto de 2015 a las 2:48

avatar de usuario
Vitruvie

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 de pila disponible y luego llama fooy finalmente escribe un rastro de lo sucedido:

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 ¡pruébalo en línea! (Algunas ejecuciones pueden llamar foo más o menos veces que otros)

  • en realidad no es un bucle infinito, si eres lo suficientemente paciente, eventualmente terminará. Sin embargo, no aguantaré la respiración por ello.

    – miente ryan

    15 de septiembre de 2012 a las 17:19


  • Yo diría que es infinito. Cada vez que alcanza la profundidad máxima de la pila, lanza una excepción y desenrolla la pila. Sin embargo, finalmente vuelve a llamar a Foo, lo que hace que vuelva a utilizar el espacio de pila que acaba de recuperar. Avanzará y retrocederá arrojando excepciones y luego retrocederá hasta que vuelva a suceder. Siempre.

    – Kibbee

    15 de septiembre de 2012 a las 18:24


  • Además, querrá que el primer system.out.println esté en la declaración de prueba, de lo contrario, desenrollará el bucle más de lo que debería. posiblemente haciendo que se detenga.

    – Kibbee

    15/09/2012 a las 18:38

  • @Kibbee El problema con su argumento es que cuando llama foo la segunda vez, en el finally bloque, ya no está en un try. Entonces, mientras vuelve a bajar en la pila y crea más desbordamientos de pila una vez, la segunda vez simplemente volverá a generar el error producido por la segunda llamada a fooen lugar de volver a profundizar.

    – amalgama

    20 de agosto de 2015 a las 2:48

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad