1 Punkte von GN⁺ 2024-08-04 | 1 Kommentare | Auf WhatsApp teilen
  • Java bereitet ein Preview-Sprachfeature vor, das Typen mit Nullness-Markern versieht: Foo! lehnt null ab, Foo? erlaubt es bewusst
  • Ein nicht markiertes Foo bleibt in seiner Nullness unspecified und damit mit bestehendem Code kompatibel; Konvertierungen zwischen Typen mit unterschiedlicher Nullness können Warnungen oder Laufzeitprüfungen nach sich ziehen
  • Felder und Arrays, die null ablehnen, können den Standardwert null nicht verwenden: Instanzfelder müssen vor dem Aufruf von super(...) definitiv zugewiesen werden, Arrays benötigen einen Initialwert für ihre Komponenten
  • Wird null auf Foo! verengt, entsteht eine NullPointerException; bei Speicherpfaden für Arrays und Felder verhindern ArrayStoreException und FieldStoreException eine Verunreinigung
  • Optimierungen wie Valhallas Flattening von Value Classes sind die Grundlage dafür, dass der Ausschluss von null verlässlich angenommen werden kann; eine Anwendung auf die Standardbibliothek oder eine automatische Neuinterpretation bestehenden Codes ist derzeit nicht Ziel

Preview-Feature zur expliziten Nullness in Java-Typen

  • Java-Typen erhalten Nullness-Marker, um auszudrücken, ob die Wertemenge eines Typs null enthält
  • Foo! ist ein null-restricted Typ, der null aus der Wertemenge ausschließt
  • Foo? ist ein nullable Typ, der null bewusst in die Wertemenge aufnimmt
  • Ein nicht markiertes Foo hat unspecified Nullness; null kann auftreten, aber es ist nicht erkennbar, ob dies beabsichtigt ist
  • Dieses Feature ist ein Preview-Feature, das über die Compiler- und Laufzeit-Flags --enable-preview aktiviert wird

Ziele und Nicht-Ziele

  • Ausdrücken, ob Java-Referenztypen null erwarten, sowie Warnungen und Prüfungen für Konvertierungen zwischen Typen mit unterschiedlicher Nullness bereitstellen
  • Mit bestehendem Java-Code kompatibel sein und schrittweise eingeführt werden können, ohne Source- oder Binärinkompatibilitäten zu erzeugen
  • Variablen eines Typs, der null ablehnt, müssen initialisiert sein, bevor sie erstmals gelesen werden; auch in separat kompilierten Klassen wird die Ablehnung von null zur Laufzeit erzwungen
  • Metadaten und Integritätsgarantien bereitstellen, damit Laufzeitoptimierungen wie das Flattening von Valhallas value class null-eingeschränkten Typen vertrauen können
  • Derzeit nicht im Zielumfang sind:
    • Bestehenden Code automatisch neu zu interpretieren
    • Jedes mögliche null zu einem Compilerfehler zu machen
    • Nullable Formen für primitive Typen wie int hinzuzufügen
    • In der aktuellen Phase die Spracherweiterung auf die Standardbibliothek anzuwenden

Warum das nötig ist

  • Eine Java-Variable vom Typ String kann eine Referenz auf ein String-Objekt oder null enthalten, aber die Sprache bietet keine Möglichkeit auszudrücken, was davon beabsichtigt ist
  • Viele Programme gehen davon aus, dass null nicht vorhanden ist; dies in Javadoc-Spezifikation und Implementierungscode konsistent zu erzwingen, erfordert jedoch zusätzliche Arbeit
  • Wird diese Erwartung verletzt, kann ein null-Wert durch Implementierungscode fließen und erst an einer weit vom ursprünglichen Bug entfernten Stelle eine Exception auslösen
  • Wenn Entwickler als Teil eines Typs ausdrücken, dass null abgelehnt oder erlaubt ist, helfen Compile-Time-Feedback und Laufzeitprüfungen dabei, unerwartete null-Werte früher zu finden
  • In Valhalla können Variablen von Value-Class-Typen zu einer geflatteten Darstellung des Werts optimiert werden; wenn jedoch zusätzliche Bits für die Kodierung von null nötig sind, kann der Speicherverbrauch steigen oder eine Speicheroptimierung unmöglich werden
  • In Amber kann die Nullness von Pattern-Matching-Kandidaten die Exhaustiveness-Beurteilung eines switch beeinflussen, und die Nullness von Typ-Patterns kann beeinflussen, ob null gematcht wird

