1 points par GN⁺ 2024-08-04 | 1 commentaires | Partager sur WhatsApp
  • Java prépare une fonctionnalité de langage en aperçu qui ajoute des marqueurs de nullité aux types : Foo! refuse null, tandis que Foo? l’autorise intentionnellement
  • Un Foo non marqué conserve une nullité non spécifiée, pour rester compatible avec le code existant ; les conversions entre types de nullité différente peuvent s’accompagner d’avertissements ou de vérifications à l’exécution
  • Les champs et tableaux qui refusent null ne peuvent pas utiliser null comme valeur par défaut : les champs d’instance doivent être définitivement assignés avant l’appel à super(...), et les tableaux doivent recevoir une valeur initiale pour leurs composants
  • Si null est restreint vers Foo!, une NullPointerException est levée ; dans les chemins de stockage des tableaux et des champs, ArrayStoreException et FieldStoreException empêchent la contamination
  • C’est une base permettant à des optimisations comme l’aplatissement des value classes de Valhalla de se fier à l’exclusion de null ; l’application à la bibliothèque standard ou la réinterprétation automatique du code existant ne sont pas des objectifs actuels

Une fonctionnalité en aperçu pour exprimer la nullité dans les types Java

  • Ajoute aux types Java des marqueurs de nullité pour indiquer si l’ensemble des valeurs d’un type inclut null
  • Foo! est un type null-restricted, qui exclut null de son ensemble de valeurs
  • Foo? est un type nullable, qui inclut intentionnellement null dans son ensemble de valeurs
  • Un Foo non marqué a une nullité unspecified : null peut apparaître, mais on ne sait pas si sa présence est intentionnelle
  • Cette fonctionnalité est une fonctionnalité en aperçu, activée avec les options de compilation et d’exécution --enable-preview

Objectifs et non-objectifs

  • Permettre d’exprimer si un type référence Java attend null, et fournir des avertissements et des vérifications pour les conversions entre types ayant des nullités différentes
  • Rester compatible avec le code Java existant, et pouvoir être introduit progressivement sans créer d’incompatibilités source ou binaire
  • Les variables de types refusant null doivent être initialisées avant leur première lecture, et le refus de null est imposé à l’exécution même pour des classes compilées séparément
  • Fournir les métadonnées et les garanties d’intégrité nécessaires pour que les optimisations à l’exécution, comme l’aplatissement des value classes de Valhalla, puissent se fier aux types à null restreint
  • Les points suivants ne sont pas des objectifs actuels
    • Ne pas réinterpréter automatiquement le code existant
    • Ne pas transformer tous les null possibles en erreurs de compilation
    • Ne pas ajouter de forme nullable aux types primitifs comme int
    • Ne pas appliquer, à ce stade, l’extension du langage à la bibliothèque standard

Pourquoi c’est nécessaire

  • Une variable String en Java peut contenir une référence vers un objet String ou null, mais le langage ne permet pas d’exprimer laquelle des deux possibilités est intentionnelle
  • Beaucoup de programmes supposent l’absence de null, mais un travail supplémentaire est nécessaire pour imposer cette contrainte de manière cohérente dans les spécifications Javadoc et le code d’implémentation
  • Quand cette attente est rompue, une valeur null peut circuler dans le code d’implémentation puis provoquer une exception à un endroit très éloigné du bug d’origine
  • Si les développeurs expriment, comme partie du type, leur intention de refuser ou d’autoriser null, le feedback à la compilation et les vérifications à l’exécution aident à détecter plus tôt les null inattendus
  • Dans Valhalla, les variables de type value class peuvent être optimisées sous forme de représentation aplatie des valeurs, mais si des bits supplémentaires sont nécessaires pour encoder null, la consommation mémoire peut augmenter ou l’optimisation du stockage devenir impossible
  • Dans Amber, la nullité des candidats au pattern matching peut influencer le jugement d’exhaustivité d’un switch, et la nullité des patterns de type peut déterminer si null est pris en compte ou non

