web-dev-qa-db-fra.com

Le blocage try-finally empêche StackOverflowError

Examinez les deux méthodes suivantes:

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

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

Exécuter bar() donne clairement un StackOverflowError, mais exécuter foo() ne fonctionne pas (le programme semble simplement s'exécuter indéfiniment). pourquoi est-ce?

326
arshajii

Ça ne court pas éternellement. Chaque débordement de pile entraîne le déplacement du code vers le bloc finally. Le problème est que cela prendra très, très longtemps. L'ordre du temps est O (2 ^ N), N étant la profondeur maximale de la pile.

Imaginez que la profondeur maximale est de 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()

Travailler chaque niveau dans le bloc final prend deux fois plus de temps et la profondeur de la pile peut être de 10 000 ou plus. Si vous pouvez effectuer 10 000 000 d'appels par seconde, cela prendra 10 ^ 3003 secondes ou plus longtemps que l'âge de l'univers.

328
Peter Lawrey

Lorsque vous obtenez une exception à l'invocation de foo() à l'intérieur de try, vous appelez foo() à partir de finally et recommencez la procédure de récursion. Lorsque cela provoque une autre exception, vous appelez foo() depuis un autre finally() intérieur, et ainsi de suite presque à l'infini.

40
ninjalj

Essayez d'exécuter le code suivant:

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

Vous constaterez que le bloc finally s'exécute avant de lancer une exception jusqu'au niveau supérieur. (Sortie:

Finalement

Exception dans le fil "principal" Java.lang.Exception: TEST! à test.main (test.Java:6)

Cela a du sens, comme on l’appelle enfin juste avant de quitter la méthode. Cela signifie toutefois qu’une fois que vous aurez obtenu ce premier StackOverflowError, il essaiera de le lancer, mais il devra enfin s’exécuter d’abord, de sorte qu’il relancera foo(), ce qui provoquera un autre débordement de pile, et en tant que tel fonctionne enfin à nouveau. Cela se produit éternellement, de sorte que l'exception n'est jamais imprimée.

Cependant, dans votre méthode de barre, dès que l'exception se produit, elle est simplement projetée directement au niveau supérieur et sera imprimée.

38
Alex Coleman

Afin de fournir une preuve raisonnable que cette volonté se terminera, j'offre le code suivant, plutôt insignifiant. Remarque: Java n’est PAS ma langue, ni par l’imaginaire le plus vif. Je ne présente cela que pour appuyer la réponse de Peter, qui est le réponse correcte à la question.

Cela tente de simuler les conditions de ce qui se passe lorsqu'un appel ne peut PAS arriver car cela introduirait un débordement de pile. Il me semble que la chose la plus difficile que les gens ne comprennent pas est que l'invocation ne se produit pas quand elle ne peut pas se produit.

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 sortie de ce petit tas de goo inutile est la suivante, et l'exception réelle capturée peut surprendre; Oh, et 32 ​​try-call (2 ^ 5), ce qui est tout à fait attendu:

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)
26
WhozCraig

Apprenez à suivre votre programme:

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

C'est la sortie que je vois:

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

Comme vous pouvez le constater, StackOverFlow est projeté sur certaines couches situées au-dessus. Vous pouvez donc effectuer des opérations de récursion supplémentaires jusqu'à ce que vous rencontriez une autre exception, et ainsi de suite. C'est une "boucle" infinie.

23
Karoly Horvath

Le programme semble simplement fonctionner pour toujours; elle se termine effectivement, mais plus vous disposez de beaucoup d’espace de pile de façon exponentielle. Pour prouver que cela se termine, j’ai écrit un programme qui épuise d’abord la plus grande partie de l’espace de pile disponible, puis j’ai appelé foo, puis j’ai écrit une trace de ce qui s’est passé:

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

Le code:

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

Vous pouvez essayez-le en ligne! (Certaines courses peuvent appeler foo plus ou moins de fois que d'autres)

0
Vitruvius