Java Language
Pièges de Java - Nulls et NullPointerException
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 blocsynchronized
, - 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'APIMap
renvoie une valeurnull
si vous l'appelez avec une clé sans mappage. - Les
getResource(path)
etgetResourceAsStream(path)
dans les APIClassLoader
etClass
ClassLoader
null
si la ressource est introuvable. - La méthode
get()
de l'API deReference
renvoienull
si le ramasse-miettes a effacé la référence. - Diverses méthodes
getXxxx
API de servlet Java EE renvoientnull
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
etd
doivent être des wrappers primitifs, alors nous ne devrions pas nous fier à l'initialisation par défaut, ou nous devrions testernull
. Pour le premier est l'approche correcte, à moins qu'il y ait une signification précise pour les champs dans l'étatnull
.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:
- L'
IOException
été prise trop tôt. - La structure de ce code signifie qu'il existe un risque de fuite d'une ressource.
- 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
:
- 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". - Avec Java 8, il serait possible de déclarer
getReader
comme renvoyant unOptional<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.