Syntaxe des marqueurs de nullité et structure des types

  • La nullité est traitée comme une partie intrinsèque du type ; Foo? et Foo ont une nullité différente et sont donc des types distincts
  • Les types tableau comme les types de composant de tableau peuvent tous deux avoir des marqueurs de nullité
    • Foo?[]! désigne un type où le tableau lui-même est null-restricted, et où les composants sont des Foo nullable
    • Dans les tableaux multidimensionnels, un marqueur peut être ajouté après chaque paire de crochets ; par convention, l’interprétation va de gauche à droite, de l’extérieur vers l’intérieur
  • Les types paramétrés et les arguments de type peuvent également avoir des marqueurs de nullité
    • Predicate!<Foo?> est un Predicate null-restricted, et son argument de type est un Foo nullable
  • Pour exprimer un type null-restricted ou nullable, ! ou ? doit apparaître explicitement dans le source
  • À l’avenir, il pourrait être envisagé d’interpréter par défaut tous les types d’une classe ou d’une unité de compilation comme null-restricted, avec ? comme seule exception, mais les détails relèvent d’un travail séparé

Règles d’initialisation des champs et des tableaux

  • En Java existant, la valeur par défaut des champs de type référence et des composants de tableau est null, mais elle ne convient pas comme valeur initiale d’un champ ou composant de tableau null-restricted
  • Les champs et tableaux null-restricted doivent toujours être initialisés par le programme avant d’être lus
  • Si un champ d’instance null-restricted n’a pas d’initialiseur, il doit être définitivement assigné avant l’appel explicite ou implicite à super(...) dans chaque constructeur
    • Le JEP Flexible Constructor Bodies permet d’écrire le code d’initialisation nécessaire au début d’un constructeur
    • Dans cet early construction context, il n’est pas permis de référencer this ni d’effectuer des opérations risquant de lire des champs non initialisés
  • Si un champ d’instance null-restricted possède un initialiseur, celui-ci est exécuté au début de chaque constructeur, avant l’appel à super(...)
    • Les constructeurs qui appellent this(...) constituent un cas particulier, comme dans les règles existantes : ils n’exécutent pas les initialiseurs
  • Les champs statiques null-restricted doivent être définitivement assignés avant la fin de tous les initialiseurs statiques et blocs d’initialisation de la classe
    • Si une autre classe tente de lire un tel champ pendant l’initialisation de la classe, une vérification à l’exécution détecte la lecture prématurée et lève une exception
  • Un tableau dont le type de composant est null-restricted doit fournir une valeur initiale pour chaque composant dans l’expression de création du tableau
    • Toutes les valeurs peuvent être listées avec un initialiseur de tableau
    • Une nouvelle syntaxe abrégée est aussi possible, mais sa syntaxe reste TBD

Nullité des expressions et conversions de nullité

  • Le compilateur Java détermine la nullité de toutes les expressions pendant la vérification de type
  • La nullité d’une référence à une variable vient de la déclaration de cette variable, et la nullité d’un appel de méthode vient du type de retour de la méthode référencée
  • Le littéral null est nullable
  • La plupart des autres expressions de type référence sont null-restricted
    • Cela inclut les littéraux, la concaténation de chaînes, this, la création d’instance de classe, la création de tableau, les références de méthode et les expressions lambda
  • Les conversions de nullité sont autorisées dans les contextes d’affectation, d’appel et de cast
  • Les conversions de nullité élargissantes incluent notamment
    • Foo!Foo?
    • Foo!Foo non spécifié
    • Foo?Foo non spécifié
    • Foo non spécifié → Foo?
  • Les conversions de nullité rétrécissantes incluent notamment
    • Foo?Foo!
    • Foo non spécifié → Foo!
  • Comme pour les conversions d’unboxing, le compilateur effectue automatiquement les conversions de nullité rétrécissantes, mais une vérification dynamique est effectuée à l’exécution et peut provoquer une NullPointerException
  • Toute tentative de convertir directement le littéral null vers un type null-restricted est une erreur de compilation

