Gestion efficace des exceptions Java NullPointerException

Il ne faut pas beaucoup d'expérience en développement Java pour apprendre de première main en quoi consiste NullPointerException. En fait, une personne a souligné que la gestion de ce problème était la principale erreur commise par les développeurs Java. J'ai déjà blogué sur l'utilisation de String.value (Object) pour réduire les NullPointerExceptions indésirables. Il existe plusieurs autres techniques simples que l'on peut utiliser pour réduire ou éliminer les occurrences de ce type commun d'exception RuntimeException qui est avec nous depuis JDK 1.0. Cet article de blog rassemble et résume certaines des techniques les plus populaires.

Vérifiez que chaque objet est nul avant de l'utiliser

Le moyen le plus sûr d'éviter une exception NullPointerException est de vérifier toutes les références d'objet pour s'assurer qu'elles ne sont pas nulles avant d'accéder à l'un des champs ou méthodes de l'objet. Comme l'exemple suivant l'indique, il s'agit d'une technique très simple.

final String causeStr = "adding String to Deque that is set to null."; final String elementStr = "Fudd"; Deque deque = null; try { deque.push(elementStr); log("Successful at " + causeStr, System.out); } catch (NullPointerException nullPointer) { log(causeStr, nullPointer, System.out); } try { if (deque == null) { deque = new LinkedList(); } deque.push(elementStr); log( "Successful at " + causeStr + " (by checking first for null and instantiating Deque implementation)", System.out); } catch (NullPointerException nullPointer) { log(causeStr, nullPointer, System.out); } 

Dans le code ci-dessus, le Deque utilisé est intentionnellement initialisé à null pour faciliter l'exemple. Le code du premier trybloc ne vérifie pas la valeur null avant d'essayer d'accéder à une méthode Deque. Le code du deuxième trybloc vérifie la valeur null et instancie une implémentation de Deque(LinkedList) si elle est nulle. La sortie des deux exemples ressemble à ceci:

ERROR: NullPointerException encountered while trying to adding String to Deque that is set to null. java.lang.NullPointerException INFO: Successful at adding String to Deque that is set to null. (by checking first for null and instantiating Deque implementation) 

Le message suivant ERROR dans la sortie ci-dessus indique que a NullPointerExceptionest levé lorsqu'un appel de méthode est tenté sur la valeur null Deque. Le message suivant INFO dans la sortie ci-dessus indique qu'en vérifiant Dequed'abord la valeur null, puis en instanciant une nouvelle implémentation lorsqu'elle est nulle, l'exception a été complètement évitée.

Cette approche est souvent utilisée et, comme indiqué ci-dessus, peut être très utile pour éviter les NullPointerExceptioninstances indésirables (inattendues) . Cependant, ce n'est pas sans frais. La vérification de null avant d'utiliser chaque objet peut gonfler le code, peut être fastidieuse à écrire et ouvre davantage de place aux problèmes de développement et de maintenance du code supplémentaire. Pour cette raison, il a été question d'introduire la prise en charge du langage Java pour la détection intégrée des null, l'ajout automatique de ces vérifications pour null après le codage initial, les types à sécurité nulle, l'utilisation de la programmation orientée aspect (AOP) pour ajouter la vérification des null au code d'octet et à d'autres outils de détection de null

Groovy fournit déjà un mécanisme pratique pour traiter les références d'objet potentiellement nulles. L'opérateur de navigation sécurisée de Groovy ( ?.) retourne null plutôt que de lancer un NullPointerExceptionlors de l'accès à une référence d'objet null.

Étant donné que la vérification de null pour chaque référence d'objet peut être fastidieuse et gonfle le code, de nombreux développeurs choisissent de sélectionner judicieusement les objets à vérifier pour null. Cela conduit généralement à la vérification de null sur tous les objets d'origine potentiellement inconnue. L'idée ici est que les objets peuvent être vérifiés au niveau des interfaces exposées, puis être considérés comme sûrs après la vérification initiale.

C'est une situation où l'opérateur ternaire peut être particulièrement utile. Au lieu de

// retrieved a BigDecimal called someObject String returnString; if (someObject != null) { returnString = someObject.toEngineeringString(); } else { returnString = ""; } 

l'opérateur ternaire prend en charge cette syntaxe plus concise

// retrieved a BigDecimal called someObject final String returnString = (someObject != null) ? someObject.toEngineeringString() : ""; } 

Vérifier les arguments de méthode pour Null

