Tutoriel pour comprendre plus facilement les NullPointerExceptions avec la JEP 358 (Nouveauté Java 14)

Lilian BENOIT

Cet article présente la JDK Enhancement Proposal (JEP) 358 (Helpful NullPointerExceptions) qui propose de gérer plus efficacement le NullPointerException.

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum Commentez .

- Titre : Tutoriel pour comprendre plus facilement les NullPointerExceptions avec la JEP 358 (Nouveauté Java 14)

- Auteur : Lilian BENOIT

- Parution : 1 décembre 2020

- Licence : Copyright ® 2020 Lilian BENOIT. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Contexte

- II. Mise en pratique

- II-A. Premier test

- II-B. Exemple plus concret

- II-C. Lecture du message

- II-D. Précision sur le message

- II-E. Exemple avec les tableaux

- II-F. Activation

- II-G. Approfondissement

- II-H. Comment tester ?

- III. Remerciements et références

- III-A. Liens supplémentaires

I - Contexte

En suivant l'actualité du projet OpenJDK, j'ai remarqué qu'une nouvelle JEP (JDK Enhancement Proposals) a été acceptée pour être incluse dans la version 14 du JDK.

Targeted to JDK 14 : JEP 358: Helpful NullPointerExceptions : openjdk.java.net/jeps/358 #jdk14 #openjdk #java

— OpenJDK
sur Twitter (2 octobre 2019)

Une proposition de plus, mais son petit nom ne me laisse pas indifférent. C'est la Helpful NullPointerExceptions, cela pourrait se traduire par NullPointerException utile. Qu'est-ce qui se cache dernière ce nom ?

En effet, quel développeur Java n'a pas été confronté à un NullPointerException sur son poste de développement ou en production ?

Initialement, pour illustrer cet article, j'étais parti sur une trace quelconque d'un NullPointerException. Mais, j'ai pu rencontrer un cas concret qui m'a permis de voir un vrai cas d'école. Je le présente ci-dessous :

1. DEBUG [org.apereo.cas.web.support.CookieRetrievingCookieGenerator] - <null>

2. java.lang.NullPointerException: null

3. at org.apereo.cas.web.support.EncryptedCookieValueManager.obtainCookieValue(EncryptedCookieValueManager.java:35) ~[cas-server-core-cookie-api-5.3.6.jar:5.3.6]

4. at org.apereo.cas.web.support.CookieRetrievingCookieGenerator.retrieveCookieValue(CookieRetrievingCookieGenerator.java:148) ~[cas-server-core-cookie-api-5.3.6.jar:5.3.6]

5. at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_40]

6. at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_40]

7. at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_40]

8. at java.lang.reflect.Method.invoke(Method.java:497) ~[?:1.8.0_40]

De plus, comme pour toute exception, il est normalement possible d'avoir un message.

Returns the detail message string of this throwable.
Returns: the detail message string of this Throwable instance (which may be null).

— getMessage
Extrait de la Javadoc

Mais comme d'habitude, nous obtenons le message suivant :

java.lang.NullPointerException: null

La seule information que nous avons pour l'instant est que cela se passe sur la ligne 35. En regardant le code, nous avons :

1. public final String obtainCookieValue(final Cookie cookie, final HttpServletRequest request) {

2. final String cookieValue = cipherExecutor.decode(cookie.getValue(), new Object[]{}).toString();

3. LOGGER.debug("Decoded cookie value is [{}]", cookieValue);

4. if (StringUtils.isBlank(cookieValue)) {

5. LOGGER.debug("Retrieved decoded cookie value is blank. Failed to decode cookie [{}]", cookie.getName());

6. return null;

7. }

8. return obtainValueFromCompoundCookie(cookieValue, request);

9. }

Sans plus d'information, nous avons plusieurs possibilités sur cette ligne :

- soit l'attribut cipherExecurtor est null ;

- soit le paramètre cookie est null ;

- soit la méthode decode(…) renvoie null.

II - Mise en pratique

II-A - Premier test

Passons à la mise en pratique, pour cela, nous allons commencer par un code simple

1. @Test (expected=NullPointerException.class)

2. public void testSimple() {

3. try {

4. String str = null;

5. str.length();

6. } catch (NullPointerException e) {

7. e.printStackTrace();

8. throw e;

9. }

10. }

Je vous l'accorde, c'est facile, même l'IDE me dit : la variable str est de valeur nulle. Mais voyons ensemble le résultat obtenu.

On commence par faire tourner avec un JDK classique. En l'occurrence, je prends un JDK 11. Voici le message obtenu (classique et malheureusement bien connu)

1. -------------------------------------------------------

2. T E S T S

3. -------------------------------------------------------

4. Running fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulTest

5. java.lang.NullPointerException

6. at fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulTest.testSimple(NullPointerExceptionHelpulTest.java:11)

7. at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

8. at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)

9. at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

En utilisant un JDK intégrant cette fonctionnalité, nous obtenons le message suivant :

1. -------------------------------------------------------

2. T E S T S

3. -------------------------------------------------------

4. Running fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulTest

5. java.lang.NullPointerException: 'str' is null. Can not invoke method 'int java.lang.String.length()'.

6. at fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulTest.testSimple(NullPointerExceptionHelpulTest.java:11)

7. at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

8. at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)

9. at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

10. at java.base/java.lang.reflect.Method.invoke(Method.java:567)

Le message est déjà plus clair.