Vérifications à l’exécution et exceptions

  • À l’exécution, si une valeur null subit une conversion de nullité rétrécissante vers un type null-restricted, une NullPointerException est levée
  • Des conversions de nullité rétrécissantes non visibles explicitement dans le code source peuvent aussi se produire pendant l’exécution
  • Un tableau assigné avec un type de composant null-restricted refuse les valeurs null via les vérifications habituelles de stockage de tableau, même s’il est manipulé dans le code source avec un type moins spécifique
    • Cet échec de conversion provoque une ArrayStoreException
  • Si un champ qui n’était pas null-restricted au moment de la compilation le devient ensuite via une compilation séparée, un nouveau field store check refuse le stockage de null
    • Cet échec de conversion provoque une FieldStoreException
  • Dans les appels de méthodes en relation d’override, une conversion vers le type d’appel du paramètre de la méthode parente peut être suivie d’une conversion vers le type de paramètre de la méthode qui override
  • Les valeurs de retour des méthodes peuvent aussi être converties vers le type de retour déclaré de la méthode, puis vers le type de retour attendu au point d’appel

Génériques, arguments de type et override

  • Les utilisations de variables de type peuvent aussi recevoir des marqueurs de nullité ; T! est un type null-restricted et T? un type nullable
  • Les types de variables de type null-restricted et nullable affirment une nullité spécifique à l’intérieur du code générique
  • Les types utilisés comme arguments de type peuvent aussi exprimer leur nullité, et les marqueurs de nullité attachés à un type de variable de type remplacent la nullité affirmée par l’argument de type
  • À l’intérieur de l’implémentation erased d’une API générique, la restriction de nullité ne peut pas être imposée
    • En revanche, les casts implicites habituels qui se produisent aux frontières des API génériques imposent à l’exécution les arguments de type null-restricted
  • Pour l’interopérabilité, la nullité dans les arguments de type n’est pas fortement imposée
    • Predicate<String!> peut être converti en Predicate<String> ou Predicate<String?>
    • Cette unchecked nullness conversion peut provoquer un avertissement
  • Changer la nullité du type de composant d’un tableau est également autorisé comme unchecked nullness conversion ; les conditions dans lesquelles une vérification à l’exécution est effectuée restent TBD
  • La nullité est ignorée pour déterminer l’identité des signatures de méthodes
    • Une méthode peut en override une autre même si la nullité de leurs paramètres et de leurs types de retour ne correspond pas
    • Si des API différentes introduisent indépendamment des marqueurs de nullité, ce genre d’incohérence peut être fréquent
  • La nullité n’affecte pas l’applicabilité des méthodes et ne peut pas provoquer l’échec de l’inférence des arguments de type, mais elle peut influencer la nullité inférée pour le type de retour d’une méthode générique
    • Les détails de l’algorithme d’inférence restent TBD

Avertissements et erreurs du compilateur

  • Rendre un type null-restricted peut créer de nouvelles erreurs de compilation
    • Lorsque les champs ou tableaux de ce type ne sont pas initialisés
    • Lorsqu’on tente de convertir le littéral null vers ce type
    • La comparaison du littéral null avec une expression de type null-restricted peut aussi devenir une erreur de compilation
  • Dans les autres situations, l’analyse de nullité est auxiliaire et ne crée pas d’erreurs de compilation
  • javac fournit des avertissements pour éviter les erreurs à l’exécution, et il est recommandé aux IDE et autres outils d’analyse d’aller dans le même sens
  • Les causes possibles d’avertissement incluent
    • Les conversions de nullité rétrécissantes, y compris depuis des types ?
    • L’utilisation d’expressions de type ? pour l’accès à des membres ou d’autres opérations null-hostile
    • Une nullité des arguments de type incohérente avec les bornes
    • Une nullité des paramètres ou des retours de méthodes incompatible avec celle des méthodes overridées
    • Les unchecked conversions qui changent la nullité d’un type

