La semaine dernière, j’étais à la conférence FOSDEM. Le FOSDEM a la particularité de disposer de plusieurs salles, chacune dédiée à un thème différent et organisée par une équipe. J’ai eu deux entretiens :
La deuxième conversation est d’un post précédent. Martin Bonnin a fait un tweet à partir d’une seule diapositive, et cela a fait sensation, attirant même Brian Goetz.
Dans cet article, j’aimerais développer le problème de nullabilité et comment il est résolu dans Kotlin et Java et ajouter mes commentaires au fil Twitter.
Nullabilité
Je suppose que tout le monde dans le développement de logiciels avec plus de deux ans d’expérience a entendu la citation suivante :
J’appelle ça mon erreur d’un milliard de dollars. C’était l’invention de la référence nulle en 1965. A cette époque, je concevais le premier système de type complet pour les références dans un langage orienté objet (ALGOL W). Mon objectif était de m’assurer que toute utilisation de références soit absolument sûre, avec une vérification effectuée automatiquement par le compilateur. Mais je n’ai pas pu résister à la tentation de mettre une référence nulle, simplement parce que c’était si facile à mettre en œuvre. Cela a conduit à d’innombrables erreurs, vulnérabilités et plantages du système, qui ont probablement causé un milliard de dollars de douleur et de dommages au cours des quarante dernières années.
-Tony Hoare
L’idée de base derrière null
c’est qu’on peut définir un variable non initialisée. Si l’on appelle un membre d’une telle variable, le runtime localise l’adresse mémoire de la variable… et ne parvient pas à la déréférencer car il n’y a rien derrière.
Les valeurs nulles se trouvent dans de nombreux langages de programmation sous différents noms :
- Python a
None
- JavaScript a
null
- Java, Scala et Kotlin aussi
- Rubis a
nil
- etc.
Certaines langues font pas autoriser les valeurs non initialisées, telles que Rust.
Sécurité nulle dans Kotlin
Comme je l’ai mentionné, Kotlin permet null
valeurs. Cependant, ils sont intégrés au système de type. Dans Kotlin, chaque type X
a deux en effet deux types :
-
X
, qui n’accepte pas la valeur Null. Aucune variable de typeX
peut êtrenull
. Le compilateur le garantit.Le code ci-dessus ne compilera pas.
-
X?
qui est nullable.Le code ci-dessus compile.
Si Kotlin le permet null
valeurs, pourquoi ses partisans vantent-ils sa sécurité nulle ? Le compilateur refuse d’appeler les membres sur possible valeurs nulles, c’est à diretypes nullables.
val str: String? = getNullableString()
val int: Int? = str.toIntOrNull() //1
- Ne compile pas
La façon de corriger le code ci-dessus est de vérifier si la variable est null
avant d’appeler ses membres :
val str: String? = getNullableString()
val int: Int? = if (str == null) null
else str.toIntOrNull()
L’approche ci-dessus est assez passe-partout, donc Kotlin propose l’opérateur null-safe pour obtenir la même chose :
val str: String? = getNullableString()
val int: Int? = str?.toIntOrNull()
Sécurité nulle en Java
Maintenant que nous avons décrit comment Kotlin gère null
valeurs, il est temps de vérifier comment Java le fait. Premièrement, il n’y a ni types non nullables ni opérateurs null-safe en Java. Ainsi, chaque variable peut potentiellement être null
et doit être considéré comme tel.
var MyString str = getMyString(); //1
var Integer anInt = null; //2
if (str != null) {
anInt = str.toIntOrNull();
}
String
n’a pastoIntOrNull()
méthode, alors faisons semblantMyString
est un type wrapper et délègue àString
- Une référence mutable est nécessaire
Si vous enchaînez plusieurs appels, c’est encore pire car chaque valeur de retour peut potentiellement être null
. Pour être sûr, nous devons vérifier si le résultat de chaque appel de méthode est null
. L’extrait suivant peut jeter un NullPointerException
:
var baz = getFoo().getBar().getBaz();
Voici la version corrigée mais beaucoup plus détaillée :
var foo = getFoo();
var bar = null;
var baz = null;
if (foo != null) {
bar = foo.getBar();
if (bar != null) {
baz = bar.getBaz();
}
}
Pour cette raison, Java 8 a introduit le type Optional. Optional
est un wrapper autour d’une valeur éventuellement nulle. D’autres langues l’appellent Maybe
, Option
etc.
Les concepteurs du langage Java conseillent qu’une méthode renvoie :
- Taper
X
siX
c’est pas possiblenull
- Taper
Optional
siX
peut êtrenull
Si nous changeons le type de retour de toutes les méthodes ci-dessus en Optional
nous pouvons réécrire le code de manière null-safe – et obtenir l’immuabilité en plus :
final var baz = getFoo().flatMap(Foo::getBar)
.flatMap(Bar::getBaz)
.orElse(null);
Mon principal argument concernant cette approche est que la Optional
lui-même pourrait être null
. La langue ne garantit pas que ce n’est pas le cas. Aussi, il est déconseillé d’utiliser Optional
pour les paramètres d’entrée de la méthode.
Pour faire face à cela, des bibliothèques basées sur des annotations ont fait leur apparition :
Projet | Emballer | Annotation non nulle | Annotation nullable |
---|---|---|---|
RSC 305 | javax.annotation |
@Nonnull |
@Nullable |
Printemps | org.springframework.lang |
@NonNull |
@Nullable |
JetBrains | org.jetbrains.annotations |
@NotNull |
@Nullable |
Trouver des bogues | edu.umd.cs.findbugs.annotations |
@NonNull |
@Nullable |
Éclipse | org.eclipse.jdt.annotation |
@NonNull |
@Nullable |
Cadre de vérificateur | org.checkerframework.checker.nullness.qual |
@NonNull |
@Nullable |
JSpécifier | org.jspecify |
@NonNull |
@Nullable |
Lombok | org.checkerframework.checker.nullness.qual |
@NonNull |
– |
Cependant, différentes bibliothèques fonctionnent de différentes manières :
- Spring produit des messages d’avertissement au moment de la compilation
- FindBugs nécessite une exécution dédiée
- Lombok génère du code qui ajoute une vérification nulle mais lance une
NullPointerException
si c’estnull
de toute façon - etc.
Merci à Sébastien Deleuze d’avoir mentionné JSpecify, que je ne connaissais pas auparavant. C’est un effort à l’échelle de l’industrie pour faire face au gâchis actuel. Bien sûr, la célèbre bande dessinée XKCD vient immédiatement à l’esprit :
J’espère quand même que ça va marcher !
Conclusion
Java a été créé lorsque null
– la sécurité n’était pas une grande préoccupation. Ainsi, NullPointerException
les événements sont fréquents. La seule solution sûre est d’envelopper chaque appel de méthode dans un null
vérifier. Cela fonctionne, mais c’est passe-partout et rend le code plus difficile à lire.
Plusieurs alternatives sont disponibles, mais elles ont des problèmes : elles ne sont pas à l’épreuve des balles, se font concurrence et fonctionnent très différemment.
Les développeurs louent Kotlin pour son null
-la sécurité : c’est le résultat de sa null
-mécanisme de gestion intégré dans la conception du langage. Java ne pourra jamais rivaliser avec Kotlin à cet égard, car les architectes du langage Java privilégient la rétrocompatibilité à la sécurité du code. C’est leur décision, et c’est probablement une bonne décision quand on se souvient de la douleur de la migration de Python 2 vers Python 3. Cependant, en tant que développeur, cela fait de Kotlin une option beaucoup plus attrayante que Java pour moi.
Pour aller plus loin:
Publié à l’origine sur A Java Geek le 12 févriere2023