Vue d’ensemble

StackOverflowError Java : StackOverflowError peut être agaçant pour les développeurs Java, car il s’agit de l’une des erreurs d’exécution les plus courantes que nous pouvons rencontrer.

Dans cet article, nous verrons comment cette erreur peut se produire en examinant divers exemples de code et comment y remédier.

Cadres de pile et comment StackOverflowError se produit

Commençons par les bases. Quand une méthode est appelée, un nouveau cadre de pile est créé sur la pile d’appels. Ce cadre de pile contient les paramètres de la méthode invoquée, ses variables locales et l’adresse de retour de la méthode, c’est-à-dire le point à partir duquel l’exécution de la méthode doit continuer la méthode invoquée est retournée.

La création des cadres de pile se poursuivra jusqu’à la fin des invocations de méthodes trouvées dans les méthodes imbriquées.

  • Au cours de ce processus, si la machine virtuelle Java rencontre une situation dans laquelle il n’ya plus d’espace pour la création d’un nouveau cadre de pile, elle renvoie un StackOverflowError . ** .

La cause la plus courante pour la machine virtuelle Java de rencontrer cette situation est la récursion non terminée/infinie – la description Javadoc de StackOverflowError indique que l’erreur est renvoyée à la suite d’une récursion trop profonde dans un extrait de code particulier.

Cependant, la récursivité n’est pas la seule cause de cette erreur. Cela peut également se produire dans une situation où une application conserve l’appelant des méthodes à partir de méthodes jusqu’à ce que la pile soit épuisée . C’est un cas rare, car aucun développeur ne suivrait intentionnellement de mauvaises pratiques de codage. Une autre cause rare est d’avoir un grand nombre de variables locales dans une méthode .

StackOverflowError peut également être levé lorsqu’une application est conçue pour avoir des relations cycliques entre classes . Dans cette situation, les constructeurs les uns des autres sont appelés de manière répétée, ce qui provoque la génération de cette erreur. Cela peut aussi être considéré comme une forme de récursion.

Un autre scénario intéressant qui cause cette erreur est si une classe est en cours d’instanciation dans la même classe en tant que variable d’instance de cette classe . Ainsi, le constructeur de la même classe sera appelé à plusieurs reprises (de manière récursive), ce qui aboutira finalement à StackOverflowError.

Dans la section suivante, nous examinerons quelques exemples de code illustrant ces scénarios.

StackOverflowError en action

Dans l’exemple ci-dessous, un StackOverflowError sera renvoyé en raison d’une récursion inattendue, le développeur ayant oublié de spécifier une condition de terminaison pour le comportement récursif:

public class UnintendedInfiniteRecursion {
    public int calculateFactorial(int number) {
        return number **  calculateFactorial(number - 1);
    }
}

Ici, l’erreur est renvoyée à toutes les occasions pour toute valeur transmise à la méthode:

public class UnintendedInfiniteRecursionManualTest {
    @Test(expected = StackOverflowError.class)
    public void givenPositiveIntNoOne__whenCalFact__thenThrowsException() {
        int numToCalcFactorial= 1;
        UnintendedInfiniteRecursion uir
          = new UnintendedInfiniteRecursion();

        uir.calculateFactorial(numToCalcFactorial);
    }

    @Test(expected = StackOverflowError.class)
    public void givenPositiveIntGtOne__whenCalcFact__thenThrowsException() {
        int numToCalcFactorial= 2;
        UnintendedInfiniteRecursion uir
          = new UnintendedInfiniteRecursion();

        uir.calculateFactorial(numToCalcFactorial);
    }

    @Test(expected = StackOverflowError.class)
    public void givenNegativeInt__whenCalcFact__thenThrowsException() {
        int numToCalcFactorial= -1;
        UnintendedInfiniteRecursion uir
          = new UnintendedInfiniteRecursion();

        uir.calculateFactorial(numToCalcFactorial);
    }
}

Toutefois, dans l’exemple suivant, une condition de terminaison est spécifiée mais n’est jamais remplie si une valeur de -1 est transmise à la méthode calculateFactorial () , ce qui entraîne une récursion sans fin/infinie:

public class InfiniteRecursionWithTerminationCondition {
    public int calculateFactorial(int number) {
       return number == 1 ? 1 : number **  calculateFactorial(number - 1);
    }
}

Cet ensemble de tests illustre ce scénario:

public class InfiniteRecursionWithTerminationConditionManualTest {
    @Test
    public void givenPositiveIntNoOne__whenCalcFact__thenCorrectlyCalc() {
        int numToCalcFactorial = 1;
        InfiniteRecursionWithTerminationCondition irtc
          = new InfiniteRecursionWithTerminationCondition();

        assertEquals(1, irtc.calculateFactorial(numToCalcFactorial));
    }

    @Test
    public void givenPositiveIntGtOne__whenCalcFact__thenCorrectlyCalc() {
        int numToCalcFactorial = 5;
        InfiniteRecursionWithTerminationCondition irtc
          = new InfiniteRecursionWithTerminationCondition();

        assertEquals(120, irtc.calculateFactorial(numToCalcFactorial));
    }

    @Test(expected = StackOverflowError.class)
    public void givenNegativeInt__whenCalcFact__thenThrowsException() {
        int numToCalcFactorial = -1;
        InfiniteRecursionWithTerminationCondition irtc
          = new InfiniteRecursionWithTerminationCondition();

        irtc.calculateFactorial(numToCalcFactorial);
    }
}

Dans ce cas particulier, l’erreur aurait pu être complètement évitée si la condition de terminaison était simplement posée de la manière suivante:

