Éviter la vérification de l’instruction Null en Java

Aperçu

En général, les variables nulles, les références et les collections sont difficiles à gérer dans le code Java. Non seulement ils sont difficiles à identifier, mais ils sont également complexes à gérer.

En fait, tout manque dans le traitement de null ne peut pas être identifié au moment de la compilation et entraîne une exception NullPointerException au moment de l’exécution.

Dans ce tutoriel, nous examinerons la nécessité de vérifier null en Java et diverses alternatives qui nous aident à éviter les vérifications null dans notre code.

Further reading:

Using NullAway to Avoid NullPointerExceptions

Learn how to avoid NullPointerExceptions using NullAway.
Read more →

Spring Null-Safety Annotations

A quick and practical guide to null-safety annotations in Spring.
Read more →

Introduction to the Null Object Pattern

Learn about the Null Object Pattern and how to implement it in Java
Read more →

What Is NullPointerException?

Selon l’exception Javadoc for NullPointerException, elle est levée lorsqu’une application tente d’utiliser null dans un cas où un objet est requis, tel que :

  • Appelant une méthode d’instance d’un objet null
  • Accédant ou modifiant un champ d’un objet null
  • Prenant la longueur de null comme s’il s’agissait d’un tableau
  • Accédant ou modifiant les emplacements de null comme s’il s’agissait d’un tableau
  • jetant null comme s’il s’agissait d’une valeur Throwable

Voyons rapidement quelques exemples du code Java à l’origine de cette exception:

