Comment ne plus avoir de NullPointerException en Java ?

NullPointerException : l'erreur la plus courante dans un programme Java. On est tous à un moment ou à un autre tombé sur cette exception. Malheureusement, ce n'est qu'en production à 4h du matin qu'elle arrive. On corrige donc le bug suivant :

MonObjet monObjet = null;
…
monObjet.maMethode(); // => NullPointerException

Par un rapide :

if(monObjet != null) {
  monObjet.maMethode();
}

Ce correctif est tout à fait honorable, mais pourquoi ne pas essayer de ne plus avoir aucune exception de ce type ?

Il existe plusieurs méthodes validées par le compilateur pour l'éviter, et donc avant la mise en production. Aucune n'est nouvelle, certaines controversés, mais elles sont toutes étudiées dans la suite de cet article.

Tu n'utiliseras pas null

La première règle est simple : ne jamais utiliser le mot clé null.

Globalement, il est utilisé dans deux cas :

  • initialisation d'objets
  • valeur vide

Par exemple, au lieu d'écrire :

String maVariable = null;

if(test == 0) {
  maVariable = "toto";
} else if(test > 0) {
  maVariable = "titi";
} else {
  maVariable = "tata";
}

maVariable.subString(….)

On peut tout simplement ne pas initialiser la variable maVariable. De cette façon, si on oublie un cas dans notre if, le compilateur va nous générer une erreur du type :

variable maVariable might not have been initialized

Pour les valeurs vides, le problème est plus complexe. Nous verrons donc par la suite comme remplacer un null qui fait office de valeur vide.

Tu utiliseras @NotNull, @Nullable

La JSR 308 nous apporte deux annotations @NotNull et @Nullable qui permettent une vérification statique par le compilateur des valeurs des paramètres et de retour des méthodes. Par exemple :

int maMethode(@NotNull String maChaine) {
  return maChaine.length();
}

maMethode(null);

Dans ce cas on aurait eu une NullPointerException au lancement, alors qu'avec cette annotation on aura un warning directement à la compilation.

Bien évidement, mon exemple est simpliste mais cela fonctionne aussi sur des cas plus complexes.

Cette JSR peut être intégrée à plusieurs outils (Maven, Eclipse, etc.), pour plus de détails c'est ici. Il faut noter qu'IntelliJ l'intègre nativement.

Tu utiliseras des objets vides

Une autre façon d'éviter le null est d'utiliser le Null Object Pattern. Le principe est très simple : créer des objets spécifiques qui représente le vide. Par exemple :

class Personne {
  public String getNom() { … }
  public void setNom(String nom) { … }
  public List<Personne> getEnfants() { … }
}

public final PersonneVide extends Personne {
  public String getNom() {
    return "";
  }
  public void setNom(String nom) {}
  public List<Personne> getEnfants() {
    return Collections.emptyList();
  }
}

Il suffit d'employer PersonneVide plutôt que d'utiliser null. Il y a plusieurs implémentations de cette solution. On peut aussi créer une interface Personne et l'implémenter en deux classe : PersonnePhysique (la vraie implémentation) et PersonneVide.

On peut aussi facilement étendre ce principe pour avoir d'autres objets plus spécifiques, comme le montre Martin Fowler.

Cette méthode a des nombreuses limitations. Il faut se contraindre à créer et surtout à utiliser ses objets vides. Ce qui rajoute du code et donc peut rajouter des bugs. À ma connaissance il n'existe pas de méthode automatique qui nous force à utiliser cette méthode.

Tu lanceras des exceptions

Une autre solution est d'utiliser des exceptions. Plutôt que de renvoyer une valeur null, on lance une exception. Cela à l'avantage d'avoir une validation statique faite par le compilateur. Pour plus de détail sur les bonnes pratiques sur la gestion des exceptions, c'est ici. Par exemple :

class ImpossibleDeGenererIDException extends Exception { … }

public class Personne {
  public Email genereID() throws ImpossibleDeGenererIDException {
    //On lance l'exception si on n'a pas pu générer d'ID
    //plutôt que de renvoyer null
  }
}

Il faut noter que lancer une exception est très coûteux en terme de calcul. Et comme le dis très bien l'article citer ci-dessus, utiliser une exception pour un contrôle de flux est un anti-pattern. À mon avis, cette solution n'est pas à utiliser.

Tu utiliseras le pattern Option/Maybe

Ce pattern nous vient des langages fonctionnels. Il consiste en un objet particulier Option qui encapsule un résultat. Cet objet peut avoir 2 valeurs possibles : une valeur non nulle (Some) ou None. Prenons un exemple de l'API de Java. La méthode get de MyMap renvoie null si l'objet demandé n'est pas présent dans celle-ci. Avec ce pattern, la méthode renverrait une instance d'Option. C'est donc l'utilisateur de la méthode est forcé par le compilateur de gérer le cas None. Une implémentation naïve serait donc :

interface Option<T> {
  public Boolean isNone();
}

class Some<T> implements Option<T> {
  public T get() { … }
  public Boolean isNone() { return false; }
}

class None<T> implements Option<T> {
  public Boolean isNone() { return true; }
}

class MyMap<K, V> {
  public Option<V> get(K key) { … }
}

MyMap<Integer, String> map = new MyMap<Integer, String>();
map.put(1, "un");

Option<String> result = map.get(2);
if(result.isNone()) {
  // Gestion du cas None
} else {
  String val = ((Some) result).get();
}

Cette méthode a le gros avantage d'avoir une validation statique par le compilateur. En effet, il est impossible d'utiliser directement la valeur de retour de la méthode get. Elle est du type Option<String> et donc complètement différent que celui attendu "classiquement" : String. On est donc forcé de faire le test du None. De plus, cela nous donne tout de suite l'information que la méthode peut renvoyer une valeur vide.

Cette implémentation est clairement bancale, la bibliothèque Functional Java en fournit une plus complète.

C'est à mon avis la méthode la plus sûre. Mais elle a été pensée pour les langages gérant le pattern matching comme Scala ou Haskell, où elle est intégrée nativement dans l'API.

Elle est donc un peu lourde à utiliser en Java. Et surtout, elle n'est pas intégrée à l'API. Il faut donc soit utiliser une autre API, ou faire des wrapper autour de l'API standard.

Conclusion

Ces méthodes sont adaptables à tous les langages orientés objets. Certains offrent d'autres méthodes. Par exemple en Groovy il y a l'Elvis Operator, en Ruby (avec Rails) le try.

Le problème reste lors de l'utilisation de bibliothèques externes. Même si un projet utilise ces solutions, il faudra toujours faire attention lors de l'utilisation de l'API Java ou autre (Hibernate pour ne citer que lui).

Il n'y a donc pas de méthode magique pour ne plus avoir de NullPointerException dans votre code Java (à part changer de langage ;)).