public class RecursionWithCorrectTerminationCondition {
    public int calculateFactorial(int number) {
        return number <= 1 ? 1 : number **  calculateFactorial(number - 1);
    }
}

Voici le test qui montre ce scénario en pratique:

public class RecursionWithCorrectTerminationConditionManualTest {
    @Test
    public void givenNegativeInt__whenCalcFact__thenCorrectlyCalc() {
        int numToCalcFactorial = -1;
        RecursionWithCorrectTerminationCondition rctc
          = new RecursionWithCorrectTerminationCondition();

        assertEquals(1, rctc.calculateFactorial(numToCalcFactorial));
    }
}

Voyons maintenant un scénario où StackOverflowError se produit à la suite de relations cycliques entre classes. Considérons ClassOne et ClassTwo , qui s’instancient mutuellement dans leurs constructeurs, ce qui provoque une relation cyclique:

public class ClassOne {
    private int oneValue;
    private ClassTwo clsTwoInstance = null;

    public ClassOne() {
        oneValue = 0;
        clsTwoInstance = new ClassTwo();
    }

    public ClassOne(int oneValue, ClassTwo clsTwoInstance) {
        this.oneValue = oneValue;
        this.clsTwoInstance = clsTwoInstance;
    }
}
public class ClassTwo {
    private int twoValue;
    private ClassOne clsOneInstance = null;

    public ClassTwo() {
        twoValue = 10;
        clsOneInstance = new ClassOne();
    }

    public ClassTwo(int twoValue, ClassOne clsOneInstance) {
        this.twoValue = twoValue;
        this.clsOneInstance = clsOneInstance;
    }
}

Maintenant, disons que nous essayons d’instancier ClassOne comme dans ce test:

public class CyclicDependancyManualTest {
    @Test(expected = StackOverflowError.class)
    public void whenInstanciatingClassOne__thenThrowsException() {
        ClassOne obj = new ClassOne();
    }
}

Cela aboutit à un StackOverflowError puisque le constructeur de ClassOne instancie ClassTwo, et que le constructeur de ClassTwo instancie à nouveau ClassOne. Cela se produit de manière répétée jusqu’à ce qu’il déborde de la pile.

Ensuite, nous verrons ce qui se passe lorsqu’une classe est instanciée dans la même classe en tant que variable d’instance de cette classe.

Comme indiqué dans l’exemple suivant, AccountHolder s’instancie sous la forme d’une variable d’instance jointAccountHolder :

public class AccountHolder {
    private String firstName;
    private String lastName;

    AccountHolder jointAccountHolder = new AccountHolder();
}

Lorsque la classe AccountHolder est instanciée _, un StackOverflowError_ est renvoyé en raison de l’appel récursif du constructeur, comme indiqué dans ce test:

public class AccountHolderManualTest {
    @Test(expected = StackOverflowError.class)
    public void whenInstanciatingAccountHolder__thenThrowsException() {
        AccountHolder holder = new AccountHolder();
    }
}

Traiter avec StackOverflowError

La meilleure chose à faire lorsqu’un StackOverflowError est rencontré est d’inspecter la trace de la pile avec précaution pour identifier le motif de répétition des numéros de ligne. Cela nous permettra de localiser le code présentant une récursion problématique.

Examinons quelques traces de pile causées par les exemples de code que nous avons vus précédemment.

Cette trace de pile est produite par InfiniteRecursionWithTerminationConditionManualTest si nous omettons la déclaration d’exception expected :

java.lang.StackOverflowError

 at c.b.s.InfiniteRecursionWithTerminationCondition
  .calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5)
 at c.b.s.InfiniteRecursionWithTerminationCondition
  .calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5)
 at c.b.s.InfiniteRecursionWithTerminationCondition
  .calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5)
 at c.b.s.InfiniteRecursionWithTerminationCondition
  .calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5)

Ici, on peut voir la ligne numéro 5 se répéter. C’est à cet endroit que l’appel récursif est effectué. Il ne reste plus qu’à examiner le code pour voir si la récursivité est effectuée correctement.

Voici la trace de la pile obtenue en exécutant CyclicDependancyManualTest (encore une fois, sans l’exception expected ):

java.lang.StackOverflowError
  at c.b.s.ClassTwo.<init>(ClassTwo.java:9)
  at c.b.s.ClassOne.<init>(ClassOne.java:9)
  at c.b.s.ClassTwo.<init>(ClassTwo.java:9)
  at c.b.s.ClassOne.<init>(ClassOne.java:9)

Cette trace de pile indique les numéros de ligne à l’origine du problème dans les deux classes en relation cyclique. Le numéro de ligne 9 de ClassTwo et le numéro de ligne 9 de ClassOne pointent vers l’emplacement dans le constructeur où il tente d’instancier l’autre classe.

Une fois que le code est minutieusement inspecté et si aucun des éléments suivants (ou toute autre erreur de logique de code) n’est à l’origine de l’erreur:

  • Récursivité mal implémentée (c’est-à-dire sans condition de fin)
  • Dépendance cyclique entre les cours
  • Instanciation d’une classe dans la même classe en tant que variable d’instance de

cette classe

Ce serait une bonne idée d’essayer d’augmenter la taille de la pile. Selon la JVM installée, la taille de la pile par défaut peut varier.

Le drapeau -Xss peut être utilisé pour augmenter la taille de la pile, à partir de la configuration du projet ou de la ligne de commande.

Conclusion

Dans cet article, nous avons examiné de plus près le StackOverflowError , y compris en quoi le code Java peut le provoquer et comment nous pouvons le diagnostiquer et le résoudre

Laisser un commentaire