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