web-dev-qa-db-fra.com

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

Lors de la lecture du code source de JDK, il est courant que l'auteur vérifie les paramètres s'ils sont nuls, puis lance manuellement la nouvelle NullPointerException (). Pourquoi font-ils cela? Je pense qu'il n'est pas nécessaire de le faire car il lancera une nouvelle NullPointerException () lorsqu'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;
}
181
LiJiaming

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

Fail-fast: En cas d'échec, mieux vaut échouer plus tôt que plus tard. Cela permet de détecter les problèmes 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 voué à l'échec.

Intention: Le lancement de l'exception indique clairement aux responsables que l'erreur est commise intentionnellement et que l'auteur était conscient des conséquences.

Consistance: Si l'erreur devait se produire naturellement, il est possible qu'elle ne se produise pas dans tous les scénarios. Si aucun mappage n'est trouvé, par exemple, remappingFunction ne 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 plus clair documentation .

Stabilité: Le code évolue dans le temps. Le code qui rencontre une exception peut naturellement, après un peu de refactoring, cesser de le faire ou le faire dans des circonstances différentes. En le lançant explicitement, il est moins probable que le comportement change par inadvertance.

253
shmosel

C’est pour des raisons de clarté, de cohérence et pour éviter tout travail inutile et inutile.

Pensez à ce qui se produirait 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 lorsque null aurait été passé pour le remappingFunction avant le lancement du NPE.

Pire encore, si la condition if est false, nous prenons la branche else, qui n'utilise pas du tout remappingFunction, ce qui signifie que la méthode n'est pas toujours lancer NPE quand un null est passé; cela dépend de l'état de la carte.

Les deux scénarios sont mauvais. Si null n'est pas une valeur valide pour remappingFunction, la méthode doit systématiquement émettre une exception, quel que soit l'état interne de l'objet au moment de l'appel, sans effectuer de travail inutile. inutile étant donné qu'il va juste jeter. Enfin, un bon principe de code propre et clair consiste à placer la protection à l’avant-garde de sorte que toute personne examinant le code source puisse facilement s’assurer qu’elle le fera.

Même si les exceptions étaient actuellement levées par chaque branche de code, il est possible qu'une révision future du code change cela. Effectuer la vérification au début garantit que celle-ci sera définitivement effectuée.

39
David Conrad

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

Performance: Il peut y avoir eu/des avantages en termes de performances (sur certaines machines virtuelles) à lancer explicitement le NPE plutôt que de laisser la machine virtuelle 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 plutôt intercepter le SIGSEGV qui se produit lorsqu'une instruction tente 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 l'est toujours cher dans le cas NPE.

Un test explicite de null dans le code permettrait d'éviter les problèmes de performance SIGSEGV dans un scénario où les NPE étaient fréquents.

(Je doute que ce soit une micro-optimisation intéressante dans une machine virtuelle moderne, mais cela aurait pu être le cas par le passé.)


Compatibilité: La raison probable pour laquelle il n'y a pas de message dans l'exception est la compatibilité avec les NPE générés par la machine virtuelle elle-même. Dans une implémentation Java conforme, un NPE envoyé par la machine virtuelle Java contient un message null. (Android Java est différent.)

25
Stephen C

En dehors de ce que d’autres personnes ont souligné, il convient de noter le rôle de la convention ici. En C #, par exemple, vous avez également la même convention de lever explicitement une exception dans des cas comme celui-ci, mais il s'agit spécifiquement d'un ArgumentNullException, qui est un peu plus spécifique. (La convention C # veut que NullReferenceExceptiontoujours représente un bogue - tout simplement, cela ne devrait pas jamais se produire dans le code de production; accordé, ArgumentNullException le fait généralement aussi, mais il pourrait s'agir d'un bogue plus ou moins similaire à celui du type "vous ne comprenez pas comment utiliser la bibliothèque correctement").

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

Incidemment, si vous élevez quelque chose comme ArgumentNullException ou IllegalArgumentException, cela fait partie de la vérification: vous voulez une exception différente de celle que vous obteniez "normalement".

Quoi qu'il en soit, le fait de lever explicitement l'exception renforce la bonne pratique consistant à expliquer explicitement les conditions préalables 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 explicitement recherché null, je ne sais pas si c'est parce que vous pensiez que personne ne passerait jamais un argument null, vous le comptez quand même pour lever l'exception, ou vous avez juste oublié de vérifier cela.

20
EJoshuaS

C’est ainsi que vous obtiendrez l’exception dès que vous aurez commis l’erreur, plutôt que plus tard, lorsque vous utiliserez la carte, sans comprendre pourquoi.

12
user207421

Cela transforme une condition d'erreur apparemment erratique en une violation de contrat claire: la fonction a certaines conditions préalables à son bon fonctionnement; elle les vérifie donc au préalable, en veillant à ce qu'elles soient remplies.

L'effet est que vous n'aurez pas à déboguer computeIfPresent() lorsque vous aurez l'exception. Une fois que vous voyez que l'exception provient de la vérification de la condition préalable, vous savez que vous avez appelé la fonction avec un argument illégal. Si la vérification n'y était pas, vous auriez besoin d'exclure la possibilité qu'il y ait un bogue dans computeIfPresent() lui-même qui conduise à la levée de l'exception.

Évidemment, lancer le générique NullPointerException est un très mauvais choix, car il ne signale pas une violation de contrat en soi. IllegalArgumentException serait un meilleur choix.


Sidenote:
Je ne sais pas si Java le permet (j'en doute), mais les programmeurs C/C++ utilisent une assert() dans ce cas, ce qui est nettement préférable pour le débogage: indique au programme de planter immédiatement et aussi fort que possible si la condition fournie est évaluée à false. Donc, si vous avez couru

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

    ...
}

sous un débogueur et que quelque chose passe NULL dans l'un ou l'autre argument, le programme s'arrête à la ligne indiquant quel argument est NULL et vous pouvez examiner toutes les variables locales de la pile d'appels entière à loisir.

9
cmaster

C'est parce qu'il est possible que pas se produise naturellement. Voyons un morceau de code comme ceci:

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, cet échantillon présente de nombreux problèmes concernant la qualité du code. Désolé de paresseux pour imaginer un exemple parfait)

La situation où c ne sera pas réellement utilisée et NullPointerException ne sera pas levée même si c == null est possible.

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

7
Arenim

Il est intentionnel de protéger d’autres dommages ou d’être dans un état incohérent.

5
Fairoz