Pourquoi lancer explicitement une NullPointerException plutôt que de la laisser se produire naturellement?

182

Lors de la lecture du code source JDK, je trouve courant que l'auteur vérifie les paramètres s'ils sont nuls, puis lance manuellement une nouvelle NullPointerException (). Pourquoi font-ils cela? Je pense qu'il n'est pas nécessaire de le faire car il lancera une nouvelle NullPointerException () quand il appellera une méthode. (Voici un code source de HashMap, par exemple :)

public V computeIfPresent(K key,
                          BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    if (remappingFunction == null)
        throw new NullPointerException();
    Node<K,V> e; V oldValue;
    int hash = hash(key);
    if ((e = getNode(hash, key)) != null &&
        (oldValue = e.value) != null) {
        V v = remappingFunction.apply(key, oldValue);
        if (v != null) {
            e.value = v;
            afterNodeAccess(e);
            return v;
        }
        else
            removeNode(hash, key, null, false, true);
    }
    return null;
}
LiJiaming
la source
32
un point clé du codage est l'intention
Scary Wombat
19
C'est une très bonne question pour votre première question! J'ai fait quelques modifications mineures; J'espère que cela ne vous dérange pas. J'ai également supprimé les remerciements et la note indiquant qu'il s'agissait de votre première question, car normalement ce genre de chose ne fait pas partie des questions SO.
David Conrad
11
Je suis C #, la convention est de lever ArgumentNullExceptiondans des cas comme ça (plutôt que NullReferenceException) - c'est en fait une très bonne question de savoir pourquoi vous soulevez NullPointerExceptionexplicitement ici (plutôt que différent).
EJoshuaS
21
@EJoshuaS C'est un vieux débat sur l' opportunité de lancer IllegalArgumentExceptionou NullPointerExceptionpour un argument nul. La convention JDK est la dernière.
shmosel
33
Le VRAI problème est qu'ils lancent une erreur et rejettent toutes les informations qui ont conduit à cette erreur . Semble que c'est le code source réel . Pas même un simple message sanglant. Triste.
Martin Ba

Réponses:

254

Plusieurs raisons me viennent à l'esprit, plusieurs étant étroitement liées:

Échec rapide: s'il échoue, mieux vaut échouer le plus tôt possible. Cela permet aux problèmes d'être capturés plus près de leur source, ce qui les rend plus faciles à identifier et à récupérer. Cela évite également de gaspiller des cycles de processeur sur du code qui échouera.

Intention: le fait de lancer l'exception indique clairement aux responsables de la maintenance que l'erreur est là intentionnellement et que l'auteur était conscient des conséquences.

Cohérence: si l'erreur pouvait se produire naturellement, elle pourrait ne pas se produire dans tous les scénarios. Si aucun mappage n'est trouvé, par exemple, remappingFunctionne sera jamais utilisé et l'exception ne sera pas levée. La validation préalable des entrées permet un comportement plus déterministe et une documentation plus claire .

Stabilité: le code évolue avec le temps. Un code qui rencontre une exception peut naturellement, après un peu de refactorisation, cesser de le faire ou le faire dans des circonstances différentes. Le jeter explicitement rend moins susceptible que le comportement change par inadvertance.

shmosel
la source
14
Aussi: de cette façon, l'emplacement à partir duquel l'exception est lancée est lié à exactement une variable qui est vérifiée. Sans cela, l'exception pourrait être due au fait que l'une des multiples variables est nulle.
Jens Schauder
45
Un autre: si vous attendez que NPE se produise naturellement, du code intermédiaire peut déjà avoir changé l'état de votre programme par des effets secondaires.
Thomas
6
Bien que cet extrait de code ne le fasse pas, vous pouvez utiliser le new NullPointerException(message)constructeur pour clarifier ce qui est nul. Idéal pour les personnes qui n'ont pas accès à votre code source. Ils en ont même fait un one-liner dans JDK 8 avec la Objects.requireNonNull(object, message)méthode utilitaire.
Robin
3
Le FAILURE doit être proche du FAULT. «Fail Fast» est plus qu'une règle empirique. Quand ne voudriez-vous pas ce comportement? Tout autre comportement signifie que vous cachez des erreurs. Il y a des "FAULTS" et des "FAILURES". Le FAILURE est lorsque ce programme digère un pointeur NULL et se bloque. Mais cette ligne de code n'est pas là où se trouve FAULT. Le NULL vient de quelque part - un argument de méthode. Qui a passé cet argument? À partir d'une ligne de code référençant une variable locale. Où est-ce que ... Vous voyez? Ça craint. À qui la responsabilité aurait-elle dû être de voir une mauvaise valeur être stockée? Votre programme aurait dû planter alors.
Noah Spurrier
4
@Thomas Bon point. Shmosel: Le point de Thomas peut être impliqué dans le point de défaillance rapide, mais il est en quelque sorte enterré. C'est un concept suffisamment important pour qu'il porte son propre nom: l' atomicité d'échec . Voir Bloch, Effective Java , Item 46. Il a une sémantique plus forte que le fail-fast. Je suggérerais de l'appeler dans un autre point. Excellente réponse dans l'ensemble, BTW. +1
Stuart marque
40