Syntax der Nullness-Marker und Typstruktur

  • Nullness wird als inhärenter Teil eines Typs behandelt; Foo? und Foo haben unterschiedliche Nullness und sind daher verschiedene Typen
  • Sowohl Array-Typen als auch Array-Komponententypen können Nullness-Marker haben
    • Foo?[]! ist ein Typ, bei dem das Array selbst null-restricted ist und die Komponenten nullable Foo sind
    • Bei mehrdimensionalen Arrays kann nach jedem Klammerpaar ein Marker stehen; konventionell wird von links nach rechts, von außen nach innen interpretiert
  • Parametrisierte Typen und Typ-Argumente können ebenfalls Nullness-Marker haben
    • Predicate!<Foo?> ist ein null-restricted Predicate, dessen Typ-Argument ein nullable Foo ist
  • Um einen null-restricted oder nullable Typ auszudrücken, muss ! oder ? im Quelltext explizit erscheinen
  • Künftig könnte geprüft werden, alle Typen einer Klasse oder Kompilierungseinheit standardmäßig als null-restricted zu interpretieren und nur ? als Ausnahme zu verwenden; die Details sind jedoch separate Arbeit

Initialisierungsregeln für Felder und Arrays

  • In bestehendem Java ist der Standardwert von Referenztyp-Feldern und Array-Komponenten null; für null-restricted Felder oder Array-Komponenten ist das jedoch kein geeigneter Initialwert
  • Null-restricted Felder und Arrays müssen vom Programm immer initialisiert werden, bevor sie gelesen werden
  • Hat ein null-restricted Instanzfeld keinen Initializer, muss es vor dem expliziten oder impliziten Aufruf von super(...) in jedem Konstruktor definitiv zugewiesen sein
    • Flexible Constructor Bodies JEP erlaubt es, den notwendigen Initialisierungscode am Anfang eines Konstruktors zu schreiben
    • In diesem early construction context sind Aktionen nicht erlaubt, die this referenzieren oder riskieren, nicht initialisierte Felder zu lesen
  • Hat ein null-restricted Instanzfeld einen Initializer, wird dieser am Anfang jedes Konstruktors vor dem Aufruf von super(...) ausgeführt
    • Ein Konstruktor, der this(...) aufruft, ist wie nach den bestehenden Regeln ein Sonderfall und führt Initializer nicht aus
  • Null-restricted statische Felder müssen bis zum Ende aller statischen Initializer und Initialisierungsblöcke der Klasse definitiv zugewiesen sein
    • Versucht eine andere Klasse, dieses Feld während der Klasseninitialisierung zu lesen, erkennt eine Laufzeitprüfung das zu frühe Lesen und wirft eine Exception
  • Arrays mit einem null-restricted Komponententyp müssen im Array-Erzeugungsausdruck für jede Komponente einen Initialwert bereitstellen
    • Mit einem Array-Initializer können alle Werte aufgelistet werden
    • Auch eine neue Kurzsyntax ist möglich, deren Syntax aber noch TBD ist

Nullness von Ausdrücken und Nullness-Konvertierungen

  • Der Java-Compiler bestimmt während der Typprüfung die Nullness jedes Ausdrucks
  • Die Nullness einer Variablenreferenz stammt aus der Variablendeklaration, die Nullness eines Methodenaufrufs aus dem Rückgabetyp der referenzierten Methode
  • Das Literal null ist nullable
  • Die meisten anderen Ausdrücke von Referenztypen sind null-restricted
    • Dazu gehören Literale, String-Verkettung, this, Erzeugung von Klasseninstanzen, Array-Erzeugung, Methodenreferenzen und Lambda-Ausdrücke
  • Nullness-Konvertierungen sind in Zuweisungs-, Aufruf- und Cast-Kontexten erlaubt
  • Widening-Nullness-Konvertierungen umfassen:
    • Foo!Foo?
    • Foo! → unspecified Foo
    • Foo? → unspecified Foo
    • unspecified FooFoo?
  • Narrowing-Nullness-Konvertierungen umfassen:
    • Foo?Foo!
    • unspecified FooFoo!
  • Narrowing-Nullness-Konvertierungen werden vom Compiler ähnlich wie Unboxing-Konvertierungen automatisch durchgeführt, führen zur Laufzeit aber eine dynamische Prüfung aus, die eine NullPointerException auslösen kann
  • Der Versuch, das Literal null direkt in einen null-restricted Typ zu konvertieren, ist ein Compile-Time-Fehler