Fichiers de classe, réflexion et changements auxiliaires

  • La plupart des utilisations de marqueurs de nullité sont effacées dans les fichiers class, et les conversions d’exécution associées sont exprimées directement dans le bytecode
  • La syntaxe de l’attribut Signature est mise à jour pour autoriser ! et ? dans les types
  • La nullité n’est pas encodée dans les descriptors des méthodes et des champs
  • Pour empêcher la contamination des champs, un nouvel attribut NullRestricted indique qu’un champ n’autorise pas la valeur null
    • Ce champ doit aussi être marqué ACC_STRICT, et doit être strictement initialisé
    • Le vérificateur confirme que tous les champs d’instance à initialisation stricte ont été assignés au moment où le constructeur appelle super(...)
    • Toute tentative d’écriture dans le champ vérifie la valeur null et, si elle est détectée, lève une FieldStoreException
  • La création de tableaux null-restricted n’est pas prise en charge par l’instruction anewarray et doit être effectuée via un appel à l’API de réflexion
  • Il n’existe pas de littéraux Foo!.class ou Foo?.class, ni d’instances java.lang.Class correspondantes
  • La nouvelle RuntimeType API décrit l’ensemble des types imposés à l’exécution dans les vérifications de stockage des tableaux et des champs, et inclut les variantes null-restricted de tous les types classe et interface
  • L’API Field prend en charge la consultation du RuntimeType d’un champ, qui peut différer du résultat de getType
  • L’API Array prend en charge une variante de newInstance qui exprime le type de composant avec RuntimeType
    • Cette variante permet aussi de fournir des valeurs initiales pour les composants du tableau
    • Elle refuse toute tentative de création d’un tableau null-restricted sans valeur initiale
  • La désérialisation traditionnelle n’est pas compatible avec les champs et tableaux null-restricted ; un JEP séparé fournira un mécanisme de sérialisation qui n’expose pas de champs ni de tableaux null-restricted non initialisés
  • La documentation générée par javadoc inclut les marqueurs de nullité
  • Les API java.lang.reflect.Type et javax.lang.model encodent la nullité dans la représentation des types

Alternatives et dépendances

  • Plusieurs outils de développement de l’écosystème Java ont implémenté leur propre suivi de null à la compilation, mais comme ils ne modifient pas le langage Java, leur syntaxe est principalement limitée aux annotations et les comportements qu’ils peuvent influencer se limitent aux vérifications à la compilation
  • D’autres langages suivent la nullité dans le système de types, et beaucoup sont null-restricted par défaut, considérant comme une erreur l’affectation à un type null-restricted sans vérification explicite de null
  • En Java, la fonctionnalité doit rester optionnelle et pouvoir être adoptée progressivement, sans migration massive unique
  • L’application de la nullité à l’exécution peut aussi être implémentée par des vérifications explicites ou des appels à Objects.requireNonNull
    • Mais une application cohérente est fastidieuse, nécessite une documentation supplémentaire et nuit à la lisibilité du programme
    • Il n’existe pas de moyen de l’appliquer directement aux stockages de variables comme les champs et les tableaux
  • Le prérequis est Flexible Constructor Bodies (Second Preview)
    • Les constructeurs peuvent exécuter des instructions avant l’appel à super(...) et assigner des champs d’instance, ce qui rend possible l’exigence d’initialisation des champs null-restricted
  • Les travaux futurs incluent
  • Les améliorations futures possibles incluent l’application de marqueurs de nullité à certaines API standard, une représentation concise des null checks dans le bytecode, une application bas niveau plus forte des paramètres de méthode null-restricted, et un mécanisme de langage affirmant implicitement que tous les types de certains contextes sont null-restricted

