Naviguez dans le code avec le flux d’exécution
Est-ce que vous avez déjà suivi une recette de cuisine pas à pas ? Après chaque étape, on s’arrête généralement jusqu’à être prêt à effectuer l’étape suivante. Parfois, on repère une occasion d’improviser et de changer la recette, mais, dans l’ensemble, on suit la recette et on obtient un gâteau ! 🍰
Lorsque vous parcourez pas à pas les lignes de votre programme dans un débugger, c’est tout à fait pareil. Vous suivez une recette déterminée pas à pas. La recette est votre code original. Néanmoins, lorsque vous choisissez step over (enjamber) et inspectez chaque ligne en action, vous pouvez le changer un peu, modifier les variables, et appeler des instructions. La modification des variables pendant que votre programme s’exécute peut vous aider à tester des théories et à cibler votre bug plus précisément !
Comment puis-je contrôler l’exécution de mon programme avec une telle précision ?
Vous utilisez le volet du débugger pour contrôler le flux d’exécution. Explorons-le plus en détail et voyons la palette d’options disponibles. 🎨
Détaillons cela :
Evaluate Expression
vous permet d’exécuter tout extrait arbitraire de code Java depuis le débugger, fonctionnant comme s’il précédait votre point d’arrêt actuel. Cela vous sera utile lorsque vous voudrez essayer des modifications de votre code à un point d’arrêt, car la source n’est pas modifiée.Run To Cursor
fait cesser la suspension du débugger et s’arrête à la ligne où se trouve actuellement votre curseur. Cela correspond à ajouter un point d’arrêt pour la ligne en cours et cliquer sur Resume (reprendre). Lorsque vous êtes arrêté à un point d’arrêt et que vous lisez votre code, c’est une très bonne façon de crier « par ici ! » et de faire en sorte que le débugger vous rattrape. Cela évite de créer de nouveaux points d’arrêt que vous devrez gérer par la suite !Drop Frame
rembobine votre programme jusqu’au point avant l’appel de la méthode en cours. Cela vous donne le pouvoir de voyager dans le temps lorsque vous vous êtes arrêté à un point d’arrêt et que vous avez manqué un détail, ou si vous décidez que vous avez envie de repasser l’incident après avoir repositionné vos preuves avec Evaluate Expression.Step Out
reprend l’exécution et revient jusqu’à l’appel pour continuer. Si vous enquêtez sur une méthode et que vous avez fini d’explorer tout ce qui est pertinent pour votre enquête, cela vous permet de revenir à l’appel et de continuer à débugger à partir du point après la complétion de la méthode, vous évitant l’inconvénient de devoir aller plus loin.Force Step Into
est commeStep Into
(ci-dessous). Il reprend l’exécution et la suspend immédiatement dans une méthode que vous n’avez normalement pas besoin de débugger comme les classes, les constructeurs, les getters et setters du langage Java. Cela vous permet d’entrer dans le JDK lui-même. Habituellement, nous faisons confiance à Java et nous n’avons pas besoin de débugger le JDK. Néanmoins, il y a des moments où vous pourriez avoir mal compris la Javadoc ou émis des suppositions erronées sur le rôle d’une partie du framework. Cela peut être inestimable et vous en apprendre plus sur les bibliothèques centrales de Java.Step Into
reprend l’exécution depuis un point d’arrêt sur un appel de méthode. Il suspend à nouveau à la première ligne de la méthode. Cela vous permet de vous déplacer directement dans cette méthode pour enquêter sur ce qu’elle fait et comment elle gère les arguments que vous lui passez.Step Over
reprend l’exécution du point d’arrêt en cours, et suspend à nouveau à la prochaine déclaration dans le fichier en cours (autrement dit, à la prochaine ligne valide). Une fois que vous avez suspendu à une ligne particulière, cela vous permet de continuer à suspendre à chaque ligne qui suit. SansStep Over
, votre IDE serait plein de points d’arrêt toutes les deux lignes. Vous pouvez utiliser ceci pour comprendre le flux de votre code, pas à pas !Show Execution Point
ramène l’éditeur de code au point d’arrêt actuel. Cela peut être utile si vous commencez à explorer votre code et avez oublié où le débugger s’est arrêté. Il est très facile de se perdre dans votre IDE lorsque vous lisez un code complexe ! Pensez-y comme à un moyen de vous téléporter 🕴️ en arrière jusqu’à la ligne de code où vous avez suspendu l’exécution !
Nous allons maintenant utiliser ce contrôle comme une télécommande de notre machine à remonter le temps, pour mieux comprendre pourquoi nous obtenons une taille de selle négative.
Mais est-ce que je ne pourrais pas simplement spécifier tout un tas de points d’arrêt et cliquer sur Reprendre ?
Bien que rien ne vous empêche de placer des points d’arrêt sur toutes les lignes intéressantes d’un fichier source, cela peut être lent à mettre en place et vous forcer à inspecter inutilement des déclarations dans votre code. Vous pourriez vous retrouver à devoir examiner des appels de méthodes et des déclarations qui n’ont rien à voir avec votre bug. Des examens que vous pourriez facilement éviter.
Lorsque vous ne connaissez pas la cause d’un bug, cela peut ressembler à l’exploration d’une nouvelle ville. L’utilisation des contrôles de flux d’exécution vous permet de vous promener dans votre code. Exactement comme lorsque, en vous promenant dans une ville, vous vous arrêtez lorsque vous voyez quelque chose d’intéressant ! 🚶🏿♂️ C’est généralement votre meilleure piste lorsque vous marchez dans des rues inconnues sans avoir de plan.
Voyons comment nous pouvons utiliser les flux de contrôle pour enquêter encore davantage sur notre bug.
Utilisez les flux de contrôle pour enquêter sur un bug
Notre enquête nous a aidés à éliminer les causes suivantes :
un problème avec le calcul de la date quand aucune date n’est passée ;
une modification erronée du paramètre de la date ;
un problème dans la classe
DragonSaddleSizeVerifier
levant l’exception ;une condition dans la boucle for qui ajuste la taille de selle de façon négative pendant qu’elle est calculée.
La dernière preuve que nous ayons vue est que la méthodeDragonSaddleSizeEstimator::calculateSaddleSizeFromYear
calcule une variable nomméemysticalMultiplier
et lui donne une valeur négative singulière. Quand nous utilisons cette valeur pour calculer la taille de la selle, l’estimation qui en résulte est la même que celle que nous avons vue dans notre exception qui bugge : -49.
🕵️♀️ Donc, notre prochaine théorie est que le mysticalMultiplier est calculé différemment quand une date cible est passée depuis la ligne de commande, contrairement à ce qui se passe quand elle est définie explicitement dans notre test. Utilisons nos outils pour comprendre comment le mysticalMultiplier est calculé.
Eh bien, c’était intéressant. Passons les preuves en revue :
UNIVERSAL_LUCKY_NUMBER
est une constante qui semble être définie sans aspect suspect ;mysticalMultiplier
est calculé en utilisantcopyOfUniversalConstant
,yearOfBirth
, etUNIVERSAL_LUCKY_NUMBER
;copyOfUniversalConstant
semble être défini à zéro avant d’être utilisé dans une multiplication. Cela semble suspect, car cela rend la multiplication redondante.
Le calcul de mysticalMultiplier a lieu dans la méthodecalculateSaddleSizeFromYear(int targetYear)
private double calculateSaddleSizeFromYear(int targetYear) {
// ((42-1)/41.0)
double universalLuckyNumber = new Double(UNIVERSAL_LUCKY_NUMBER);
double mysticalMultiplier = (copyOfUniversalConstant - yearOfBirth)/ universalLuckyNumber;
...
}
Étant donné que nous avons déjà des tests unitaires et d’intégration pour vérifier que nous pouvons calculer une taille de selle correcte, nous pouvons comparer les deux tests. Faisons-le en explorant l’un des tests unitaires existants qui réussissent et qui sont en rapport avec notre test d’intégration qui échoue. Si vous repensez au début de notre enquête, nous avons déjà plusieurs tests unitaires qui semblent passer. Nous n’avons jamais établi pourquoi ces tests réussissent !
Encore autre chose : qu’est-ce que cettecopyOfUniversalConstant
est censée être ? Heureusement, nous avons l’exemple d’un test que nous pouvons débugger et qui nous montre ce quecopyOfUniversalConstant
devrait être.
Nous allons débugger un test JUnit qui réussit, qui calcule une taille de selle en 2021, mais nous allons utiliser la fonctionnalitéEvaluate Expression
pour changer la définition de l’année cible de 2021 à 2020, pendant l’exécution. Nous allons aussi nous promener dans le code et utiliserRun to Cursor
pour casser le code à des endroits arbitraires. Notre but est de découvrir :
quelle est la valeur de
copyOfUniversalConstant
quand elle fonctionne ;ce qui se passe si nous définissons
copyOfUniversalConstant
à 0 (zéro), comme c’était le cas quand nous avons débuggé le test d’intégration qui échouait.
Lançons-nous et fouillons dans notre code :
Avez-vous vu ce qu’il s’est passé ici ?
Nous avons pris un test existant et l’avons utilisé pour qu’il nous amène à une instance de
DragonSaddleSizeEstimator
bien configurée !Une fois dans la méthode
estimateSaddleSizeInCentiMeters(targetYear)
, nous avons utilisé les contrôles du débugger pour modifier le comportement du code, et tester nos méthodes avec différents paramètres et différentes valeurs définis dans les champs duDragonSaddleSizeEstimator
.Nous avons utilisé « drop frame » pour annuler notre appel à
calculateSaddleSizeFromYear(targetYear)
, pour pouvoir recommencer du début. Nous étions essentiellement en train de voyager dans le temps vers le passé et vers l’avenir dans notre code, et essayions de voir comment différents états de départ affectaient l’avenir. Tout cela sans avoir à redémarrer notre JVM !
Qu’est-ce que cela nous dit au sujet de notre bug ?
Nous avons appris que la différence clé entre une exécution valide du calculateur de taille de selle et une exécution cassée est la suivante : dans une exécution cassée, nous avons le champ de la constante copyOfUniversalConstant
du DragonSaddleSizeEstimator
défini à 0, plutôt qu’à 42.
Découvrons davantage d’outils de débug que vous pouvez utiliser pour déterminer d’où vient cette différence.
Observez des valeurs qui changent avec des watches et des watchpoints
Est-ce que vous avez déjà suivi quelqu’un sur un média social ? Les gros sites de médias sociaux hébergent des millions et des milliards de comptes des quatre coins du monde. Avec un tel nombre de personnes partageant des mises à jour quotidiennes, comment suivre les personnes qui vous intéressent ? Qu’il s’agisse d’une célébrité ou de votre grand-tante, cela reste la mise à jour d’une seule personne parmi un flux incroyablement vaste. Si vous êtes un utilisateur averti, vous savez que vous n’avez qu’à simplement suivre quelqu’un.
Votre code est rempli de nombreuses variables que vous utilisez pour vous aider à façonner le monde qu’il contient. À mesure que votre code satisfait à différents résultats, certaines de ces variables changent ou évoluent. Si vous essayez de trouver la cause d’un bug, il peut être utile de suivre des mises à jour de variables clés, comme vous le feriez pour votre grand-tante, cette célébrité Instagram. 👵📱 Ceci peut vous aider à vérifier si une mise à jour non intentionnelle a introduit un défaut dans votre logiciel.
Heureusement, votre débugger vous permet de surveiller des variables spécifiques dans votre code, et de les observer pendant leur évolution.
Comment décider quelles variables je devrais suivre ?
Cette décision doit venir de votre enquête, étant donné que les variables changeantes font partie de la façon dont fonctionne le logiciel. Vous devez cibler votre enquête sur les variables qui impactent visiblement le comportement dans votre bug. Par exemple, la variable copyOfUniversalConstant
mentionnée précédemment serait une bonne candidate. Elle impacte directement notre résultat, et nous avons vu qu’elle a une valeur suspecte. Une valeur qui pourrait contribuer à notre bug !
Watches
Alors, comment surveiller une variable ? Lorsque vous êtes dans la boîte à outils de débug, sélectionnez n’importe quelle variable dans votre volet Variables, cliquez droit, et cliquez sur Add to Watches.
Vous pouvez également supprimer tous les watches en utilisant l’élément tout en haut de ce menu. Après avoir ajouté une variable à vos watches, vous la verrez toujours dans votre volet Variables avec une paire de lunettes à côté d’elle. Ainsi, vous pouvez garder à l’œil tous les changements qui lui sont apportés.
Notez que targetYear a des lunettes à côté.
Watchpoints
Est-ce que je peux mettre le débugger sur pause quand une variable change ?
Si la variable que vous surveillez est un champ dans une classe, vous pouvez y ajouter un watchpoint, et le débugger stoppera et s’arrêtera automatiquement sur toute ligne qui modifie ce champ. Vous pouvez ajouter un watchpoint avec votre IDE en cliquant sur la marge à côté d’une déclaration de champ, comme vous le feriez pour ajouter un point d’arrêt à tout autre endroit. Cela affichera un œil rouge, contrairement au cercle rouge que vous avez vu jusqu’à présent !
Le clic droit là-dessus ouvre un dialogue similaire à celui utilisé pour les points d’arrêt. Cela vous permet de cocher la case si vous voulez vous arrêter sur Field access (lecture du champ), Field modification (évolution du champ), ou les deux.
Testons une théorie. Si nous pensons qu’il y a un problème avec le champ copyOfUniversalConstant dans la classe DragonSaddleSizeEstimator, comment pourrions-nous garder un œil sur les changements à copyOfUniversalConstant ? Nous allons y placer un watchpoint et découvrir ce qui le modifie :
Vous avez vu la façon dont nous avons utilisé un watchpoint pour nous arrêter sur toute déclaration Java qui modifiait la valeur du champcopyOfUniversalConstant
? Et maintenant, regardons les preuves ensemble.
En plaçant un watchpoint surcopyofUniversalConstant
, nous avons appris que :
copyOfUniversalConstant
est initialement définie dans le constructeur deDragonSaddleSizeEstimator
;elle est définie depuis une variable statique, une constante, nommée
UNIVERSAL_CONSTANT
;au point de définition de
copyOfUniversalConstant
,UNIVERSAL_CONSTANT
a une valeur de 0 ;UNIVERSAL_CONSTANT
est la deuxième déclaration dansDragonSaddleSizeEstimator
et est codée en dur à 42 ;la variable statique
INSTANCE
est déterminée juste avant la déclaration deUNIVERSAL_CONSTANT
;UNIVERSAL_CONSTANT
était utilisée dans le constructeur avant qu’on ne lui attribue la valeur de 42.
Élémentaire. 🕵🏽 Il semble logique qu’en raison d’un problème d’ordre, UNIVERSAL_CONSTANT
soit utilisée dans une invocation du constructeur avant d’être définie. Conclusion, une variable statique est utilisée avant d’être définie.
Et maintenant, testons notre nouvelle théorie. Les quelques premières lignes de notre DragonSaddleSizeEstimator ressemblent à ceci :
public class DragonSaddleSizeEstimator {
// Singleton instance of the Dragon Size Estimator
public static final DragonSaddleSizeEstimator INSTANCE = new DragonSaddleSizeEstimator();
/**
* The universal constant which is 42.
*/
public static int UNIVERSAL_CONSTANT = 42;
// The year when dragons were first spawned on Earth in 1 AD
public static final int DRAGON_SPAWN_YEAR = 1;
// Private fields
private int copyOfUniversalConstant;
private int yearOfBirth;
private DragonSaddleSizeVerifier verifier;
/**
* Constructor
**/
public DragonSaddleSizeEstimator() {
copyOfUniversalConstant = UNIVERSAL_CONSTANT;
yearOfBirth = DRAGON_SPAWN_YEAR;
...
}
....
}
Ligne 4 : l’attribution d’instance appelle le constructeur à la ligne 22.
Ligne 9 :
UNIVERSAL_CONSTANT
est définie à 42 après la création d’une instance.Ligne 12 :
DRAGON_SPAWN_YEAR
est défini à 1 après la création de l’instance.Ligne 23 : la première fois que le constructeur a été appelé à la ligne 4,
UNIVERSAL_CONSTANT
n’avait même pas été définie. Java initialise par défaut ce type de valeur d’entier à 0.Ligne 24 : la première fois que le constructeur a été appelé à la ligne 4,
DRAGON_SPAWN_YEAR
n’avait pas encore été défini. À nouveau, Java a attribué 0 par défaut. Même si c’était un 0 !
Vous voulez dire que la variable finale à la ligne 12 a été utilisée avant d’être définie ? Les finales ne peuvent pas être modifiées !
Bien observé. Vous avez raison. C’est généralement le cas, mais en raison du flux de contrôle Java pour les entités statiques, ce n’est pas le cas lorsqu’il s’agit de champs statiques. Java commence par scanner pour trouver tous les champs statiques et les crée avec des valeurs par défaut. Dans ce cas, int
a été mis à 0 par défaut. Ensuite, il les attribue et les exécute dans leur ordre d’occurrence dans le code. Cela signifie que la ligne 4 est attribuée en premier.
L’attribution initiale de copyOfUniversalConstant=UNIVERSAL_CONSTANT
est effectuée avant que nous ayons l’occasion de redéfinir UNIVERSAL_CONSTANT
du 0 par défaut à 42.
Oui, Java exploserait et se plaindrait dans un monde idéal. Mais il ne le fait pas dans celui-ci. C’est une particularité du langage, qui, comme vous l’avez vu ici, peut facilement apparaître et vous surprendre. Elle ne fait pas ce à quoi vous vous attendez !
Quelle est la solution ?
Déplacez simplement la ligne 4 après les lignes 9 et 12. Ainsi, les variables statiques auront été définies avant d’être utilisées. Mais comment prouver cette théorie ? Comme toute autre théorie ; nous devons la tester. Regardons à nouveau le début de la classeDragonSaddleSizeEstimator
, avec les commentaires un peu nettoyés :
public class DragonSaddleSizeEstimator {
/**
* Singleton instance of the Dragon Size Estimator
**/
// Makes use of the next two defined static variables
public static final DragonSaddleSizeEstimator INSTANCE new DragonSaddleSizeEstimator();
/**
* The universal constant which is 42.
*/
// FIXME this isn't a constant until you add final
public static int UNIVERSAL_CONSTANT = 42;
/**
* The year when dragons were first spawned on Earth in 1 AD
**/
public static final int DRAGON_SPAWN_YEAR = 1;
...
}
Étant donné que la première variable statique dans le code dépend des deux qui la suivent, la solution impliquera un remaniement, la déplaçant sous la variable à la ligne 18. Ce remaniement est fait car le constructeur actuellement appelé à la ligne 7 appellera ces deux valeurs.
Si vous appliquez la bonne solution, votre test d’intégration devrait commencer à réussir. Effectuons-le ensemble.
Réparez le bug
Nous allons exécuter le test d’intégration en premier et nous rafraîchir la mémoire quant au bug original dans le programme. Nous voulons aussi découvrir pourquoi il y a cet étrange moyen de contournement qui consiste à passer une année comme argument au programme. Nous pourrons ensuite cibler la cause sous-jacente et la réparer. Réunissons tous les suspects dans la bibliothèque et identifions ce bug !
Vous avez remarqué la façon dont la méthode principale avait été forcée de fonctionner quand on lui passait un argument ? Cela s’appelle un setter, qui a redéfini copyOfUniversalConstant
. Les constantes ne sont pas censées changer ; cette copie n’existe que pour être redéfinie.
L’utilisation de ce type de réparation revient à mettre du scotch sur votre code. Ce n’est pas une solution à long terme, en particulier car elle ne répare pas la classe DragonSaddleSizeEstimator
. Si nous écrivions une autre classe qui avait besoin d’utiliser le DragonSaddleSizeEstimator
, nous aurions à appliquer le même scotch partout. Le problème sous-jacent continuerait d’exister dans le code. À la place, nous avons résolu le problème en déplaçant la déclaration d’instance vers le bas, là où nous avons défini les variables dont elle dépend.
Notre débugger nous a donné une loupe sous stéroïdes, avec laquelle nous avons pu résoudre ce mystère. Le code doit être nettoyé, et heureusement, cela fonctionne, avec des tests. Qui que ce soit qui nettoie le code aura l’assurance qu’il ne le casse pas davantage.
Essayez par vous-même !
La réparation est sur la branche nommée bug-fix-1 :
Regardez cette branche et exécutez le programme par vous-même.
Regardez bug-fix-1 dans Git, ou dans IntelliJ
VCS
->git
->branches
->origin/bug-fix-1
->Checkout As
Essayez de définir un watchpoint sur
copyOfUniversalConstant
et voyez d’où on y accède et la modifie.
Est-ce qu’on dirait que nous l’avons réparé ?
En résumé
Les contrôles de flux d’exécution de votre débugger vous permettent de :
Show Execution Point
Rafraîchir l'éditeur pour afficher votre point d’arrêt actuel.Step Over
Exécuter la commande au point d’arrêt actuel et suspendre la ligne suivante.Step In
Entrer dans la méthode sur laquelle vous avez un point d’arrêt et vous arrêter dedans.Force Step In
Entrer dans une méthode du JDK comme new Double() ).Step Out
Aller jusqu’au bout de la méthode en cours et suspendre tout de suite après l’élément qui appelle.Drop Frame
Revenir à l’élément qui appelle cette méthode, comme si cette méthode n’avait jamais été appelée ; annuler tout changement.Run to Cursor
Reprendre depuis ce point d’arrêt et exécuter toutes les déclarations, vous arrêtant à nouveau lorsque vous atteignez la ligne sur laquelle se trouve actuellement le curseur.Evaluate Expression
Exécuter toute affirmation Java que vous souhaitez. Vous pouvez voir des valeurs et tester de potentiels changements au code sans changer le code source !
Watches est une liste de variables utile à épingler à votre volet Variables. Cela vous permet de garder un œil sur tout changement.
Les watchpoints sont des variables que non seulement vous surveillez, mais que vous avez également configurées pour déclencher des points d’arrêt sur toute déclaration qui tente d’y accéder ou de les modifier. Vous pouvez utiliser ceci pour trouver la partie du code qui ne définit pas correctement l’une d’entre elles.
Vous avez exploré les techniques et outils permettant d'enquêter sur un bug. Passez au quiz !