Laufzeitprüfungen und Exceptions

  • Wenn zur Laufzeit ein null-Wert per Narrowing-Nullness-Konvertierung in einen null-restricted Typ konvertiert wird, entsteht eine NullPointerException
  • Narrowing-Nullness-Konvertierungen, die im Quellcode nicht explizit sichtbar sind, können ebenfalls während der Laufzeitausführung auftreten
  • Ein Array, das mit einem null-restricted Komponententyp angelegt wurde, lehnt null-Werte bei der üblichen Array-Store-Prüfung ab, selbst wenn es im Quellcode als weniger spezifischer Typ behandelt wird
    • Dieser Konvertierungsfehler verursacht eine ArrayStoreException
  • Wenn ein Feld, das zum Kompilierzeitpunkt nicht null-restricted war, später durch separate Kompilierung null-restricted wird, lehnt ein neuer Field-Store-Check das Speichern von null ab
    • Dieser Konvertierungsfehler verursacht eine FieldStoreException
  • Bei Methodenaufrufen in Override-Beziehungen kann auf die Konvertierung in den Aufruftyp des Parameters der Obermethode eine Konvertierung in den Parametertyp der überschreibenden Methode folgen
  • Auch Rückgabewerte von Methoden können zunächst in den deklarierten Rückgabetyp der Methode und danach in den am Aufrufpunkt erwarteten Rückgabetyp konvertiert werden

Generics, Typ-Argumente und Overriding

  • Auch Verwendungen von Typvariablen können Nullness-Marker tragen; T! ist ein null-restricted Typ und T? ein nullable Typ
  • Null-restricted und nullable Typvariablen-Typen behaupten innerhalb generischen Codes eine bestimmte Nullness
  • Auch als Typ-Argumente verwendete Typen können Nullness ausdrücken; Nullness-Marker an Typvariablen-Typen überschreiben die im Typ-Argument behauptete Nullness
  • Innerhalb der erased Implementierung einer generischen API können Null-Einschränkungen nicht erzwungen werden
    • Allerdings erzwingen die üblichen impliziten Casts an Grenzen generischer APIs zur Laufzeit null-restricted Typ-Argumente
  • Für Interoperabilität wird Nullness innerhalb von Typ-Argumenten nicht strikt erzwungen
    • Predicate<String!> kann in Predicate<String> oder Predicate<String?> konvertiert werden
    • Eine solche unchecked nullness conversion kann Warnungen auslösen
  • Auch Änderungen der Nullness von Array-Komponententypen sind als unchecked nullness conversion erlaubt; unter welchen Bedingungen Laufzeitprüfungen erfolgen, ist TBD
  • Bei der Beurteilung der Gleichheit von Methodensignaturen wird Nullness ignoriert
    • Eine Methode kann eine andere überschreiben, auch wenn die Nullness von Parametern und Rückgabetypen nicht übereinstimmt
    • Wenn verschiedene APIs unabhängig voneinander Nullness-Marker einführen, können solche Abweichungen häufig auftreten
  • Nullness beeinflusst nicht die Anwendbarkeit von Methoden und kann keine fehlgeschlagene Typ-Argument-Inferenz verursachen, kann aber die für den Rückgabetyp generischer Methoden inferierte Nullness beeinflussen
    • Details des Inferenzalgorithmus sind TBD

Compilerwarnungen und Fehler

  • Einen Typ null-restricted zu machen, kann neue Compile-Time-Fehler erzeugen
    • Wenn Felder oder Arrays dieses Typs nicht initialisiert sind
    • Wenn versucht wird, das Literal null in diesen Typ zu konvertieren
    • Auch der Vergleich des Literals null mit einem Ausdruck eines null-restricted Typs kann ein Compile-Time-Fehler werden
  • In anderen Situationen ist die Nullness-Analyse unterstützend und erzeugt keine Compile-Time-Fehler
  • javac liefert Warnungen, um Laufzeitfehler zu vermeiden; IDEs und andere Analysewerkzeuge werden in dieselbe Richtung empfohlen
  • Mögliche Warnungsursachen sind:
    • Narrowing-Nullness-Konvertierungen, einschließlich solcher aus ?-Typen
    • Die Verwendung von Ausdrücken eines ?-Typs für Member-Zugriffe oder andere null-feindliche Operationen
    • Wenn die Nullness von Typ-Argumenten nicht konsistent mit Bounds ist
    • Wenn die Nullness von Methodenparametern oder Rückgaben nicht zur überschriebenen Methode passt
    • Unchecked Conversions, die die Nullness eines Typs ändern

