Java 14: die NullPointerException lernt sprechen!

14.11.2019

Wer kennt sie nicht, die NullPointerException? Manche nennen sie auch den „Billion Dollar Mistake“, weil sie häufig auftritt und ihre Bugfixes im Lauf der Jahrzehnte zusammengerechnet Millarden von Dollar gekostet haben müssen.

Modernere Sprachen wie etwa Kotlin treten diesem Umstand gegenüber, indem sie es standardmäßig gar nicht erst erlauben, dass eine Variable den Wert null annimmt.

Sehr „geschwätzig“ ist die NullPointerException nicht. Wenn sie auftritt, sieht die Fehlerausgabe üblicherweise so aus:

java.lang.NullPointerException
	at MyClass.someMethod(MyClass.java:10)
	at MyClass.main(MyClass.java:5)

Gut, nun weiß man dass die NPE in Zeile 10 von MyClass.java aufgetreten ist. Was aber, wenn dort so etwas steht:

String s = a.b.c.d.toString();

Das Beispiel ist ein Extremfall, der in dieser Form (hoffentlich) nicht oft vorkommt. Aber die Frage, welche der Referenzen in der betroffenen Zeile null ist, stellt sich öfter, und ist nicht leicht zu beantworten.

Doch das soll sich bald ändern. Denn es gibt ein Java Enhancement Proposal namens „JEP-358: Helpful NullPointerExceptions“ 1, das Abhilfe schaffen soll. Das JEP soll nach aktuellem Stand in Java 14 enthalten sein, das im März 2020 erscheint2.

Ich habe den Early Access Build von Java 14 heruntergeladen3 und das neue Feature schon einmal ausprobiert. Dafür habe ich die folgende Testklasse geschrieben:

import java.util.List;
import java.util.ArrayList;

public class NpeTest {
   
   private static void object() {
      try {
         Object o = null;
         o.toString(); 
      } catch(NullPointerException e) {
         e.printStackTrace();
      }
   }
   
   private static void array() {
      try {         
         boolean[] booleanArray = null;
         boolean b = booleanArray[0];
      } catch(NullPointerException e) {
         e.printStackTrace();
      }
   }
   
   private static void elementInArray() {
      try {         
         String[] stringArray = new String[1];
         stringArray[0] = null; // nur damit explizit dasteht was auch so der Fall ist
         stringArray[0].substring(1);
      } catch(NullPointerException e) {
         e.printStackTrace();
      }
   }
   
   private static void elementInList() {
      try { 
         List<Object> list = new ArrayList<>();
         list.add(null);
         list.get(0).hashCode();
      } catch(NullPointerException e) {
         e.printStackTrace();
      }
   }
   
   private static void autoUnboxing() {
      try {
         Integer i = null;
         int x = i + 3; 
      } catch(NullPointerException e) {
         e.printStackTrace();
      }  
   }
   
   private static void nestedObject() {
      try {         
         A a = new A();
         a.b.c.d.print();
      } catch(NullPointerException e) {
         e.printStackTrace();
      }
   }
   
   public static void main(String... args) {
      object();
      array();
      elementInArray();
      elementInList();
      autoUnboxing();
      nestedObject();
   }
}

class A {
   public B b = new B();
}

class B {
   public C c = null;
}

class C {
   public D d = new D();
}

class D {
   public void print() {
      System.out.println("D");
   }
}

Diese habe ich mit Java 14 (EA) kompiliert…

> javac NpeTest.java

…und ausgeführt:

> java NpeTest

Die Ausgabe war:

java.lang.NullPointerException
        at NpeTest.object(NpeTest.java:9)
        at NpeTest.main(NpeTest.java:63)
java.lang.NullPointerException
        at NpeTest.array(NpeTest.java:18)
        at NpeTest.main(NpeTest.java:64)
java.lang.NullPointerException
        at NpeTest.elementInArray(NpeTest.java:28)
        at NpeTest.main(NpeTest.java:65)