C'est pour la clarté, la cohérence et pour éviter que des travaux supplémentaires et inutiles ne soient effectués.

Considérez ce qui se passerait s'il n'y avait pas de clause de garde en haut de la méthode. Il appellerait toujours hash(key)et getNode(hash, key)même s'il nullavait été passé remappingFunctionavant que le NPE ne soit jeté.

Pire encore, si la ifcondition est falsealors nous prenons la elsebranche, qui n'utilise pas du remappingFunctiontout le, ce qui signifie que la méthode ne lance pas toujours NPE quand a nullest passé; cela dépend de l'état de la carte.

Les deux scénarios sont mauvais. If nulln'est pas une valeur valide pour remappingFunctionla méthode doit systématiquement lever une exception quel que soit l'état interne de l'objet au moment de l'appel, et il doit le faire sans faire de travail inutile qui est inutile étant donné qu'il va simplement lancer. Enfin, c'est un bon principe de code propre et clair d'avoir la garde à l'avant afin que quiconque examine le code source puisse facilement voir qu'il le fera.

Même si l'exception était actuellement levée par chaque branche de code, il est possible qu'une future révision du code change cela. Effectuer le contrôle au début garantit qu'il sera définitivement exécuté.

David Conrad
la source
25

En plus des raisons énumérées par l'excellente réponse de @ shmosel ...

Performances: il peut y avoir / y avoir eu des avantages en termes de performances (sur certaines machines virtuelles Java) à lancer le NPE explicitement plutôt que de laisser la machine virtuelle Java le faire.

Cela dépend de la stratégie adoptée par l'interpréteur Java et le compilateur JIT pour détecter le déréférencement des pointeurs nuls. Une stratégie consiste à ne pas tester null, mais à piéger le SIGSEGV qui se produit lorsqu'une instruction essaie d'accéder à l'adresse 0. C'est l'approche la plus rapide dans le cas où la référence est toujours valide, mais elle est coûteuse dans le cas NPE.

Un test explicite pour nulldans le code éviterait l'atteinte des performances SIGSEGV dans un scénario où les NPE étaient fréquents.

(Je doute que ce soit une micro-optimisation valable dans une JVM moderne, mais cela aurait pu être dans le passé.)


Compatibilité: la raison probable pour laquelle il n'y a pas de message dans l'exception est la compatibilité avec les NPE qui sont lancés par la JVM elle-même. Dans une implémentation Java conforme, un NPE lancé par la JVM contient un nullmessage. (Android Java est différent.)

Stephen C
la source
20