La technique dont nous venons de parler peut être utilisée sur tous les objets. Comme indiqué dans la description de cette technique, de nombreux développeurs choisissent de vérifier uniquement les objets pour null lorsqu'ils proviennent de sources «non fiables». Cela signifie souvent tester la valeur null en premier dans les méthodes exposées aux appelants externes. Par exemple, dans une classe particulière, le développeur peut choisir de vérifier la valeur NULL sur tous les objets passés aux publicméthodes, mais pas de vérifier la valeur NULL dans les privateméthodes.

Le code suivant illustre cette vérification de null sur l'entrée de méthode. Il inclut une seule méthode comme méthode démonstrative qui se retourne et appelle deux méthodes, en passant à chaque méthode un seul argument nul. L'une des méthodes recevant un argument null vérifie d'abord cet argument pour null, mais l'autre suppose simplement que le paramètre transmis n'est pas nul.

 /** * Append predefined text String to the provided StringBuilder. * * @param builder The StringBuilder that will have text appended to it; should * be non-null. * @throws IllegalArgumentException Thrown if the provided StringBuilder is * null. */ private void appendPredefinedTextToProvidedBuilderCheckForNull( final StringBuilder builder) { if (builder == null) { throw new IllegalArgumentException( "The provided StringBuilder was null; non-null value must be provided."); } builder.append("Thanks for supplying a StringBuilder."); } /** * Append predefined text String to the provided StringBuilder. * * @param builder The StringBuilder that will have text appended to it; should * be non-null. */ private void appendPredefinedTextToProvidedBuilderNoCheckForNull( final StringBuilder builder) { builder.append("Thanks for supplying a StringBuilder."); } /** * Demonstrate effect of checking parameters for null before trying to use * passed-in parameters that are potentially null. */ public void demonstrateCheckingArgumentsForNull() { final String causeStr = "provide null to method as argument."; logHeader("DEMONSTRATING CHECKING METHOD PARAMETERS FOR NULL", System.out); try { appendPredefinedTextToProvidedBuilderNoCheckForNull(null); } catch (NullPointerException nullPointer) { log(causeStr, nullPointer, System.out); } try { appendPredefinedTextToProvidedBuilderCheckForNull(null); } catch (IllegalArgumentException illegalArgument) { log(causeStr, illegalArgument, System.out); } } 

Lorsque le code ci-dessus est exécuté, la sortie apparaît comme indiqué ci-dessous.

ERROR: NullPointerException encountered while trying to provide null to method as argument. java.lang.NullPointerException ERROR: IllegalArgumentException encountered while trying to provide null to method as argument. java.lang.IllegalArgumentException: The provided StringBuilder was null; non-null value must be provided. 

Dans les deux cas, un message d'erreur a été enregistré. Cependant, le cas dans lequel un null a été vérifié a jeté une IllegalArgumentException publiée qui incluait des informations de contexte supplémentaires sur le moment où la null a été rencontrée. Alternativement, ce paramètre nul aurait pu être géré de différentes manières. Pour le cas où un paramètre nul n'était pas géré, il n'y avait aucune option pour le gérer. Beaucoup de gens préfèrent lancer un NullPolinterExceptionavec les informations de contexte supplémentaires lorsqu'un null est explicitement découvert (voir l'élément n ° 60 dans la deuxième édition de Effective Java ou l'élément n ° 42 dans la première édition), mais j'ai une légère préférence pour IllegalArgumentExceptionquand il s'agit explicitement d'un argument de méthode qui est nul car je pense que l'exception même ajoute des détails de contexte et il est facile d'inclure «null» dans le sujet.

La technique de vérification des arguments de méthode pour null est en réalité un sous-ensemble de la technique plus générale de vérification de tous les objets pour null. Cependant, comme indiqué ci-dessus, les arguments des méthodes exposées publiquement sont souvent les moins fiables d'une application et leur vérification peut donc être plus importante que la vérification de l'objet moyen pour null.

La vérification des paramètres de méthode pour null est également un sous-ensemble de la pratique plus générale de vérification des paramètres de méthode pour la validité générale, comme discuté dans l'élément n ° 38 de la deuxième édition de Java efficace (élément 23 de la première édition).

Considérez les primitives plutôt que les objets