1 commentaires

 
GN⁺ 2024-08-04
Avis sur Hacker News
  • Je trouve intéressant de voir en quoi cette approche diffère de celle que C# a introduite il y a quelques années. En C#, quand on active la nullabilité dans un projet, toutes les variables sont déclarées non nulles sauf si elles sont explicitement marquées comme nullable ; dans cette proposition, les variables existantes deviennent en pratique soit nullable, soit explicitement nullable, soit explicitement non-nullable
    Kotlin est lui aussi un langage JVM, mais comme C#, il considère les types comme non nuls s’ils ne sont pas explicitement indiqués autrement, avec toutefois l’avantage de ne pas porter le poids de la rétrocompatibilité. Il propose aussi une échappatoire avec lateinit var, qui permet de laisser un type non-nullable vide jusqu’à son initialisation par une autre méthode, et lève une exception dédiée si on y accède avant l’initialisation
    Je me demande pourquoi il y a trois options. Ne pourrait-on pas garder les variables non annotées comme nullable, et ne rendre explicitement non-nullables que les variables annotées ? Si, en l’absence de marqueur, c’est automatiquement nullable, je ne vois pas vraiment pourquoi on voudrait déclarer explicitement nullable
    Je préfère l’approche de C#, mais celle-ci a l’avantage de pouvoir être utilisée dans une base de code legacy sans devoir résoudre tous les problèmes de nullabilité. À l’inverse, C# fait apparaître immédiatement les problèmes de nullabilité, alors que cette proposition permet de continuer à les masquer comme jusqu’ici
    Par ailleurs, la partie disant qu’« une méthode peut en surcharger une autre même si la nullité de ses paramètres et de sa valeur de retour ne correspond pas » me paraît étrange. En surchargeant/implémentant un callback dont la méthode d’origine spécifie un retour non nul, cela risque de devenir un piège à se tirer une balle dans le pied si l’on renvoie null

    • Dans mon entreprise actuelle, nous migrons une base de code C# legacy vers les types référence nullable, et disposer d’un marqueur non-nullable explicite serait vraiment pratique pour savoir immédiatement quelles parties ont déjà été examinées et annotées
      Par manque de temps, nous avons pour l’instant choisi d’activer seulement les annotations, sauf à quelques endroits clés, mais nous conservons encore les NotNullAttribute et CanBeNullAttribute de JetBrains comme marqueurs afin de repérer tout de suite les endroits où une décision consciente a été prise. Le second pourrait être supprimé puisque nullable a un marqueur explicite, mais le premier entre en conflit de nom avec la fonctionnalité native de C#
      En ce sens, ces trois options sont plutôt souhaitables. Quand le code compte des centaines de milliers de lignes, il est difficile de migrer vite et facilement
      Dans d’autres projets internes, nous élargissons progressivement le périmètre de nullabilité en ajoutant des #nullable enable autour du code que nous touchons. Nous avons aussi posé comme condition que le nouveau code soit dans un contexte nullable. C’est également acceptable pour indiquer les zones déjà annotées, mais c’est une approche possible avec une base de code et une équipe beaucoup plus petites
    • Kotlin possède aussi des types dont la nullabilité n’est pas spécifiée, même si on ne peut pas les définir directement. Kotlin les appelle des types de plateforme (platform types) et les affiche dans les erreurs et diagnostics avec un point d’exclamation, comme String!
      Le terme « types de plateforme » et le symbole point d’exclamation prêtent à confusion, mais à part cela l’approche de Kotlin fonctionne plutôt bien. Comme Kotlin a eu la nullabilité dès le départ, les programmeurs ne peuvent pas spécifier directement des types de plateforme, mais ils restent nécessaires pour être compatibles avec des plateformes sous-jacentes où la nullabilité est ambiguë, comme la JVM ou JavaScript
      Dans cette approche, la valeur par défaut reste raisonnable. Par défaut, cela devrait toujours être non-nullable, et ceux qui pensent autrement n’ont rien appris de Tony Hoare. En même temps, la rétrocompatibilité est préservée. Kotlin avait la tâche relativement facile, tandis que Java et C# doivent aussi préserver la compatibilité avec le code source existant
      Ni l’approche de Java ni celle de C# ne sont idéales. Celle de C# modifie fortement le comportement du code selon un flag du compilateur, et celle de Java fait du pire choix la valeur par défaut
      Je penche tout de même plutôt du côté de C#. Si l’on fait de « probablement nullable » le choix le plus facile, la plupart des programmeurs le choisiront par défaut. Ce sera particulièrement vrai dans un langage aussi orienté entreprise que Java. Les linters et les avertissements du compilateur aideront à long terme, mais il faudra probablement des années avant que la plupart du code Java soit correctement annoté pour la nullabilité. Les utilisateurs de C# souffriront davantage à court terme, mais ils ont bien plus de chances d’atteindre rapidement l’objectif d’une nullabilité claire
      https://kotlinlang.org/docs/java-interop.html#null-safety-an...
      https://www.infoq.com/presentations/Null-References-The-Bill...
    • Je suis d’accord pour dire que c’est étrange. L’approche à laquelle je m’attendais était qu’au niveau du code source, T? signifie nullable, T! signifie non-nullable, et que le T ordinaire dans les sources nouvellement compilées ait une valeur par défaut définie par une déclaration façon pragma, soit par fichier source, soit par fichier package-info, soit globalement via une option du compilateur
      Ensuite, une version LTS ultérieure de Java pourrait changer la valeur par défaut globale. Cela faciliterait la transition des projets, permettrait d’insérer ou de mettre à jour automatiquement le pragma, et de conserver l’ancien comportement par défaut si nécessaire
      En outre, si l’annotation JSR-305 existante @Nonnull avait été utilisée dans les fichiers de classe pour représenter l’apparition de types non-nullables, cela aurait aussi pu offrir une compatibilité bidirectionnelle avec les anciens JDK
    • Vers la fin, il y a une section qui dit : « fournir un mécanisme par lequel le langage affirme que tous les types d’un certain contexte sont implicitement soumis à une restriction de nullité, sans que le programmeur ait à écrire explicitement le symbole ! »
      Même un simple flag du compilateur ou une balise de module pourrait suffire
    • La raison est probablement la rétrocompatibilité du code, que Java valorise bien plus que d’autres langages
      Cela dit, je pense qu’il y aura quand même une option du compilateur supposant automatiquement les types non-nullables lorsqu’aucune indication distincte n’est fournie
  • Ça a l’air bien. Enfin un moyen, au niveau du langage, de supprimer des milliers d’exceptions inutiles et de vérifications de null. En revanche, la conversion automatique qui restreint la nullabilité me paraît mal conçue
    Dans les exemples de la proposition, String? id(String! arg) { return arg; }, String s = null;, Object! o1 = s; // NPE, Object o2 = id(s); // NPE, Object o3 = (String!) s; // NPE, il me semble qu’au moins les deux premiers cas devraient être des erreurs de compilation
    Le dernier est plus ambigu parce qu’il est explicite, mais je préférerais que, dans un if (s != null), le compilateur sache que le type effectif est String! et autorise String! ss = s;. Dans ce cas, il n’y aurait pas de possibilité d’erreur

    • Pour avoir utilisé Java dans l’industrie, je préfère que les vérifications dynamiques n’aient lieu que lorsqu’elles sont explicitement demandées, et que tout le reste soit traité statiquement. Bean Validation fonctionne bien parce qu’un objet peut être temporairement invalide, mais il est valide au moment où on le valide explicitement, ou au moment où le framework le valide avant d’entrer dans mon code
      En fait, je préfère utiliser Objects.requireNonNull(s), qui est plus explicite que le cast du dernier cas. Cela dit, ce serait bien d’avoir aussi quelque chose comme Objects.unsafeForceNonNull(s), qui contournerait la vérification explicite sauf dans les cas bloqués pour des raisons d’optimisation. Avec une méthode unsafe, on pourrait implémenter soi-même requireNonNull sans ajouter d’analyse statique complexe
    • Il est écrit que « l’objectif n’est pas d’exiger que le programme traite explicitement toutes les valeurs null possibles ; les valeurs null non traitées peuvent donner lieu à un avertissement à la compilation, mais pas à une erreur »
      Malheureusement, cela ne sera vérifié qu’au runtime
    • Si le premier ou le deuxième exemple devenait une erreur de compilation, cela voudrait dire qu’il faudrait annoter tous les points d’utilisation de toutes les bibliothèques. Jusqu’à ce que la bibliothèque en question migre vers les types null, ou si elle ne migre jamais, presque chaque ligne serait remplie de casts. Ça n’a pas de sens
      Par exemple, la bibliothèque standard indique explicitement qu’elle ne migrera pas vers les types null, au moins pendant un certain temps
    • Si le cœur du problème est l’introduction de null dans les API, je ne suis pas sûr que ce soit une erreur du langage. Je comprends que ce soit verbeux et qu’il faille nettoyer, mais au final, le problème ne vient-il pas des développeurs qui écrivent le code de cette façon ?
    • Si le deuxième cas n’est pas une erreur de compilation, du code legacy pourra appeler une fonction qui prend un argument non-null, mais je ne sais pas si c’est forcément une bonne chose. Difficile de juger avant de l’avoir vraiment utilisé
  • Il semble absolument nécessaire d’avoir un moyen, au niveau du package ou au moins du fichier, de marquer toutes les variables comme non-null par défaut. Sinon, pour des raisons de sûreté, l’argument en faveur de l’utilisation de la syntaxe T! sur presque toutes les variables deviendra très fort, ce qui ne fera qu’ajouter beaucoup de bruit

    • Dans la section « améliorations possibles à l’avenir », il y a : « fournir un mécanisme par lequel le langage permettrait au programmeur d’affirmer que tous les types dans un certain contexte sont implicitement restreints à non-null, sans avoir à écrire le symbole explicite ! »
    • Ça ne me paraît pas si mauvais. Dans notre projet, nous avons déjà une règle selon laquelle toutes les variables du code Java que nous possédons ne sont pas null. Si une variable nullable est nécessaire, il faut l’annoter avec @Null
      Le problème ne se pose qu’aux frontières du code qui interagit avec des bibliothèques. Cette nouvelle syntaxe serait probablement similaire
    • Un linter agressif pourrait considérer les types non annotés comme non-null et n’exiger une annotation de nullabilité que là où c’est nécessaire
  • La partie qui dit que « pour l’instant, l’objectif n’est pas d’appliquer l’amélioration du langage à la bibliothèque standard » est décevante
    Pour avoir dû utiliser PHP, c’est pénible de devoir retirer ou reconstruire, à chaque interaction avec une énorme bibliothèque standard, des propriétés déjà garanties à l’avance sur les données
    Java devrait intégrer plus activement cette expressivité dans la bibliothèque standard et en faire un citoyen de première classe

    • L’un des plus gros problèmes de Java, c’est qu’à force de s’améliorer progressivement, le code un peu amélioré nouvellement devient à nouveau du code legacy à la fonctionnalité incrémentale suivante. Le code legacy qui utilise Optional devient un poids pour une proposition de nullable/non-nullable explicite. Les types record auraient aussi pu être non-nullable par défaut, à mon avis
    • On dirait que vous lisez « pour l’instant » comme « jamais », alors qu’en réalité, c’est plutôt que cette transition est elle-même un énorme chantier et qu’ils veulent la traiter séparément
      De plus, en séparant en deux étapes, il est plus facile de publier cette fonctionnalité en preview, de recevoir des retours, puis de figer la conception. Si on essaie de tout faire d’un coup, il ne reste presque aucune marge pour itérer réellement sur la fonctionnalité
    • L’expression clé est « pour l’instant ». Je pense que cela deviendra un objectif une fois la fonctionnalité stabilisée
    • « Devoir utiliser PHP », vraiment ? Le PHP moderne est plutôt agréable
  • J’aimerais bien que cette fonctionnalité arrive dans Java. La possibilité explicite, au niveau du langage, avec quelque chose comme T?, a fortement amélioré la qualité de vie des développeurs dans Kotlin et TypeScript. Il existe des outils comme NullAway pour Java, mais c’est fastidieux
    À mon avis, le support au niveau du langage est bien meilleur que Optional/Maybe, parce qu’il permet de se concentrer sur la logique réelle plutôt que de mettre le code sur des rails de map/flatMap
    https://github.com/uber/NullAway

    • Vous voulez casser plus de 30 ans de compatibilité source ? Pourquoi faudrait-il faire ça ?
      Autant utiliser simplement un autre langage JVM, non ?
  • Le site étant actuellement hors ligne, voici un lien d’archive : https://web.archive.org/web/20240802081039/https://bugs.open...

    • Pas surprenant qu’ils soient passés au « jira » que tout le monde adore
  • « Exiger que le programme traite explicitement toutes les valeurs null qui peuvent se produire n’est pas l’objectif ; les valeurs null non traitées peuvent donner lieu à des avertissements à la compilation, mais pas à des erreurs » est une mauvaise décision.
    Java est en grande partie un langage à typage statique ; je ne comprends pas pourquoi on y ajoute encore un comportement dynamique. J’espère qu’il y aura un moyen simple de transformer ces avertissements en erreurs.

    • Ce n’est pas possible. Cela casserait tous les programmes Java existants. Si on l’imposait, il faudrait réécrire tous les programmes pour gérer partout les réponses null. Avec cet ajout, on ne peut imposer la gestion de null que pour les opérations liées aux nouveaux types, ce qui ne casse rien.
      Les langages dont le système de types impose la gestion de null/nil dans tous les cas sont nettement meilleurs. Mais Java n’est pas ce genre de langage aujourd’hui. Cela dit, ce sera tout de même une grande amélioration.
  • C’est dommage qu’on apprenne ces leçons si tard. Par défaut, tout devrait être non-nullable, immuable par défaut, et avec la portée la plus étroite par défaut.
    Dans les nouvelles conceptions, on choisit trop souvent la commodité immédiate plutôt que de « tomber du côté sûr ». Des valeurs par défaut sûres exigent une conception et une expérience utilisateur bien plus soignées, mais le résultat, c’est que presque tous les langages, plateformes et technologies se retrouvent truffés de pièges à se tirer une balle dans le pied. Le génie civil et l’électrotechnique ont des normes ; le logiciel, lui, réapprend les mêmes leçons tous les 30 ans environ avec de nouveaux langages et de nouvelles technologies.

    • Malheureusement, Java ne semble toujours pas avoir appris la règle du non-nullable par défaut. Cette proposition introduit deux nouveaux types, mais si l’on demande si le type de base sans annotation est nullable, la réponse reste « ça dépend ».
    • Ce qui est dommage, c’est que Java apprenne ce genre de chose tardivement. « Nous » et « Java » ne sont pas la même chose.
      Java ressemble à un village du tiers-monde qui découvre qu’il suffit de faire bouillir l’eau pour tuer les germes.
  • La majeure partie de ce que j’ai fait chez Facebook consistait à utiliser Hack. La nullabilité est un élément central du système de types de Hack, et elle élimine vraiment beaucoup d’erreurs inutiles.
    Bien sûr, cela ne veut pas dire qu’on ne reçoit jamais de null inattendu. Comme cette fonctionnalité a aussi été ajoutée au langage plus tard, il restait beaucoup de types mixed hérités des racines PHP, ce qui voulait en pratique dire que n’importe quoi était accepté.
    Je me demande d’abord ce qu’il en est des tableaux nullables. L’exemple String![] montre le cas où l’objet peut être null, mais qu’en est-il du tableau lui-même ? En Java, String labels[] = null; est parfaitement légal. Faudrait-il donc déclarer quelque chose comme String![]! labels; ?
    En Hack, vec $foo signifie que ni foo ni ses éléments ne sont null, tandis que ?vec $foo signifie que les éléments sont non-null, mais que foo peut être null. En pratique, il y a très peu de raisons d’utiliser un tableau null, donc la valeur par défaut devrait être non-null. Le problème, c’est que Java a tout le code legacy qui suppose la possibilité de null.
    Dans les exemples Object! o1 = s, Object o2 = id(s), Object o3 = (String!) s de la proposition, il me semble que les cas 2 et 3 devraient être des erreurs de compilation.
    Enfin, je préfère l’opérateur de contrainte as de Hack au cast de Java. Par exemple, foo($b) est une erreur de compilation, $b as A produit une erreur à l’exécution si c’est null, et $a as ?B caste si c’est un B, sinon renvoie null.
    Au final, la question est de savoir si l’on pourra appliquer cela par-dessus le SDK Java, et à quoi cela ressemblera dans le code legacy.
    https://docs.hhvm.com/hack/types/nullable-types

    • Ce sera probablement simplement String![]? labels = null;.
  • On dirait que les bonnes idées de Kotlin arrivent maintenant toutes dans Java.
    Cela dit, je préfère continuer à travailler avec Kotlin, où je n’ai pas à gérer des choses comme Lombok. Les records Java sont bien, quand même.

    • Les programmeurs Java continueront malgré tout à utiliser les anciennes habitudes, et resteront attachés à d’anciennes bases de code. Il faudra du temps avant que ces nouveautés soient largement utilisées, donc les programmeurs Kotlin n’ont pas à s’inquiéter.
    • Java a une longue histoire d’inspiration puisée dans d’autres langages. C’est agréable de le voir continuer à évoluer à un rythme plutôt correct tout en conservant globalement la compatibilité ascendante.