Outre ce que d'autres personnes ont souligné, il convient de noter ici le rôle de la convention. En C #, par exemple, vous avez également la même convention de lever explicitement une exception dans des cas comme celui-ci, mais c'est spécifiquement un ArgumentNullException, qui est un peu plus spécifique. (La convention C # est que représente NullReferenceException toujours un bogue d'un type quelconque - tout simplement, cela ne devrait jamais se produire dans le code de production; d'accord, le fait ArgumentNullExceptiongénéralement aussi, mais cela pourrait être un bogue plus du genre "vous ne le faites pas comprendre comment utiliser correctement la librairie "genre de bogue).

Donc, fondamentalement, en C # NullReferenceExceptionsignifie que votre programme a réellement essayé de l'utiliser, alors que ArgumentNullExceptioncela signifie qu'il a reconnu que la valeur était fausse et qu'il n'a même pas pris la peine d'essayer de l'utiliser. Les implications peuvent en fait être différentes (selon les circonstances) car cela ArgumentNullExceptionsignifie que la méthode en question n'a pas encore eu d'effets secondaires (puisqu'elle a échoué aux conditions préalables de la méthode).

Incidemment, si vous augmentez quelque chose comme ArgumentNullExceptionou IllegalArgumentException, cela fait partie du point de faire le contrôle: vous voulez une exception différente de celle que vous obtiendriez "normalement".

Dans tous les cas, la levée explicite de l'exception renforce la bonne pratique consistant à être explicite sur les pré-conditions et les arguments attendus de votre méthode, ce qui facilite la lecture, l'utilisation et la maintenance du code. Si vous n'avez pas vérifié explicitement null, je ne sais pas si c'est parce que vous pensiez que personne ne passerait jamais un nullargument, vous le comptez pour lever l'exception de toute façon, ou vous avez simplement oublié de vérifier cela.

EJoshuaS - Réintégrer Monica
la source
4
+1 pour le paragraphe du milieu. Je dirais que le code en question devrait 'lancer une nouvelle IllegalArgumentException ("remappingFunction ne peut pas être nul");' De cette façon, ce qui n'allait pas est immédiatement évident. Le NPE montré est un peu ambigu.
Chris Parker
1
@ChrisParker J'étais du même avis, mais il s'avère que NullPointerException est destiné à signifier un argument nul passé à une méthode qui attend un argument non nul, en plus d'être une réponse d'exécution à une tentative de déréférencer null. De la javadoc: "Les applications doivent lancer des instances de cette classe pour indiquer d'autres utilisations illégales de l' nullobjet." Je n'en suis pas fou, mais cela semble être le design prévu.
VGR
1
Je suis d'accord, @ChrisParker - je pense que cette exception est plus spécifique (parce que le code n'a même jamais essayé de faire quoi que ce soit avec la valeur, il a immédiatement reconnu qu'il ne devrait pas l'utiliser). J'aime la convention C # dans ce cas. La convention C # est que NullReferenceException(équivalent à NullPointerException) signifie que votre code a réellement essayé de l' utiliser (ce qui est toujours un bogue - cela ne devrait jamais arriver dans le code de production), contre "Je sais que l'argument est faux, donc je n'ai même pas essayez de l'utiliser. " Il y a aussi ArgumentException(ce qui signifie que l'argument était faux pour une autre raison).
EJoshuaS
2
Je vais dire ceci, je lance TOUJOURS une IllegalArgumentException comme décrit. Je me sens toujours à l'aise de bafouer les conventions quand j'ai l'impression que la convention est stupide.
Chris Parker
1
@PieterGeerkens - ouais, parce que la ligne 35 de NullPointerException est tellement meilleure que la ligne 35 d'IllegalArgumentException ("La fonction ne peut pas être nulle"). Sérieusement?
Chris Parker
12

C'est ainsi que vous obtiendrez l'exception dès que vous perpétrez l'erreur, plutôt que plus tard lorsque vous utilisez la carte et que vous ne comprendrez pas pourquoi cela s'est produit.

Marquis de Lorne
la source
9

Cela transforme une condition d'erreur apparemment erratique en une violation de contrat claire: la fonction a certaines conditions préalables pour fonctionner correctement, elle les vérifie donc au préalable, les imposant de les respecter.

L'effet est que vous n'aurez pas à déboguer computeIfPresent()lorsque vous en retirerez l'exception. Une fois que vous voyez que l'exception provient de la vérification des conditions préalables, vous savez que vous avez appelé la fonction avec un argument non autorisé. Si la vérification n'était pas là, vous devriez exclure la possibilité qu'il y ait un bogue en computeIfPresent()lui-même qui conduit à la levée de l'exception.

De toute évidence, lancer le générique NullPointerExceptionest un très mauvais choix, car cela ne signale pas une violation du contrat en soi. IllegalArgumentExceptionserait un meilleur choix.


Sidenote:
Je ne sais pas si Java le permet (j'en doute), mais les programmeurs C / C ++ utilisent assert()dans ce cas, ce qui est nettement meilleur pour le débogage: il dit au programme de planter immédiatement et aussi fort que possible si le condition évaluée à faux. Donc, si tu courais

void MyClass_foo(MyClass* me, int (*someFunction)(int)) {
    assert(me);
    assert(someFunction);

    ...
}

sous un débogueur, et quelque chose est passé NULLdans l'un ou l'autre des arguments, le programme s'arrêterait juste à la ligne indiquant quel argument était NULL, et vous seriez capable d'examiner toutes les variables locales de la pile d'appels entière à loisir.

cmaster - réintégrer monica
la source
1
assert something != null;mais cela nécessite l' -assertionsindicateur lors de l'exécution de l'application. Si le -assertionsdrapeau n'est pas là, le mot clé assert ne lancera pas une AssertionException
Zoe
Je suis d'accord, c'est pourquoi je préfère la convention C # ici - référence nulle, argument invalide et argument nul impliquent tous généralement un bogue quelconque, mais ils impliquent différents types de bogues. «Vous essayez d'utiliser une référence nulle» est souvent très différent de «vous utilisez mal la bibliothèque».
EJoshuaS
7

C'est parce qu'il est possible que cela ne se produise pas naturellement. Voyons un morceau de code comme celui-ci:

bool isUserAMoron(User user) {
    Connection c = UnstableDatabase.getConnection();
    if (user.name == "Moron") { 
      // In this case we don't need to connect to DB
      return true;
    } else {
      return c.makeMoronishCheck(user.id);
    }
}

(bien sûr, il y a de nombreux problèmes dans cet exemple concernant la qualité du code. Désolé d'être paresseux d'imaginer un exemple parfait)

Situation où cne sera pas réellement utilisé et NullPointerExceptionne sera pas lancé même si c == nullc'est possible.

Dans des situations plus compliquées, il devient très difficile de traquer de tels cas. C'est pourquoi une vérification générale comme if (c == null) throw new NullPointerException()c'est mieux.

Arenim
la source
On peut soutenir qu'un morceau de code qui fonctionne sans connexion à une base de données alors que ce n'est pas vraiment nécessaire est une bonne chose, et le code qui se connecte à une base de données juste pour voir s'il ne peut pas le faire est généralement assez ennuyeux.
Dmitry Grigoryev
5

Il est intentionnel de protéger d'autres dommages ou d'entrer dans un état incohérent.

Fairoz
la source
1

En dehors de toutes les autres excellentes réponses ici, je voudrais également ajouter quelques cas.

Vous pouvez ajouter un message si vous créez votre propre exception

Si vous lancez le vôtre, NullPointerExceptionvous pouvez ajouter un message (ce que vous devriez certainement!)

Le message par défaut est un nullfrom new NullPointerException()et toutes les méthodes qui l'utilisent, par exemple Objects.requireNonNull. Si vous imprimez ce null, il peut même se traduire en une chaîne vide ...

Un peu court et peu informatif ...

La trace de la pile donnera beaucoup d'informations, mais pour que l'utilisateur sache ce qui était nul, il doit extraire le code et regarder la ligne exacte.

Imaginez maintenant que NPE soit enveloppé et envoyé sur le net, par exemple sous la forme d'un message dans une erreur de service Web, peut-être entre différents services ou même des organisations. Dans le pire des cas, personne ne peut comprendre ce que nullsignifie ...

Les appels de méthode en chaîne vous permettront de deviner

Une exception vous indiquera uniquement sur quelle ligne l'exception s'est produite. Considérez la ligne suivante:

repository.getService(someObject.someMethod());

Si vous obtenez un NPE et qu'il pointe sur cette ligne, lequel de repositoryet someObjectétait nul?

Au lieu de cela, vérifier ces variables lorsque vous les obtenez pointera au moins vers une ligne où elles sont, espérons-le, la seule variable gérée. Et, comme mentionné précédemment, encore mieux si votre message d'erreur contient le nom de la variable ou similaire.

Les erreurs lors du traitement d'un grand nombre d'entrées devraient donner des informations d'identification

Imaginez que votre programme traite un fichier d'entrée avec des milliers de lignes et qu'il y a soudainement une NullPointerException. Vous regardez l'endroit et réalisez que certaines entrées étaient incorrectes ... quelle entrée? Vous aurez besoin de plus d'informations sur le numéro de ligne, peut-être la colonne ou même le texte de la ligne entière pour comprendre quelle ligne de ce fichier doit être corrigée.

Erk
la source