Je ne pense pas que ce soit une bonne idée de sélectionner un type de données primitif (tel que int) sur son type de référence d'objet correspondant (tel que Integer) simplement pour éviter la possibilité d'un NullPointerException, mais on ne peut nier que l'un des avantages de types primitifs, c'est qu'ils ne conduisent pas à l' NullPointerExceptionart. Cependant, la validité des primitives doit encore être vérifiée (un mois ne peut pas être un entier négatif) et cet avantage peut donc être faible. D'un autre côté, les primitives ne peuvent pas être utilisées dans les collections Java et il y a des moments où l'on souhaite avoir la possibilité de définir une valeur sur null.

Le plus important est d'être très prudent quant à la combinaison de primitives, de types de référence et d'autoboxing. Il y a un avertissement dans Effective Java (deuxième édition, article n ° 49) concernant les dangers, y compris le rejet NullPointerException, liés au mélange imprudent de types primitifs et de référence.

Considérez attentivement les appels de méthode chaînés

Un NullPointerExceptionpeut être très facile à trouver car un numéro de ligne indiquera où cela s'est produit. Par exemple, une trace de pile peut ressembler à celle illustrée ci-dessous:

java.lang.NullPointerException at dustin.examples.AvoidingNullPointerExamples.demonstrateNullPointerExceptionStackTrace(AvoidingNullPointerExamples.java:222) at dustin.examples.AvoidingNullPointerExamples.main(AvoidingNullPointerExamples.java:247) 

La trace de pile montre clairement que le a NullPointerExceptionété renvoyé à la suite d'un code exécuté à la ligne 222 de AvoidingNullPointerExamples.java. Même avec le numéro de ligne fourni, il peut toujours être difficile de déterminer quel objet est nul s'il y a plusieurs objets avec des méthodes ou des champs accessibles sur la même ligne.

Par exemple, une instruction comme someObject.getObjectA().getObjectB().getObjectC().toString();a quatre appels possibles qui pourraient avoir renvoyé l' NullPointerExceptionattribué à la même ligne de code. L'utilisation d'un débogueur peut aider à cela, mais il peut y avoir des situations où il est préférable de simplement casser le code ci-dessus afin que chaque appel soit effectué sur une ligne distincte. Cela permet au numéro de ligne contenu dans une trace de pile d'indiquer facilement quel appel exact était le problème. En outre, il facilite la vérification explicite de chaque objet pour null. Cependant, en revanche, la décomposition du code augmente le nombre de lignes de code (pour certains, c'est positif!) Et peut ne pas toujours être souhaitable, surtout si l'on est certain qu'aucune des méthodes en question ne sera jamais nulle.

Rendre NullPointerExceptions plus informatif

In the above recommendation, the warning was to consider carefully use of method call chaining primarily because it made having the line number in the stack trace for a NullPointerException less helpful than it otherwise might be. However, the line number is only shown in a stack trace when the code was compiled with the debug flag turned on. If it was compiled without debug, the stack trace looks like that shown next:

java.lang.NullPointerException at dustin.examples.AvoidingNullPointerExamples.demonstrateNullPointerExceptionStackTrace(Unknown Source) at dustin.examples.AvoidingNullPointerExamples.main(Unknown Source) 

Comme le montre la sortie ci-dessus, il existe un nom de méthode, mais pas de numéro de ligne pour le NullPointerException. Cela rend plus difficile d'identifier immédiatement ce qui dans le code a conduit à l'exception. Une façon de résoudre ce problème consiste à fournir des informations de contexte dans tous les levés NullPointerException. Cette idée a été démontrée plus tôt quand un a NullPointerExceptionété capturé et relancé avec des informations de contexte supplémentaires comme un IllegalArgumentException. Cependant, même si l'exception est simplement renvoyée comme une autre NullPointerExceptionavec des informations de contexte, cela reste utile. Les informations de contexte aident la personne qui débogue le code à identifier plus rapidement la véritable cause du problème.

L'exemple suivant illustre ce principe.

final Calendar nullCalendar = null; try { final Date date = nullCalendar.getTime(); } catch (NullPointerException nullPointer) { log("NullPointerException with useful data", nullPointer, System.out); } try { if (nullCalendar == null) { throw new NullPointerException("Could not extract Date from provided Calendar"); } final Date date = nullCalendar.getTime(); } catch (NullPointerException nullPointer) { log("NullPointerException with useful data", nullPointer, System.out); } 

La sortie de l'exécution du code ci-dessus se présente comme suit.

ERROR: NullPointerException encountered while trying to NullPointerException with useful data java.lang.NullPointerException ERROR: NullPointerException encountered while trying to NullPointerException with useful data java.lang.NullPointerException: Could not extract Date from provided Calendar