Optionals: Mal hat man’s und mal nicht (1 / 3)

In vielen Programmiersprachen gibt es Konstrukte, die „keinen Wert“ repräsentieren. In Java ist dies klassischerweise der Wert null. Immer, wenn man einer Variable noch keinen Wert zugewiesen hat oder explizit ausdrücken möchte, dass sie aktuell keinen validen Wert hält, erhält sie den Wert null.

Code, der auf eine solche Variable zugreift, muss explizit prüfen, ob die Variable gerade einen richtigen Wert enthält oder diesen speziellen null Wert. In letzterem Fall darf dann der Inhalt des Werts nicht verwendet werden, denn es gibt ja keinen. Falls man das doch probiert, fliegt die allseits beliebte NullPointerException.

Mit Java 8 wurden Optionals eingeführt. Diese machen die Arbeit mit dem Konzept „Kein Wert“ viel einfacher und übersichtlicher und helfen stark bei der Erstellung übersichtlichen Codes. In diesem Artikel schauen wir uns an was Optionals zu bieten haben und wie man sie richtig verwendet.

Schauen wir uns zunächst die klassische Arbeit mit null Werten an. Wenn wir dann die dabei auftretenden Probleme verstehen, untersuchen wir, was Optionals zur Lösung dieser Probleme beitragen.

class Test {
  public Integer a;

  public void ausgeben() {
    System.out.println(a.toString());
  }
}

In diesem einfachen Beispiel wird eine Klasse mit einer Instanzvariable definiert. Die Variable ist nicht initialisiert, wenn ein Objekt mit Test test = new Test() erzeugt wird. Der Aufruf der Methode test.ausgeben() wird nun zu einer NullPointerException führen, weil der darin enthaltene Aufruf a.toString() nicht ausführbar ist. a ist ja null.

Test test = new Test();
test.ausgeben();

Erst wenn wir a einen validen Wert zuweisen kann die ausgeben() Methode aufgerufen werden:

Test test = new Test();
test.a = 5;
test.ausgeben();

Es ist unbefriedigend, dass der Verwender der Klasse Test genau wissen muss in welchem Zustand sich das Objekt test befindet um entscheiden zu können, ob eine Methode auf dem Objekt aufgerufen werden kann. Wir sollten die Methode ausgeben() so modifizieren, dass wir sie immer aufrufen können, unabhängig davon, welche Werte die Instanzvariablen haben.

class Test {
  public Integer a;

  public void ausgeben() {
    if(a == null) {
      System.out.println("a hat keinen Wert");
    } else {
      System.out.println(a.toString());
    }
  }
}

Bei dieser Version der Klasse prüft die Methode ausgeben() nun, ob a den Wert null oder einen richtigen Wert hat. Falls der Wert null ist, wird eine statische Meldung ausgeben. Und nur, wenn a einen von null verschiedenen Wert hat, wird die toString() Methode für a aufgerufen. Im Ergebnis können wir die Methode ausgeben() nun immer aufrufen; unabhängig davon, ob wir der Instanzvariable einen Wert zugewiesen haben oder nicht.

Minimal eleganter, aber im Ergebnis identisch, kann es mit dem ternären „?:“ Operator formuliert werden:

class Test {
  public Integer a;

  public void ausgeben() {
    System.out.println(a == null ? "a hat keinen Wert" : a.toString());
  }
}

Wir können mit null Werten und null-Checks also Situationen behandeln, in denen ein Wert da sein könnte, aber nicht vorhanden ist. Der null Wert zeigt an, dass der Wert „fehlt“. Der null-Check bei der Verwendung des Werts verhindert unliebsame Überraschungen in Form einer NullPointerException.

Wozu benötigt man also ein weiteres Konzept, wenn man doch alle Probleme so behandeln kann? Das ist tatsächlich gar nicht so offensichtlich. Schließlich arbeiten wir Entwickler mittlerweile seit Jahrzehnten mit null und null-Checks. Aber schauen wir uns doch mal an, was passiert, wenn wir weitere Klassen, mehr Instanzvariable und mehr Methoden haben:

class TestB {
  public Integer wert;

  public void ausgeben() {
    System.out.println("wert: " + wert.toString());
  }
};

class TestA {
  public Integer a;
  public TestB b;
  public Integer c;

  public Integer summe() {
    return a + b.wert + c;
  }

  public void ausgeben() {
    System.out.println("a: " + a.toString());
    b.ausgeben();
    System.out.println("c: " + c.toString());
  }
};

Wenn alle Instanzvariablen initialisiert sind, gibt es kein Problem:

TestB b = new TestB();
b.wert = 2;
TestA a = new TestA();
a.a = 1;
a.b = b;
a.c = 3;
System.out.println("Summe: " + a.summe());
a.ausgeben();

Wenn aber auch nur eine der Instanzvariablen null ist, gibt es zwangsläufig eine NullPointerException. Hier müssten nun also jede Menge null-Checks eingebaut werden:

class TestB {
  public Integer wert;

  public void ausgeben() {
    System.out.println("wert: " + (wert == null ? "-" : wert.toString()));
  }
};

class TestA {
  public Integer a;
  public TestB b;
  public Integer c;

  public Integer summe() {
    if(a == null || b == null || c == null) {
      return null;
    }
    return a + b.wert + c;
  }

  public void ausgeben() {
    System.out.println("a: " + (a == null ? "-" : a.toString()));
    if(b == null) {
      System.out.println("-");
    } else {
      b.ausgeben();
    }
    System.out.println("c: " + (c == null ? "-" : c.toString()));
  }
};

Auch wenn man den Code in der Praxis noch ein wenig eleganter formulieren würde: Derartige null-Check Ansammlungen sieht man tatsächlich häufig in produktivem Code. Und hier ist auch zu erkennen, dass es mit zunehmendem Umfang immer schwieriger wird, diesen Code zu verstehen und weiterzuentwickeln. Hier kommen nun die Optionals, die mit Java 8 eingeführt wurden, ins Spiel. Prüfen wir aber noch kurz eine Alternative…

Warum null-Checks und kein Exception Handling?

Wenn die null-Checks unangenehm sind, könnte man auch auf die Idee kommen, die Exceptions einfach fliegen zu lassen und an geeigneter Stelle abzufangen und zu behandeln, also bei der Version ohne null-Checks beispielsweise:

try {
  TestA a = new TestA();
  System.out.println("Summe: " + a.summe());
  a.ausgeben();
} catch(NullPointerException e) {
  System.out.println("Die NullPointerException wurde gefangen...");
}

Das hat gleich mehrere Probleme:

  1. Hier wird Exception Handling verwendet um den Kontrollfluss des Programms zu steuern. Das macht es noch unwartbarer und ist ein klares Antipattern.
  2. Es gibt in den meisten Fällen keine geeignete Stelle um die Exception abzufangen und sinnvoll zu behandeln.
  3. Wenn ein Wert null sein kann, gibt es meistens einen guten Grund dafür. Dann darf die Operation nicht einfach durch eine Exception abgebrochen werden. Gewünscht ist üblicherweise eine gesonderte Behandlung des „Kein Wert vorhanden“ Falls. In der Version mit null Checks soll die Ausgabe in diesem Fall beispielsweise einen Hinweis darauf liefern, dass ein Wert nicht vorhanden ist („-“ in den entsprechenden Ausgaben).

Wie macht man es nun aber richtig und erhöht gleichzeitig die Lesbarkeit und die Wartbarkeit? Jetzt kommen die Optionals ins Spiel! Mehr dazu im zweiten Teil dieses Artikels…

Schreibe einen Kommentar

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