Recherche…


Remarques

La valeur null est la valeur par défaut pour une valeur non initialisée d'un champ dont le type est un type de référence.

NullPointerException (ou NPE) est l'exception levée lorsque vous tentez d'effectuer une opération inappropriée sur la référence d'objet null . Ces opérations comprennent:

  • appeler une méthode d'instance sur un objet cible null ,
  • accéder à un champ d'un objet cible null ,
  • tenter d'indexer un objet de tableau null ou d'accéder à sa longueur,
  • utiliser une référence d'objet null comme mutex dans un bloc synchronized ,
  • lancer une référence à un objet null ,
  • désencapsuler une référence d'objet null et
  • lancer une référence d'objet null .

Les causes profondes les plus courantes des NPE:

  • oublier d'initialiser un champ avec un type de référence,
  • oublier d'initialiser des éléments d'un tableau d'un type de référence, ou
  • ne pas tester les résultats de certaines méthodes API spécifiées comme renvoyant null dans certaines circonstances.

Les exemples de méthodes couramment utilisées qui renvoient null incluent:

  • La méthode get(key) de l'API Map renvoie une valeur null si vous l'appelez avec une clé sans mappage.
  • Les getResource(path) et getResourceAsStream(path) dans les API ClassLoader et Class ClassLoader null si la ressource est introuvable.
  • La méthode get() de l'API de Reference renvoie null si le ramasse-miettes a effacé la référence.
  • Diverses méthodes getXxxx API de servlet Java EE renvoient null si vous tentez de récupérer un paramètre de requête, un attribut de session ou de session inexistant, etc.

Il existe des stratégies pour éviter les NPE indésirables, comme tester explicitement null ou utiliser "Yoda Notation", mais ces stratégies ont souvent pour résultat indésirable de cacher des problèmes dans votre code qui devraient vraiment être résolus.

Piège - L'utilisation inutile de Wrappers primitifs peut mener à des exceptions NullPointerExceptions

Parfois, les programmeurs qui sont de nouveaux Java utilisent des types et des encapsuleurs primitifs de manière interchangeable. Cela peut entraîner des problèmes. Considérez cet exemple:

public class MyRecord {
    public int a, b;
    public Integer c, d;
}

...
MyRecord record = new MyRecord();
record.a = 1;               // OK
record.b = record.b + 1;    // OK
record.c = 1;               // OK
record.d = record.d + 1;    // throws a NullPointerException

Notre classe MyRecord 1 repose sur une initialisation par défaut pour initialiser les valeurs sur ses champs. Ainsi, lorsque nous avons de new record, les a et b les champs seront mis à zéro, et les c et d les champs seront mis à null .

Lorsque nous essayons d'utiliser les champs initialisés par défaut, nous voyons que les champs int fonctionnent tout le temps, mais les champs Integer fonctionnent dans certains cas et pas dans d'autres. Spécifiquement, dans le cas où cela échoue (avec d ), ce qui se passe est que l'expression du côté droit tente de décomprimer une référence null , et c'est ce qui provoque la levée de l'exception NullPointerException .

Il y a plusieurs façons de regarder ceci:

  • Si les champs c et d doivent être des wrappers primitifs, alors nous ne devrions pas nous fier à l'initialisation par défaut, ou nous devrions tester null . Pour le premier est l'approche correcte, à moins qu'il y ait une signification précise pour les champs dans l'état null .

  • Si les champs ne doivent pas nécessairement être des enveloppes primitives, il est erroné de les transformer en enveloppes primitives. En plus de ce problème, les wrappers primitifs ont des surcharges supplémentaires par rapport aux types primitifs.

La leçon ici est de ne pas utiliser les types d’encapsuleurs primitifs, sauf si vous en avez vraiment besoin.


1 - Ce cours n’est pas un exemple de bonne pratique de codage. Par exemple, une classe bien conçue n'aurait pas de champs publics. Cependant, ce n'est pas le but de cet exemple.

Pitfall - Utiliser null pour représenter un tableau ou une collection vide

Certains programmeurs pensent que c'est une bonne idée d'économiser de l'espace en utilisant un null pour représenter un tableau ou une collection vide. S'il est vrai que vous pouvez économiser une petite quantité d'espace, le revers de la médaille est que cela rend votre code plus compliqué et plus fragile. Comparez ces deux versions d'une méthode de sommation d'un tableau:

La première version est la façon dont vous devez normalement coder la méthode:

/**
 * Sum the values in an array of integers.
 * @arg values the array to be summed
 * @return the sum
 **/
public int sum(int[] values) {
    int sum = 0;
    for (int value : values) {
        sum += value;
    }
    return sum;
}

