Diagnostic et résolution de StackOverflowError

Un récent message du forum de la communauté JavaWorld (Stack Overflow après instanciation d'un nouvel objet) m'a rappelé que les bases de StackOverflowError ne sont pas toujours bien comprises par les nouveaux utilisateurs de Java. Heureusement, StackOverflowError est l'une des erreurs d'exécution les plus faciles à déboguer et dans cet article de blog, je montrerai à quel point il est souvent facile de diagnostiquer une StackOverflowError. Notez que le potentiel de débordement de pile n'est pas limité à Java.

Le diagnostic de la cause d'un StackOverflowError peut être assez simple si le code a été compilé avec l'option de débogage activée afin que les numéros de ligne soient disponibles dans la trace de pile résultante. Dans de tels cas, il s'agit généralement simplement de trouver le motif répétitif des numéros de ligne dans la trace de pile. Le modèle de répétition des numéros de ligne est utile car une StackOverflowError est souvent causée par une récursivité non terminée. Les numéros de ligne répétitifs indiquent le code qui est appelé directement ou indirectement de manière récursive. Notez qu'il existe des situations autres que la récursivité illimitée dans lesquelles un débordement de pile peut se produire, mais cette publication de blog est limitée à StackOverflowErrorcausée par une récursivité illimitée.

La relation de récursivité qui a mal tourné StackOverflowErrorest notée dans la description Javadoc de StackOverflowError qui indique que cette erreur est «levée lorsqu'un débordement de pile se produit car une application récursent trop profondément». Il est significatif qui StackOverflowErrorse termine par le mot Error et est une erreur (étend java.lang.Error via java.lang.VirtualMachineError) plutôt qu'une exception vérifiée ou d'exécution. La différence est significative. Les Erroret Exceptionsont chacun un jetable spécialisé, mais leur manipulation prévue est assez différente. Le didacticiel Java souligne que les erreurs sont généralement externes à l'application Java et ne peuvent donc et ne doivent normalement pas être détectées ou gérées par l'application.

Je vais montrer StackOverflowErrorcomment se heurter à une récursivité illimitée avec trois exemples différents. Le code utilisé pour ces exemples est contenu dans trois classes, dont la première (et la classe principale) est affichée ensuite. Je liste les trois classes dans leur intégralité car les numéros de ligne sont significatifs lors du débogage du StackOverflowError.

StackOverflowErrorDemonstrator.java

package dustin.examples.stackoverflow; import java.io.IOException; import java.io.OutputStream; /** * This class demonstrates different ways that a StackOverflowError might * occur. */ public class StackOverflowErrorDemonstrator { private static final String NEW_LINE = System.getProperty("line.separator"); /** Arbitrary String-based data member. */ private String stringVar = ""; /** * Simple accessor that will shown unintentional recursion gone bad. Once * invoked, this method will repeatedly call itself. Because there is no * specified termination condition to terminate the recursion, a * StackOverflowError is to be expected. * * @return String variable. */ public String getStringVar() { // // WARNING: // // This is BAD! This will recursively call itself until the stack // overflows and a StackOverflowError is thrown. The intended line in // this case should have been: // return this.stringVar; return getStringVar(); } /** * Calculate factorial of the provided integer. This method relies upon * recursion. * * @param number The number whose factorial is desired. * @return The factorial value of the provided number. */ public int calculateFactorial(final int number) { // WARNING: This will end badly if a number less than zero is provided. // A better way to do this is shown here, but commented out. //return number <= 1 ? 1 : number * calculateFactorial(number-1); return number == 1 ? 1 : number * calculateFactorial(number-1); } /** * This method demonstrates how unintended recursion often leads to * StackOverflowError because no termination condition is provided for the * unintended recursion. */ public void runUnintentionalRecursionExample() { final String unusedString = this.getStringVar(); } /** * This method demonstrates how unintended recursion as part of a cyclic * dependency can lead to StackOverflowError if not carefully respected. */ public void runUnintentionalCyclicRecusionExample() { final State newMexico = State.buildState("New Mexico", "NM", "Santa Fe"); System.out.println("The newly constructed State is:"); System.out.println(newMexico); } /** * Demonstrates how even intended recursion can result in a StackOverflowError * when the terminating condition of the recursive functionality is never * satisfied. */ public void runIntentionalRecursiveWithDysfunctionalTermination() { final int numberForFactorial = -1; System.out.print("The factorial of " + numberForFactorial + " is: "); System.out.println(calculateFactorial(numberForFactorial)); } /** * Write this class's main options to the provided OutputStream. * * @param out OutputStream to which to write this test application's options. */ public static void writeOptionsToStream(final OutputStream out) { final String option1 = "1. Unintentional (no termination condition) single method recursion"; final String option2 = "2. Unintentional (no termination condition) cyclic recursion"; final String option3 = "3. Flawed termination recursion"; try { out.write((option1 + NEW_LINE).getBytes()); out.write((option2 + NEW_LINE).getBytes()); out.write((option3 + NEW_LINE).getBytes()); } catch (IOException ioEx) { System.err.println("(Unable to write to provided OutputStream)"); System.out.println(option1); System.out.println(option2); System.out.println(option3); } } /** * Main function for running StackOverflowErrorDemonstrator. */ public static void main(final String[] arguments) { if (arguments.length < 1) { System.err.println( "You must provide an argument and that single argument should be"); System.err.println( "one of the following options:"); writeOptionsToStream(System.err); System.exit(-1); } int option = 0; try { option = Integer.valueOf(arguments[0]); } catch (NumberFormatException notNumericFormat) { System.err.println( "You entered an non-numeric (invalid) option [" + arguments[0] + "]"); writeOptionsToStream(System.err); System.exit(-2); } final StackOverflowErrorDemonstrator me = new StackOverflowErrorDemonstrator(); switch (option) { case 1 : me.runUnintentionalRecursionExample(); break; case 2 : me.runUnintentionalCyclicRecusionExample(); break; case 3 : me.runIntentionalRecursiveWithDysfunctionalTermination(); break; default : System.err.println("You provided an unexpected option [" + option + "]"); } } } 

La classe ci-dessus démontre trois types de récursivité illimitée: récursivité accidentelle et complètement non intentionnelle, récursivité involontaire associée à des relations intentionnellement cycliques et récursivité intentionnelle avec une condition de terminaison insuffisante. Chacun de ceux-ci et leurs résultats sont discutés ensuite.

Récursivité complètement involontaire

Il peut y avoir des moments où la récursivité se produit sans aucune intention. Une cause commune peut être le fait qu'une méthode s'appelle accidentellement. Par exemple, il n'est pas trop difficile d'être un peu trop imprudent et de sélectionner la première recommandation d'un IDE sur une valeur de retour pour une méthode "get" qui pourrait finir par être un appel à cette même méthode! C'est en fait l'exemple montré dans la classe ci-dessus. La getStringVar()méthode s'appelle elle-même à plusieurs reprises jusqu'à ce que le StackOverflowErrorsoit rencontré. La sortie apparaîtra comme suit:

Exception in thread "main" java.lang.StackOverflowError at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at 

La trace de pile montrée ci-dessus est en fait plusieurs fois plus longue que celle que j'ai placée ci-dessus, mais c'est simplement le même motif répétitif. Comme le modèle se répète, il est facile de diagnostiquer que la ligne 34 de la classe est à l'origine du problème. Quand nous regardons cette ligne, nous voyons que c'est bien la déclaration return getStringVar()qui finit par s'appeler à plusieurs reprises. Dans ce cas, on peut vite se rendre compte que le comportement prévu était plutôt le cas return this.stringVar;.

Récursion involontaire avec relations cycliques

Il existe certains risques à avoir des relations cycliques entre les classes. L'un de ces risques est la plus grande probabilité de rencontrer une récursion involontaire où les dépendances cycliques sont continuellement appelées entre les objets jusqu'à ce que la pile déborde. Pour démontrer cela, j'utilise deux autres classes. La Stateclasse et la Cityclasse ont une relation cyclique car une Stateinstance a une référence à sa capitale Cityet a Citya une référence à la Statedans laquelle elle se trouve.

State.java

package dustin.examples.stackoverflow; /** * A class that represents a state and is intentionally part of a cyclic * relationship between City and State. */ public class State { private static final String NEW_LINE = System.getProperty("line.separator"); /** Name of the state. */ private String name; /** Two-letter abbreviation for state. */ private String abbreviation; /** City that is the Capital of the State. */ private City capitalCity; /** * Static builder method that is the intended method for instantiation of me. * * @param newName Name of newly instantiated State. * @param newAbbreviation Two-letter abbreviation of State. * @param newCapitalCityName Name of capital city. */ public static State buildState( final String newName, final String newAbbreviation, final String newCapitalCityName) { final State instance = new State(newName, newAbbreviation); instance.capitalCity = new City(newCapitalCityName, instance); return instance; } /** * Parameterized constructor accepting data to populate new instance of State. * * @param newName Name of newly instantiated State. * @param newAbbreviation Two-letter abbreviation of State. */ private State( final String newName, final String newAbbreviation) { this.name = newName; this.abbreviation = newAbbreviation; } /** * Provide String representation of the State instance. * * @return My String representation. */ @Override public String toString() { // WARNING: This will end badly because it calls City's toString() // method implicitly and City's toString() method calls this // State.toString() method. return "StateName: " + this.name + NEW_LINE + "StateAbbreviation: " + this.abbreviation + NEW_LINE + "CapitalCity: " + this.capitalCity; } } 

Ville.java