Class-Dateien, Reflection und begleitende Änderungen

  • Die meisten Verwendungen von Null-Markern werden in class-Dateien gelöscht; begleitende Laufzeitkonvertierungen werden direkt im Bytecode ausgedrückt
  • Die Syntax des Signature-Attributs wird aktualisiert, um ! und ? innerhalb von Typen zu erlauben
  • Nullness wird nicht in Deskriptoren von Methoden und Feldern kodiert
  • Um Feldverunreinigung zu verhindern, markiert ein neues NullRestricted-Attribut, dass ein Feld keine null-Werte erlaubt
    • Dieses Feld muss auch mit ACC_STRICT markiert sein und streng initialisiert werden
    • Der Verifier prüft, ob alle streng initialisierten Instanzfelder zugewiesen sind, wenn ein Konstruktor super(...) aufruft
    • Jeder Schreibversuch auf das Feld prüft auf null-Werte und wirft bei Fund eine FieldStoreException
  • Die Erzeugung null-restricted Arrays wird nicht durch die Anweisung anewarray unterstützt, sondern muss über einen Aufruf der Reflection-API erfolgen
  • Es gibt keine Literale Foo!.class oder Foo?.class und auch keine entsprechenden java.lang.Class-Instanzen
  • Eine neue RuntimeType API beschreibt die Typmengen, die zur Laufzeit bei Speicherprüfungen für Arrays und Felder erzwungen werden, einschließlich null-restricted Varianten aller Klassen- und Interface-Typen
  • Die Field-API unterstützt das Abfragen des RuntimeType eines Felds; dieser Wert kann vom Ergebnis von getType abweichen
  • Die Array-API unterstützt Varianten von newInstance, die den Komponententyp als RuntimeType ausdrücken
    • Diese Variante erlaubt auch die Bereitstellung von Initialwerten für Array-Komponenten
    • Sie lehnt Versuche ab, ein null-restricted Array ohne Initialwerte zu erzeugen
  • Traditionelle Deserialisierung ist nicht mit null-restricted Feldern und Arrays kompatibel; ein separates JEP soll einen Serialisierungsmechanismus bereitstellen, der nicht initialisierte null-restricted Felder und Arrays nicht offenlegt
  • Von javadoc erzeugte Dokumentation enthält Nullness-Marker
  • Die APIs java.lang.reflect.Type und javax.lang.model kodieren Nullness in der Typdarstellung

Alternativen und Abhängigkeiten

  • Mehrere Entwicklerwerkzeuge im Java-Ökosystem haben eigenes Compile-Time-Tracking von null implementiert; da sie die Java-Sprache aber nicht ändern, ist die Syntax hauptsächlich auf Annotationen beschränkt und die beeinflussbaren Verhaltensweisen sind auf Compile-Time-Prüfungen begrenzt
  • Andere Sprachen verfolgen Nullness im Typsystem; viele Sprachen sind standardmäßig null-restricted und betrachten Zuweisungen an null-restricted Typen ohne explizite Nullprüfung als Fehler
  • In Java muss das Feature optional sein und ohne eine einzige groß angelegte Migration schrittweise nutzbar sein
  • Laufzeiterzwingung von Nullness kann auch durch explizite Prüfungen oder Aufrufe von Objects.requireNonNull implementiert werden
    • Eine konsistente Anwendung ist jedoch umständlich, erfordert zusätzliche Dokumentation und verschlechtert die Lesbarkeit des Programms
    • Es gibt keine Möglichkeit, sie direkt auf Variablenspeicher wie Felder und Arrays anzuwenden
  • Voraussetzung ist Flexible Constructor Bodies (Second Preview)
    • Konstruktoren können dadurch vor dem Aufruf von super(...) Anweisungen ausführen und Instanzfeldern Werte zuweisen, was die Initialisierungsanforderung für null-restricted Felder ermöglicht
  • Künftige Arbeiten umfassen:
  • Weitere mögliche künftige Verbesserungen umfassen die Anwendung von Nullness-Markern auf Teile der Standard-API, eine kompakte Darstellung von Nullprüfungen im Bytecode, stärkere Low-Level-Erzwingung null-restricted Methodenparameter sowie Sprachmechanismen, die alle Typen in bestimmten Kontexten implizit als null-restricted behaupten