La deuxième version est la manière dont vous devez coder la méthode si vous avez l'habitude d'utiliser null pour représenter un tableau vide.

/**
 * Sum the values in an array of integers.
 * @arg values the array to be summed, or null.
 * @return the sum, or zero if the array is null.
 **/
public int sum(int[] values) {
    int sum = 0;
    if (values != null) {
        for (int value : values) {
            sum += value;
        }
    }
    return sum;
}

Comme vous pouvez le constater, le code est un peu plus compliqué. Ceci est directement attribuable à la décision d'utiliser null de cette manière.

Maintenant, considérez si ce tableau qui pourrait être null est utilisé dans de nombreux endroits. À chaque endroit où vous l'utilisez, vous devez déterminer si vous devez tester null . Si vous manquez un test null devant être présent, vous risquez une NullPointerException . Ainsi, la stratégie d’utilisation de null de cette manière rend votre application plus fragile; c'est-à-dire plus vulnérable aux conséquences des erreurs de programmation.


La leçon ici est d'utiliser des tableaux vides et des listes vides lorsque c'est ce que vous voulez dire.

int[] values = new int[0];                     // always empty
List<Integer> list = new ArrayList();          // initially empty
List<Integer> list = Collections.emptyList();  // always empty

La surcharge d'espace est faible et il existe d'autres moyens de le minimiser si cela s'avère utile.

Piège - "Réussir" des nulls inattendus

Sur StackOverflow, nous voyons souvent du code comme celui-ci dans Réponses:

public String joinStrings(String a, String b) {
    if (a == null) {
        a = "";
    }
    if (b == null) {
        b = "";
    }
    return a + ": " + b;
}

Souvent, cela est accompagné d'une assertion qui est la "meilleure pratique" pour tester null comme ceci pour éviter NullPointerException .

Est-ce la meilleure pratique? En bref: non

Certaines hypothèses sous-jacentes doivent être remises en question avant de pouvoir dire si c'est une bonne idée de le faire dans nos joinStrings :

Qu'est-ce que cela signifie pour que "a" ou "b" soit nul?

Une valeur de String peut être zéro ou plusieurs caractères, nous avons donc déjà un moyen de représenter une chaîne vide. Est-ce que null signifie quelque chose de différent de "" ? Si non, il est alors problématique d'avoir deux manières de représenter une chaîne vide.

Est-ce que le null provient d'une variable non initialisée?

Un null peut provenir d'un champ non initialisé ou d'un élément de tableau non initialisé. La valeur peut être non initialisée par conception ou par accident. Si c'était par accident, alors c'est un bug.

Le null représente-t-il un "ne sait pas" ou une "valeur manquante"?

Parfois, un null peut avoir un sens véritable; Par exemple, la valeur réelle d'une variable est inconnue ou indisponible ou "facultative". Dans Java 8, la classe Optional offre un meilleur moyen d’exprimer cela.

S'il s'agit d'un bogue (ou d'une erreur de conception), faut-il "réparer"?

Une interprétation du code est que nous "réparons" un null inattendu en utilisant une chaîne vide à sa place. La stratégie est-elle correcte? Serait-il préférable de laisser l'exception NullPointerException se produire, puis d'attraper l'exception plus haut dans la pile et de l'enregistrer en tant que bogue?

Le problème avec "rendre bon" est que cela risque de cacher le problème ou de rendre le diagnostic plus difficile.

Est-ce efficace / bon pour la qualité du code?

Si l'approche "make good" est utilisée de manière cohérente, votre code contiendra beaucoup de tests null "défensifs". Cela va le rendre plus long et plus difficile à lire. En outre, tous ces tests et «correctifs» sont susceptibles d’avoir un impact sur les performances de votre application.

En résumé

Si null est une valeur significative, alors le test du cas null est la bonne approche. Le corollaire est que si une valeur null est significative, cela devrait être clairement documenté dans les javadocs de toutes les méthodes qui acceptent la valeur null ou la renvoient.

Sinon, il est préférable de traiter un null inattendu en tant qu'erreur de programmation et de laisser le NullPointerException se produire pour que le développeur sache qu'il y a un problème dans le code.

Pitfall - Renvoyer null au lieu de lancer une exception

Certains programmeurs Java ont une aversion générale pour lancer ou propager des exceptions. Cela conduit à un code comme celui-ci:

public Reader getReader(String pathname) {
    try {
        return new BufferedReader(FileReader(pathname));
    } catch (IOException ex) {
        System.out.println("Open failed: " + ex.getMessage());
        return null;
    }

}

Alors, quel est le problème avec ça?