java.lang.NullPointerException
        at NpeTest.elementInList(NpeTest.java:38)
        at NpeTest.main(NpeTest.java:66)
java.lang.NullPointerException
        at NpeTest.autoUnboxing(NpeTest.java:47)
        at NpeTest.main(NpeTest.java:67)
java.lang.NullPointerException
        at NpeTest.nestedObject(NpeTest.java:56)
        at NpeTest.main(NpeTest.java:68)

Hm, gegenüber früherer Versionen ist keine Änderung festzustellen. Ein Blick in den JEP-358 bringt Licht ins Dunkel. Die Funktionalität muss mit einem Kommandozeilenparameter aktiviert werden (in späteren Java-Versionen soll sie dann standardmäßig aktiv sein):

> java -XX:+ShowCodeDetailsInExceptionMessages NpeTest

Jetzt sieht die Ausgabe so aus:

java.lang.NullPointerException: Cannot invoke "Object.toString()" because "<local0>" is null
        at NpeTest.object(NpeTest.java:9)
        at NpeTest.main(NpeTest.java:63)
java.lang.NullPointerException: Cannot load from byte/boolean array because "<local0>" is null
        at NpeTest.array(NpeTest.java:18)
        at NpeTest.main(NpeTest.java:64)
java.lang.NullPointerException: Cannot invoke "String.substring(int)" because "<local0>[0]" is null
        at NpeTest.elementInArray(NpeTest.java:28)
        at NpeTest.main(NpeTest.java:65)
java.lang.NullPointerException: Cannot invoke "Object.hashCode()" because the return value of "java.util.List.get(int)" is null
        at NpeTest.elementInList(NpeTest.java:38)
        at NpeTest.main(NpeTest.java:66)
java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "<local0>" is null
        at NpeTest.autoUnboxing(NpeTest.java:47)
        at NpeTest.main(NpeTest.java:67)
java.lang.NullPointerException: Cannot read field "d" because "<local0>.b.c" is null
        at NpeTest.nestedObject(NpeTest.java:56)
        at NpeTest.main(NpeTest.java:68)

Schon besser, oder?

Allerdings immer noch ein bisschen unschön, weil statt der Namen unserer Referenzen im Code <local0> angezeigt wird. Dem schaffen wir Abhilfe, indem wir beim Kompilieren Debug-Informationen hinzufügen lassen (Kommandozeilenparameter -g):

> javac -g NpeTest.java
> java -XX:+ShowCodeDetailsInExceptionMessages NpeTest

Nun haben wir schöne sprechende Fehlermeldungen:

java.lang.NullPointerException: Cannot invoke "Object.toString()" because "o" is null
        at NpeTest.object(NpeTest.java:9)
        at NpeTest.main(NpeTest.java:63)
java.lang.NullPointerException: Cannot load from byte/boolean array because "booleanArray" is null
        at NpeTest.array(NpeTest.java:18)
        at NpeTest.main(NpeTest.java:64)
java.lang.NullPointerException: Cannot invoke "String.substring(int)" because "stringArray[0]" is null
        at NpeTest.elementInArray(NpeTest.java:28)
        at NpeTest.main(NpeTest.java:65)
java.lang.NullPointerException: Cannot invoke "Object.hashCode()" because the return value of "java.util.List.get(int)" is null
        at NpeTest.elementInList(NpeTest.java:38)
        at NpeTest.main(NpeTest.java:66)
java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "i" is null
        at NpeTest.autoUnboxing(NpeTest.java:47)
        at NpeTest.main(NpeTest.java:67)
java.lang.NullPointerException: Cannot read field "d" because "a.b.c" is null
        at NpeTest.nestedObject(NpeTest.java:56)
        at NpeTest.main(NpeTest.java:68)

Quellen:

1 https://openjdk.java.net/jeps/358 ^

2 https://openjdk.java.net/projects/jdk/14/ ^

3 https://jdk.java.net/14/ ^

 

 

Zurück zur Übersicht

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

*Pflichtfelder

*