II-B - Exemple plus concret

Prenons la méthode suivante :

public boolean isVide(StringHolder holder) {
return (holder.getChaine().length() > 0);
}

Si je lance le test avec un JDK 11, nous obtenons le message suivant :

1. Running fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulHolderTest

2. java.lang.NullPointerException

3. at fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulHolderTest.isVide(NullPointerExceptionHelpulHolderTest.java:19)

4. at fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulHolderTest.testHolder(NullPointerExceptionHelpulHolderTest.java:11)

Nous sommes déjà sur un cas plus délicat. Nous avons deux possibilités :

- soit le paramètre holder est null ;

- soit la méthode holder.getChaine() renvoie null.

En revanche, on obtient un message très clair avec la nouvelle fonctionnalité :

1. Running fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulHolderTest

2. java.lang.NullPointerException: The return value of 'java.lang.String fr.lbenoit.billets.codes_sources.StringHolder.getChaine()' is null. Can not invoke method 'int java.lang.String.length()'.

3. at fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulHolderTest.isVide(NullPointerExceptionHelpulHolderTest.java:19)

4. at fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulHolderTest.testHolder(NullPointerExceptionHelpulHolderTest.java:11)

II-C - Lecture du message

En réalité, le message est composé de deux parties :

The return value of 'java.lang.String fr.lbenoit.billets.codes_sources.StringHolder.getChaine()' is null

— c'est le retour de la méthode getChaine() qui renvoie null
Can not invoke method 'int java.lang.String.length()'

— nous ne pouvons pas invoquer la méthode length().

- La première partie est la raison du NullPointerException.

- La seconde est la conséquence de l'exception.

II-D - Précision sur le message

Les tests ont été réalisés avec le prototype de mars 2019. Il est disponible sur la branche JEP-8220715-NPE_messages. Cependant, dans le cadre de la JEP, les deux parties du message seront inversées. Cela donnera le message suivant :

Can not invoke method 'int java.lang.String.length()' because The return value of 'java.lang.String fr.lbenoit.billets.codes_sources.StringHolder.getChaine()' is null

II-E - Exemple avec les tableaux

Prenons le cas de manipulation de tableau, en ligne 17, nous avons l’instruction suivante :

1. tab[i][j][k] = 25;

En lançant le test avec un JDK 11, nous obtenons le message suivant :

Running fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulTableauTest
java.lang.NullPointerException
at fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulTableauTest.testTableau(NullPointerExceptionHelpulTableauTest.java:17)

En regardant le code, nous nous posons la question sur l'origine :

- soit c'est tab qui est null ;

- soit c'est tab[i] qui est null ;

- soit c'est tab[i][j] qui est null.

Là encore, la réponse est très claire avec un JDK incluant cette fonctionnalité

'tab[i][j]' is null. Can not store to null int array.

En replaçant le message dans la pile d'appels, nous obtenons la trace suivante :

1. -------------------------------------------------------

2. T E S T S

3. -------------------------------------------------------

4. Running fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulTableauTest

5. java.lang.NullPointerException: 'tab[i][j]' is null. Can not store to null int array.

6. at fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulTableauTest.testTableau(NullPointerExceptionHelpulTableauTest.java:17)

II-F - Activation

Dans le cas de la réalisation de cette fonctionnalité, il faut noter qu'elle ne sera pas activée par défaut. Deux raisons sont avancées :

- fuite d'informations dans les traces ;

- impact sur les parseurs des journaux.

Donc, pour l'activer, il faudra ajouter l'option suivante :

XX:{+|-}ShowCodeDetailsInExceptionMessages

Elle sera activée par défaut dans une prochaine version.

II-G - Approfondissement

En étudiant la proposition d'évolution, nous pouvons voir que l'exception peut provoquer en réalité plusieurs conséquences :

- ne peut pas charger un élément du tableau (« Cannot load from <element type> array ») ;

- ne peut pas lire la longueur du tableau (« Cannot read the array length ») ;

- ne peut pas enregistrer l'élément du tableau (« Cannot store to <element type> array ») ;

- ne peut pas lever une exception (« Cannot throw exception ») ;

- ne peut pas lire un attribut (« Cannot read field '<field name>' ») ;

- ne peut pas appeler une méthode (« Cannot invoke '<method>' ») ;

- ne peut pas entrer dans un bloc synchronisé (« Cannot enter synchronized block ») ;

- ne peut pas sortir d'un bloc synchronisé (« Cannot exit synchronized block ») ;

- ne peut pas affecter l'attribut (« Cannot assign field '<field name>' »).

II-H - Comment tester ?

Le projet Java contenant les cas de test présentés dans ce billet est disponible sur GitHub.

- Dépôt GitHub billets-codes-sources

III - Remerciements et références

Cet article a été publié avec l'aimable autorisation de Lilian Benoit. L'article original (JEP 358 Helpful NullPointerExceptions) peut être vu sur le blog https://www.lilian-benoit.fr/.

Nous tenons à remercier escartefigue pour sa relecture attentive de cet article et Winjerome pour la mise au gabarit.

- Annonce Twitter

- JEP 358

- Fiche associée à la JEP 358

- Branche JEP-8220715-NPE_messages

- Résultat des tests

III-A - Liens supplémentaires

- Annonce de la proposition sur la liste de diffusion jdk-dev

- Proposition acceptée sur la liste de diffusion jdk-dev