public void doSomething() { String result = doSomethingElse(); if (result.equalsIgnoreCase("Success")) // success }}private String doSomethingElse() { return null;}

Ici, nous avons essayé d’appeler un appel de méthode pour une référence nulle. Cela entraînerait une exception NullPointerException.

Un autre exemple courant est si nous essayons d’accéder à un tableau null :

public static void main(String args) { findMax(null);}private static void findMax(int arr) { int max = arr; //check other elements in loop}

Cela provoque une exception NullPointerException à la ligne 6.

Ainsi, l’accès à n’importe quel champ, méthode ou index d’un objet null provoque une exception NullPointerException, comme on peut le voir dans les exemples ci-dessus.

Une façon courante d’éviter l’exception NullPointerException consiste à vérifier la valeur null:

public void doSomething() { String result = doSomethingElse(); if (result != null && result.equalsIgnoreCase("Success")) { // success } else // failure}private String doSomethingElse() { return null;}

Dans le monde réel, les programmeurs ont du mal à identifier quels objets peuvent être nuls. Une stratégie agressivement sûre pourrait consister à vérifier null pour chaque objet. Cela, cependant, provoque beaucoup de vérifications nulles redondantes et rend notre code moins lisible.

Dans les prochaines sections, nous passerons en revue certaines des alternatives en Java qui évitent une telle redondance.

Gérer null Via le contrat API

Comme indiqué dans la dernière section, l’accès aux méthodes ou aux variables des objets null provoque une exception NullPointerException. Nous avons également discuté du fait que mettre une vérification nulle sur un objet avant d’y accéder élimine la possibilité de NullPointerException.

Cependant, il existe souvent des API qui peuvent gérer des valeurs nulles. Par exemple :

public void print(Object param) { System.out.println("Printing " + param);}public Object process() throws Exception { Object result = doSomething(); if (result == null) { throw new Exception("Processing fail. Got a null response"); } else { return result; }}

L’appel de méthode print() affichera simplement « null” mais ne lancera pas d’exception. De même, process() ne retournerait jamais null dans sa réponse. Cela lève plutôt une exception.

Ainsi, pour un code client accédant aux API ci-dessus, il n’est pas nécessaire de vérifier null.

Cependant, ces API doivent le rendre explicite dans leur contrat. Un lieu commun pour les API pour publier un tel contrat est le JavaDoc.

Ceci, cependant, ne donne aucune indication claire du contrat API et s’appuie donc sur les développeurs de code client pour assurer sa conformité.

Dans la section suivante, nous verrons comment quelquesEs et autres outils de développement aident les développeurs à cela.

Automatisation des contrats API

4.1. L’utilisation de l’analyse de code statique

Les outils d’analyse de code statique aident à améliorer considérablement la qualité du code. Et quelques outils de ce type permettent également aux développeurs de maintenir le contrat nul. Un exemple est FindBugs.

FindBugs permet de gérer le contrat nul via les annotations @Nullable et @NonNull. Nous pouvons utiliser ces annotations sur n’importe quelle méthode, champ, variable locale ou paramètre. Cela indique explicitement au code client si le type annoté peut être null ou non. Voyons un exemple:

public void accept(@Nonnull Object param) { System.out.println(param.toString());}

Ici, @NonNull indique clairement que l’argument ne peut pas être nul. Si le code client appelle cette méthode sans vérifier l’argument null, FindBugs générerait un avertissement au moment de la compilation.

4.2. Utilisation de la prise en charge des EDI

Les développeurs s’appuient généralement sur les EDI pour écrire du code Java. Et des fonctionnalités telles que la complétion intelligente du code et des avertissements utiles, comme lorsqu’une variable n’a peut-être pas été assignée, aident certainement dans une large mesure.

CertainsEs permettent également aux développeurs de gérer les contrats d’API et éliminent ainsi le besoin d’un outil d’analyse de code statique. IntelliJ IDEA fournit les annotations @NonNull et @Nullable. Pour ajouter le support de ces annotations dans IntelliJ, nous devons ajouter la dépendance Maven suivante:

<dependency> <groupId>org.jetbrains</groupId> <artifactId>annotations</artifactId> <version>16.0.2</version></dependency>

Maintenant, IntelliJ générera un avertissement si la vérification nulle est manquante, comme dans notre dernier exemple.

IntelliJ fournit également une annotation de contrat pour gérer les contrats d’API complexes.

5. Assertions

Jusqu’à présent, nous n’avons parlé que de supprimer le besoin de vérifications nulles du code client. Mais, cela est rarement applicable dans des applications réelles.

Supposons maintenant que nous travaillons avec une API qui ne peut pas accepter les paramètres null ou peut renvoyer une réponse nulle qui doit être gérée par le client. Cela présente la nécessité pour nous de vérifier les paramètres ou la réponse pour une valeur nulle.

Ici, nous pouvons utiliser des assertions Java au lieu de l’instruction conditionnelle traditionnelle de contrôle null :

public void accept(Object param){ assert param != null; doSomething(param);}

À la ligne 2, nous vérifions un paramètre null. Si les assertions sont activées, cela entraînerait une AssertionError.

Bien que ce soit un bon moyen d’affirmer des conditions préalables comme des paramètres non nuls, cette approche présente deux problèmes majeurs:

  1. Les assertions sont généralement désactivées dans une machine virtuelle java
  2. Une fausse assertion entraîne une erreur non contrôlée qui est irrécupérable

Par conséquent, il n’est pas recommandé aux programmeurs d’utiliser des assertions pour vérifier les conditions. Dans les sections suivantes, nous discuterons d’autres façons de gérer les validations nulles.

Éviter Les Contrôles Nuls Grâce Aux Pratiques De Codage

6.1. Conditions préalables

C’est généralement une bonne pratique d’écrire du code qui échoue tôt. Par conséquent, si une API accepte plusieurs paramètres qui ne sont pas autorisés à être nuls, il est préférable de vérifier chaque paramètre non nul comme condition préalable de l’API.

Par exemple, regardons deux méthodes – une qui échoue tôt et une qui ne le fait pas:

public void goodAccept(String one, String two, String three) { if (one == null || two == null || three == null) { throw new IllegalArgumentException(); } process(one); process(two); process(three);}public void badAccept(String one, String two, String three) { if (one == null) { throw new IllegalArgumentException(); } else { process(one); } if (two == null) { throw new IllegalArgumentException(); } else { process(two); } if (three == null) { throw new IllegalArgumentException(); } else { process(three); }}

Clairement, nous devrions préférer goodAccept() à badAccept().

Comme alternative, nous pouvons également utiliser les conditions préalables de Guava pour valider les paramètres de l’API.

6.2. Utiliser des primitives Au lieu des classes Wrapper

Puisque null n’est pas une valeur acceptable pour des primitives comme int, nous devrions les préférer à leurs homologues wrapper comme Integer dans la mesure du possible.

Considérons deux implémentations d’une méthode qui somme deux entiers :

public static int primitiveSum(int a, int b) { return a + b;}public static Integer wrapperSum(Integer a, Integer b) { return a + b;}

Maintenant, appelons ces API dans notre code client :

int sum = primitiveSum(null, 2);

Cela entraînerait une erreur de compilation car null n’est pas une valeur valide pour un int.

Et lorsque vous utilisez l’API avec des classes wrapper, nous obtenons une exception NullPointerException:

assertThrows(NullPointerException.class, () -> wrapperSum(null, 2));

Il existe également d’autres facteurs pour utiliser des primitives sur des wrappers, comme nous l’avons couvert dans un autre tutoriel, les primitives Java par rapport aux objets.

6.3. Collections vides

De temps en temps, nous devons renvoyer une collection en réponse à une méthode. Pour de telles méthodes, nous devrions toujours essayer de renvoyer une collection vide au lieu de null:

public List<String> names() { if (userExists()) { return Stream.of(readName()).collect(Collectors.toList()); } else { return Collections.emptyList(); }}

Par conséquent, nous avons évité la nécessité pour notre client d’effectuer une vérification null lors de l’appel de cette méthode.

Utilisation d’objets

Java 7 a introduit la nouvelle API des objets. Cette API dispose de plusieurs méthodes utilitaires statiques qui enlèvent beaucoup de code redondant. Regardons une telle méthode, requireNonNull():

public void accept(Object param) { Objects.requireNonNull(param); // doSomething()}

Maintenant, testons la méthode accept():

assertThrows(NullPointerException.class, () -> accept(null));

Donc, si null est passé en argument, accept() lance une exception NullPointerException.

Cette classe a également des méthodes isNull() et nonNull() qui peuvent être utilisées comme prédicats pour vérifier la valeur null d’un objet.

En utilisant l’option

8.1. L’utilisation d’orElseThrow

Java 8 a introduit une nouvelle API optionnelle dans le langage. Cela offre un meilleur contrat pour gérer les valeurs facultatives par rapport à null. Voyons comment Optional supprime le besoin de vérifications nulles:

public Optional<Object> process(boolean processed) { String response = doSomething(processed); if (response == null) { return Optional.empty(); } return Optional.of(response);}private String doSomething(boolean processed) { if (processed) { return "passed"; } else { return null; }}

En renvoyant une option, comme indiqué ci-dessus, la méthode de processus indique clairement à l’appelant que la réponse peut être vide et doit être gérée au moment de la compilation.

Cela supprime notamment le besoin de vérifications nulles dans le code client. Une réponse vide peut être traitée différemment en utilisant le style déclaratif de l’API optionnelle:

assertThrows(Exception.class, () -> process(false).orElseThrow(() -> new Exception()));

En outre, il fournit également un meilleur contrat aux développeurs d’API pour signifier aux clients qu’une API peut renvoyer une réponse vide.

Bien que nous ayons éliminé la nécessité d’une vérification nulle sur l’appelant de cette API, nous l’avons utilisée pour renvoyer une réponse vide. Pour éviter cela, Optional fournit une méthode ofNullable qui renvoie une option avec la valeur spécifiée, ou vide, si la valeur est nulle :

public Optional<Object> process(boolean processed) { String response = doSomething(processed); return Optional.ofNullable(response);}

8.2. Utiliser Optional avec des collections

Tout en traitant des collections vides, Optional est pratique:

public String findFirst() { return getList().stream() .findFirst() .orElse(DEFAULT_VALUE);}

Cette fonction est censée renvoyer le premier élément d’une liste. La fonction FindFirst de l’API Stream renverra une option vide lorsqu’il n’y a pas de données. Ici, nous avons utilisé orElse pour fournir une valeur par défaut à la place.

Cela nous permet de gérer soit des listes vides, soit des listes qui, après avoir utilisé la méthode de filtrage de la bibliothèque de flux, n’ont aucun élément à fournir.

Alternativement, nous pouvons également permettre au client de décider comment gérer les vides en renvoyant Optional de cette méthode:

public Optional<String> findOptionalFirst() { return getList().stream() .findFirst();}

Par conséquent, si le résultat de getList est vide, cette méthode renverra une option vide au client.

L’utilisation d’Optional with collections nous permet de concevoir des API qui ne manqueront pas de renvoyer des valeurs non nulles, évitant ainsi des vérifications nulles explicites sur le client.

Il est important de noter ici que cette implémentation repose sur getList ne renvoyant pas null. Cependant, comme nous l’avons discuté dans la dernière section, il est souvent préférable de renvoyer une liste vide plutôt qu’une liste nulle.

8.3. Combinaison d’options

Lorsque nous commençons à rendre nos fonctions facultatives, nous avons besoin d’un moyen de combiner leurs résultats en une seule valeur. Prenons notre exemple de getList plus tôt. Que se passe-t-il s’il devait renvoyer une liste facultative ou s’il devait être enveloppé avec une méthode qui enveloppait un null avec Optional using ofNullable?

Notre méthode FindFirst veut renvoyer un premier élément optionnel d’une liste optionnelle:

public Optional<String> optionalListFirst() { return getOptionalList() .flatMap(list -> list.stream().findFirst());}

En utilisant la fonction flatMap sur l’option renvoyée par getOptional, nous pouvons décompresser le résultat d’une expression interne qui renvoie Optional. Sans flatMap, le résultat serait optionnel <Optionnel <Chaîne >>. L’opération flatMap n’est effectuée que lorsque l’option Facultative n’est pas vide.

Bibliothèques

9.1. L’utilisation de Lombok

Lombok est une excellente bibliothèque qui réduit la quantité de code standard dans nos projets. Il est livré avec un ensemble d’annotations qui remplacent les parties communes du code que nous écrivons souvent nous-mêmes dans les applications Java, telles que les getters, les setters et toString(), pour n’en nommer que quelques-unes.

Une autre de ses annotations est @NonNull. Ainsi, si un projet utilise déjà Lombok pour éliminer le code standard, @NonNull peut remplacer le besoin de vérifications nulles.

Avant de passer à quelques exemples, ajoutons une dépendance Maven pour Lombok:

<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.6</version></dependency>

Maintenant, nous pouvons utiliser @NonNull partout où une vérification nulle est nécessaire:

public void accept(@NonNull Object param){ System.out.println(param);}

Donc, nous avons simplement annoté l’objet pour lequel la vérification nulle aurait été requise, et Lombok génère le classe compilée :

public void accept(@NonNull Object param) { if (param == null) { throw new NullPointerException("param"); } else { System.out.println(param); }}

Si param est null, cette méthode lève une exception NullPointerException. La méthode doit le rendre explicite dans son contrat et le code client doit gérer l’exception.

9.2. En utilisant StringUtils

Généralement, la validation de chaîne inclut une vérification d’une valeur vide en plus de la valeur nulle. Par conséquent, une instruction de validation commune serait:

public void accept(String param){ if (null != param && !param.isEmpty()) System.out.println(param);}

Cela devient rapidement redondant si nous devons traiter beaucoup de types de chaînes. C’est là que StringUtils est pratique. Avant de voir cela en action, ajoutons une dépendance Maven pour commons-lang3:

<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.11</version></dependency>

Refactorisons maintenant le code ci-dessus avec des StringUtils:

public void accept(String param) { if (StringUtils.isNotEmpty(param)) System.out.println(param);}

Nous avons donc remplacé notre vérification nulle ou vide par une méthode utilitaire statique isNotEmpty(). Cette API offre d’autres méthodes utilitaires puissantes pour gérer les fonctions de chaîne courantes.

Conclusion

Dans cet article, nous avons examiné les différentes raisons de NullPointerException et pourquoi il est difficile à identifier. Ensuite, nous avons vu différentes façons d’éviter la redondance dans le code autour de la vérification de null avec des paramètres, des types de retour et d’autres variables.

Tous les exemples sont disponibles sur GitHub.

Commencez avec Spring 5 et Spring Boot 2, à travers le cours Learn Spring:

>>CONSULTEZ LE COURS



Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.