1 Kommentare

 
GN⁺ 2024-08-04
Meinungen auf Hacker News
  • Interessant ist, wie sich dieser Ansatz von dem unterscheidet, den C# vor einigen Jahren eingeführt hat. Wenn man in C# Null-Zulässigkeit in einem Projekt aktiviert, werden alle Variablen als non-null deklariert, sofern sie nicht explizit als nullable markiert sind; in diesem Vorschlag werden bestehende Variablen dagegen effektiv zu einer von drei Varianten: nullable, explizit nullable oder explizit non-nullable.
    Kotlin ist ebenfalls eine JVM-Sprache, behandelt Typen aber wie C# als non-null, wenn nichts explizit angegeben ist; allerdings hat Kotlin keine Last der Abwärtskompatibilität. Mit lateinit var bietet es zudem einen Ausweg, bei dem ein non-nullable Typ leer bleiben kann, bis er in einer anderen Methode initialisiert wird, und bei Zugriff vor der Initialisierung eine spezielle Exception auslöst.
    Ich frage mich, warum man drei Wahlmöglichkeiten vorsieht. Könnte man nicht unannotierte Variablen nullable lassen und nur annotierte Variablen explizit non-null machen? Wenn etwas ohne Markierung ohnehin automatisch nullable ist, fällt mir kein Grund ein, warum man nullable überhaupt ausdrücklich deklarieren wollen sollte.
    Mir gefällt der C#-Ansatz besser, aber dieser Ansatz hat den Vorteil, dass man ihn in Legacy-Codebasen nutzen kann, ohne alle Probleme rund um Null-Zulässigkeit lösen zu müssen. Umgekehrt macht C# Probleme mit Null-Zulässigkeit sofort sichtbar, während dieser Vorschlag erlaubt, sie wie bisher zu verstecken.
    Auch die Stelle, dass „eine Methode eine andere Methode überschreiben kann, selbst wenn die Null-Eigenschaft von Parametern und Rückgabewerten nicht übereinstimmt“, wirkt seltsam. Beim Überschreiben/Implementieren von Callbacks scheint das eine Fußpistole zu sein: Die ursprüngliche Methode gibt non-null als Rückgabe vor, aber die überschreibende Methode liefert null zurück.

    • In meiner aktuellen Firma migrieren wir gerade eine Legacy-C#-Codebasis auf nullable reference types, und eine explizite non-nullable-Markierung wäre wirklich hilfreich, weil man sofort sehen könnte, welche Stellen bereits geprüft und annotiert wurden.
      Aus Zeitgründen haben wir uns im Moment dafür entschieden, abgesehen von einigen Kernstellen nur die Annotationen einzuschalten, behalten aber JetBrains’ NotNullAttribute und CanBeNullAttribute weiterhin als Marker bei, um bewusst getroffene Entscheidungen sofort zu erkennen. Letzteres ließe sich entfernen, weil nullable eine explizite Markierung hat; Ersteres kollidiert aber namentlich mit der C#-eigenen Funktionalität.
      In diesem Sinne sind drei Wahlmöglichkeiten ziemlich wünschenswert. Denn bei Hunderttausenden Codezeilen lässt sich eine Migration nicht schnell und einfach durchführen.
      In anderen internen Projekten streuen wir #nullable enable in die Umgebung des Codes, an dem wir gerade arbeiten, und erweitern so schrittweise den Bereich der Null-Zulässigkeit. Außerdem gilt die Bedingung, dass neuer Code in einem nullable-Kontext stehen muss. Auch das ist in Ordnung, um bereits annotierte Teile explizit zu machen, ist aber ein Ansatz, der bei einer deutlich kleineren Codebasis und einem kleineren Team möglich ist.
    • Auch in Kotlin gibt es Typen, deren Null-Zulässigkeit nicht festgelegt ist, auch wenn man sie nicht direkt selbst definieren kann. Kotlin nennt sie Plattformtypen (platform types) und zeigt sie in Fehlern und Diagnosen mit einem Ausrufezeichen an, etwa String!.
      Der Begriff „Plattformtyp“ und das Ausrufezeichen sind verwirrend, aber ansonsten funktioniert der Kotlin-Ansatz ziemlich gut. Da Kotlin von Anfang an Null-Zulässigkeit hatte, können Programmierer Plattformtypen nicht direkt angeben; sie werden aber weiterhin gebraucht, um mit zugrunde liegenden Plattformen wie JVM oder JavaScript kompatibel zu bleiben, bei denen die Null-Zulässigkeit unklar ist.
      Bei diesem Ansatz ist der Standardwert weiterhin vernünftig. Standardmäßig sollte immer non-nullable gelten, und wer anders denkt, hat von Tony Hoare nichts gelernt. Gleichzeitig bleibt Abwärtskompatibilität erhalten. Kotlin hatte es vergleichsweise leicht; Java und C# müssen auch die Kompatibilität mit bestehendem Quellcode wahren.
      Weder der Java- noch der C#-Ansatz ist ideal. Der C#-Ansatz verändert das Verhalten von Code stark abhängig von einem Compiler-Flag, und der Java-Ansatz macht die schlechteste Option zum Standard.
      Trotzdem tendiere ich eher zu C#. Wenn man „wahrscheinlich nullbar“ zur einfachsten Option macht, werden die meisten Programmierer sie standardmäßig wählen. Besonders bei einer unternehmensfreundlichen Sprache wie Java scheint das plausibel. Linter und Compiler-Warnungen werden langfristig helfen, aber es dürfte viele Jahre dauern, bis der Großteil des Java-Codes die Null-Zulässigkeit korrekt annotiert. C#-Nutzer werden kurzfristig mehr Schmerzen haben, erreichen das Ziel einer klaren Null-Zulässigkeit aber wahrscheinlich deutlich schneller.
      https://kotlinlang.org/docs/java-interop.html#null-safety-an...
      https://www.infoq.com/presentations/Null-References-The-Bill...
    • Ich stimme zu, dass das seltsam ist. Erwartet hätte ich, dass auf Quellcode-Ebene T? nullable und T! non-nullable bedeutet und dass es für normales T in neu kompiliertem Quellcode eine pragma-ähnliche Deklaration gibt, mit der der Standardwert pro Quelldatei, pro package-info-Datei oder global per Compiler-Switch festgelegt wird.
      In einer späteren LTS-Java-Version könnte man dann den globalen Standardwert ändern. Das würde die Umstellung von Projekten erleichtern, Pragmas könnten automatisch eingefügt/aktualisiert werden, und bei Bedarf ließe sich auch der alte Standardwert beibehalten.
      Wenn man außerdem die bestehenden JSR-305-@Nonnull-Annotationen genutzt hätte, um Vorkommen von non-nullable Typen in Klassendateien darzustellen, hätte man auch bidirektionale Kompatibilität mit älteren JDKs bieten können.
    • Am Ende gibt es einen Abschnitt darüber, „einen Mechanismus bereitzustellen, mit dem die Sprache zusichert, dass alle Typen in einem bestimmten Kontext implizit null-beschränkt sind, ohne dass Programmierer ein explizites !-Symbol schreiben müssen“.
      Schon ein Compiler-Flag oder ein Modul-Tag würde dafür wohl ausreichen.
    • Der Grund dürfte die Code-Abwärtskompatibilität sein, die Java viel stärker gewichtet als andere Sprachen.
      Trotzdem gehe ich davon aus, dass es ein Compiler-Flag geben wird, das ohne gesonderte Angabe automatisch non-nullable annimmt.
  • Sieht gut aus. Endlich gibt es auf Sprachebene eine Möglichkeit, Tausende unnötiger Exceptions und Null-Prüfungen loszuwerden. Allerdings fühlt sich die automatische Konvertierung, die die Nullbarkeit eingrenzt, falsch an.
    Bei den Beispielen im Vorschlag String? id(String! arg) { return arg; }, String s = null;, Object! o1 = s; // NPE, Object o2 = id(s); // NPE, Object o3 = (String!) s; // NPE sollten meiner Meinung nach zumindest die ersten beiden Fälle Compile-Fehler sein.
    Der letzte Fall ist explizit und daher etwas uneindeutig, aber besser wäre es, wenn der Compiler innerhalb von if (s != null) den effektiven Typ als String! erkennen und String! ss = s; erlauben würde. Dann gäbe es keine Fehlermöglichkeit.

    • Aus der Perspektive von jemandem, der in der Branche Java genutzt hat, wäre es besser, wenn dynamische Prüfungen nur dann stattfinden, wenn sie explizit angefordert werden, und alles Übrige statisch behandelt wird. Bean Validation funktioniert gut, weil ein Objekt zwar vorübergehend ungültig sein kann, aber in dem Moment gültig ist, in dem man explizit validiert, oder in dem das Framework validiert, bevor es in meinen Code gelangt.
      Eigentlich bevorzuge ich im letzten Fall statt des Casts Objects.requireNonNull(s), weil das expliziter ist. Schön wäre allerdings auch etwas wie Objects.unsafeForceNonNull(s), das explizite Prüfungen umgeht und nur die Fälle ausnimmt, die wegen Optimierung blockiert werden. Mit einer unsafe-Methode könnte man requireNonNull direkt implementieren, ohne komplexe statische Analyse hinzuzufügen.
    • Es heißt: „Es ist kein Ziel, dass Programme jeden möglichen Null-Wert explizit behandeln müssen; unbehandelte Null-Werte können zur Compile-Zeit Warnungen sein, aber keine Fehler.“
      Leider wird das nur zur Laufzeit überprüfbar sein.
    • Wenn das erste oder zweite Beispiel ein Compile-Fehler wäre, hieße das, dass man jede Verwendungsstelle jeder Bibliothek annotieren müsste. Bis diese Bibliothek auf Null-Typen migriert, oder falls sie das nie tut, wäre fast jede Zeile voller Casts. Das ergibt keinen Sinn.
      Zum Beispiel sagt die Standardbibliothek ausdrücklich, dass sie zumindest vorerst nicht auf Null-Typen migrieren wird.
    • Wenn der Kern des Problems darin besteht, Null in eine API einzuführen, bin ich mir nicht sicher, ob das ein Sprachfehler ist. Ich verstehe, dass man dafür viel Boilerplate braucht und aufräumen will, aber am Ende liegt das Problem doch bei den Entwicklern, die Code so schreiben.
    • Wenn das zweite kein Compile-Fehler ist, kann Legacy-Code Funktionen aufrufen, die Non-null-Argumente entgegennehmen; ob das wirklich gut ist, weiß ich aber nicht. Vor der praktischen Nutzung ist das schwer zu beurteilen.
  • Es scheint unbedingt nötig, auf Paketebene oder zumindest Dateiebene alle Variablen standardmäßig als non-null markieren zu können. Andernfalls wird aus Sicherheitsgründen stark dafür argumentiert werden, fast überall die T!-Syntax zu verwenden, und das erzeugt nur viel Rauschen.

    • Unter „Mögliche zukünftige Verbesserungen“ steht: „Bereitstellung eines Mechanismus, mit dem die Sprache zusichern kann, dass alle Typen in einem bestimmten Kontext implizit null-beschränkt sind, ohne dass Programmierer das explizite !-Symbol schreiben müssen.“
    • So schlecht klingt das nicht. In unserem Projekt haben wir bereits den Standard, dass alle Variablen in dem Java-Code, den wir besitzen, nicht null sind. Wenn eine nullable Variable nötig ist, muss sie mit @Null annotiert werden.
      Probleme entstehen nur an den Codegrenzen bei der Interaktion mit Bibliotheken. Diese neue Syntax dürfte ähnlich sein.
    • Ein aggressiver Linter könnte nicht markierte Typen als non-null betrachten und Nullbarkeitshinweise nur dort verlangen, wo sie nötig sind.
  • Der Teil „Derzeit ist es nicht Ziel, die Sprachverbesserung auf die Standardbibliothek anzuwenden“ ist bedauerlich.
    Aus Erfahrung mit PHP, das ich notgedrungen nutzen musste: Es ist lästig, Eigenschaften, die man für Daten vorab garantiert hat, bei jeder Interaktion mit einer riesigen Standardbibliothek wieder abzustreifen oder neu aufzubauen.
    Java sollte solche Ausdruckskraft ebenfalls aktiver in die Standardbibliothek einbringen und sie zu einem First-Class Citizen machen.

    • Eines der größten Probleme von Java ist, dass durch schrittweise Verbesserungen gerade neu etwas verbesserter Code bei der nächsten inkrementellen Funktion wieder zu Legacy-Code wird. Legacy-Code, der Optional verwendet, wird bei einem Vorschlag für explizit nullable/non-nullable zur Last. Record-Typen hätten vielleicht standardmäßig non-nullable sein können.
    • Du scheinst „derzeit“ als „niemals“ zu lesen, aber tatsächlich geht es eher darum, diese Umstellung separat zu behandeln, weil sie selbst eine riesige Aufgabe ist.
      Außerdem lässt sich dieses Feature in zwei Schritten leichter als Preview-Feature veröffentlichen, Feedback einsammeln und das Design danach endgültig festlegen. Wenn man alles auf einmal machen will, bleibt fast kein Raum mehr, das Feature in der Praxis iterativ zu verbessern.
    • Das Schlüsselwort ist „derzeit“. Wenn sich das Feature etabliert hat, wird es meiner Meinung nach zum Ziel werden.
    • Von „notgedrungen“ kann keine Rede sein; modernes PHP ist ziemlich angenehm.
  • Ich fände es gut, wenn dieses Feature in Java käme. Eine explizite Wahlmöglichkeit auf Sprachebene wie T? hat in Kotlin und TypeScript die Lebensqualität von Entwicklern deutlich erhöht. Für Java gibt es Tools wie NullAway, aber sie sind umständlich.
    Unterstützung auf Sprachebene halte ich für viel besser als Optional/Maybe, weil man sich auf die eigentliche Logik konzentrieren kann, statt den Code auf map-/flatMap-Schienen zu setzen.
    https://github.com/uber/NullAway

    • Du willst also mehr als 30 Jahre Source-Kompatibilität brechen? Warum sollte man das tun?
      Dann könnte man doch einfach eine andere JVM-Sprache verwenden.
  • Die Website ist derzeit offline, daher hier ein Archivlink: https://web.archive.org/web/20240802081039/https://bugs.open...

    • Kein Wunder, dass sie auf das von allen geliebte „jira“ umgestellt haben.
  • „Es ist nicht das Ziel, vom Programm zu verlangen, dass es alle möglichen null-Werte explizit behandelt; unbehandelte null-Werte können zur Compile-Zeit eine Warnung sein, aber kein Fehler“ ist eine schlechte Entscheidung.
    Java ist größtenteils eine statisch typisierte Sprache – warum also noch ein weiteres dynamisches Verhalten einbauen? Ich hoffe, es gibt eine einfache Möglichkeit, solche Warnungen zu Fehlern hochzustufen.

    • Das geht nicht. Damit würde jedes bestehende Java-Programm kaputtgehen. Würde man es erzwingen, müssten alle Programme so umgeschrieben werden, dass sie überall null-Antworten behandeln. Mit dieser Erweiterung kann erzwungene null-Behandlung nur auf Operationen angewendet werden, die mit den neuen Typen zusammenhängen, und deshalb geht nichts kaputt.
      Eine Sprache, deren Typsystem die Behandlung von null/nil in allen Fällen erzwingt, ist natürlich viel besser. Aber Java ist derzeit keine solche Sprache. Trotzdem wird das eine große Verbesserung sein.
  • Schade, dass diese Lektionen so spät gelernt werden. Standardmäßig non-nullable, standardmäßig unveränderlich, standardmäßig der engste Scope.
    Bei neuen Designs entscheidet man sich viel zu oft für unmittelbare Bequemlichkeit, statt „auf den sicheren Pfad zu fallen“. Sichere Defaults erfordern viel sorgfältigeres Design und eine bessere User Experience, aber das Ergebnis ist, dass fast jede Sprache, Plattform und Technologie voller Fußangeln ist. Im Bau- und Elektroingenieurwesen gibt es Vorschriften; in der Software lernen wir ungefähr alle 30 Jahre bei neuen Sprachen und Technologien dieselben Lektionen erneut.

    • Leider scheint Java die Regel standardmäßig non-nullable immer noch nicht gelernt zu haben. Dieser Vorschlag führt zwei neue Typen ein, aber wenn man fragt, ob der unveränderte, nicht annotierte Standardtyp nullable ist, lautet die Antwort weiterhin „tja“.
    • Schade ist, dass Java so etwas spät lernt. „Wir“ und „Java“ sind nicht dasselbe.
      Java ist wie ein Dorf in der Dritten Welt, das lernt, dass man Wasser abkochen muss, um Keime abzutöten.
  • Ein Großteil meiner Arbeit bei Facebook bestand darin, Hack zu verwenden. Nullability ist ein Kernelement des Hack-Typsystems und beseitigt wirklich viele unnötige Fehler.
    Natürlich heißt das nicht, dass man niemals unerwartetes null bekommt. Da diese Funktion erst später zur Sprache hinzugefügt wurde, gab es immer noch viele Legacy-mixed-Typen, die die PHP-Wurzeln widerspiegeln und faktisch bedeuten, dass alles möglich ist.
    Zunächst frage ich mich, was mit nullable Arrays passiert. Das Beispiel String![] zeigt den Fall, dass das Objekt null sein kann, aber was ist mit dem Array selbst? In Java ist String labels[] = null; völlig legal. Muss man dann so etwas wie String![]! labels; deklarieren?
    In Hack bedeutet vec $foo, dass weder foo noch seine Elemente null sind, während ?vec $foo bedeutet, dass die Elemente non-null sind, foo selbst aber null sein kann. In der Praxis gibt es kaum einen Grund, null-Arrays zu verwenden, also sollte der Default nicht-null sein. Das Problem ist allerdings, dass Java bei allem Legacy-Code von möglicher null-Zulässigkeit ausgeht.
    Bei den Beispielen Object! o1 = s, Object o2 = id(s), Object o3 = (String!) s im Vorschlag habe ich das Gefühl, dass Nummer 2 und 3 Compile-Fehler sein sollten.
    Schließlich gefällt mir Hacks as-Zwangsoperator besser als Javas Casting. Zum Beispiel wäre foo($b) ein Compile-Fehler, $b as A ein Laufzeitfehler, wenn es null ist, und $a as ?B würde casten, wenn es ein B ist, andernfalls null zurückgeben.
    Am Ende stellt sich die Frage, ob man das über das Java SDK legen kann und wie es in Legacy-Code aussehen wird.
    https://docs.hhvm.com/hack/types/nullable-types

    • Vermutlich einfach String![]? labels = null;.
  • Die guten Dinge aus Kotlin scheinen nun alle in Java zu landen.
    Trotzdem würde ich lieber weiter mit Kotlin arbeiten, wo ich mich nicht mit Dingen wie Lombok herumschlagen muss. Java Records sind allerdings gut.

    • Trotzdem werden Java-Programmierer weiter die alten Idiome verwenden und an alte Codebasen gebunden sein. Es wird lange dauern, bis sich solche Dinge breit durchsetzen, also müssen sich Kotlin-Programmierer keine Sorgen machen.
    • Java hat eine lange Geschichte darin, sich von anderen Sprachen inspirieren zu lassen. Es ist schön zu sehen, dass es sich weiter in recht ordentlichem Tempo entwickelt und dabei die Rückwärtskompatibilität weitgehend bewahrt.