Le problème est que getReader renvoie une valeur null tant que valeur spéciale pour indiquer que le Reader n'a pas pu être ouvert. Maintenant, la valeur renvoyée doit être testée pour voir si elle est null avant d'être utilisée. Si le test est omis, le résultat sera une NullPointerException .

Il y a en fait trois problèmes ici:

  1. L' IOException été prise trop tôt.
  2. La structure de ce code signifie qu'il existe un risque de fuite d'une ressource.
  3. Un null été utilisé, puis renvoyé car aucun "vrai" Reader n'était disponible pour revenir.

En fait, en supposant que l'exception devait être détectée tôt comme cela, il y avait quelques alternatives à la restitution de null :

  1. Il serait possible d'implémenter une classe NullReader ; Par exemple, une opération où les opérations de l'API se comportent comme si le lecteur était déjà à la position "fin du fichier".
  2. Avec Java 8, il serait possible de déclarer getReader comme renvoyant un Optional<Reader> .

Pitfall - Ne pas vérifier si un flux d'E / S n'est même pas initialisé lors de la fermeture

Pour éviter les fuites de mémoire, il ne faut pas oublier de fermer un flux d'entrée ou un flux de sortie dont le travail est effectué. Cela se fait généralement avec une instruction try - catch - finally sans la partie catch :

void writeNullBytesToAFile(int count, String filename) throws IOException {
    FileOutputStream out = null;
    try {
        out = new FileOutputStream(filename);
        for(; count > 0; count--)
            out.write(0);
    } finally {
        out.close();
    }
}

Bien que le code ci-dessus puisse paraître innocent, il présente une faille qui peut rendre le débogage impossible. Si la ligne où out est initialisée ( out = new FileOutputStream(filename) ) génère une exception, alors out sera null lorsque out.close() sera exécuté, entraînant une méchante NullPointerException !

Pour éviter cela, assurez-vous simplement que le flux n'est pas null avant de le fermer.

void writeNullBytesToAFile(int count, String filename) throws IOException {
    FileOutputStream out = null;
    try {
        out = new FileOutputStream(filename);
        for(; count > 0; count--)
            out.write(0);
    } finally {
        if (out != null)
            out.close();
    }
}

Une approche encore meilleure consiste à try avec des ressources, car cela ferme automatiquement le flux avec une probabilité de 0 pour lancer un NPE sans avoir besoin d'un bloc finally .

void writeNullBytesToAFile(int count, String filename) throws IOException {
    try (FileOutputStream out = new FileOutputStream(filename)) {
        for(; count > 0; count--)
            out.write(0);
    }
}

Pitfall - Utiliser la notation "Yoda" pour éviter une exception NullPointerException

Un grand nombre d'exemples de code postés sur StackOverflow incluent des extraits comme ceci:

if ("A".equals(someString)) {
    // do something
}

Cela "empêche" ou "évite" une possible NullPointerException dans le cas où someString est null . En outre, il est discutable que

    "A".equals(someString)

est mieux que:

    someString != null && someString.equals("A")

(Il est plus concis et, dans certaines circonstances, il pourrait être plus efficace. Cependant, comme nous le soutenons plus loin, la concision pourrait être négative.)

Cependant, le véritable piège consiste à utiliser le test Yoda pour éviter les NullPointerExceptions .

Lorsque vous écrivez "A".equals(someString) vous "A".equals(someString) " le cas où someString est null . Mais comme un autre exemple ( Pitfall - "Faire de bonnes" nulls inattendus ) explique, "faire" null valeurs null peut être nocif pour diverses raisons.

Cela signifie que les conditions de Yoda ne sont pas les "meilleures pratiques" 1 . À moins que la valeur null soit attendue, il est préférable de laisser l' NullPointerException se produire pour obtenir un échec de test d'unité (ou un rapport de bogue). Cela vous permet de trouver et de corriger le bogue qui a provoqué l'apparition de la null inattendue / indésirable.

Les conditions Yoda ne doivent être utilisées que dans les cas où la valeur null est attendue car l'objet que vous testez provient d'une API documentée comme renvoyant une valeur null . Et sans doute, il serait préférable d'utiliser l'une des méthodes les moins jolies pour exprimer le test, car cela aide à mettre en évidence le test null pour quelqu'un qui examine votre code.


1 - Selon Wikipedia : "Les meilleures pratiques de codage sont un ensemble de règles informelles que la communauté du développement de logiciels a appris au fil du temps, ce qui peut aider à améliorer la qualité des logiciels." . Utiliser la notation Yoda ne permet pas d'atteindre cet objectif. Dans beaucoup de situations, cela aggrave le code.



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow