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

Im ersten Teil des Artikels haben wir uns angesehen wie traditionell die Aussage „ich habe keinen Wert“ in Java modelliert und behandelt wird. Die null Checks, mit denen auf null Werte reagiert wird, werden dabei recht schnell etwas unübersichtlich. Deshalb betrachten wir nun wie die mit Java 8 eingeführten Optionals beim Umgang mit diesem Problem helfen.

Prinzip: Nur wirklich optionale Werte können null werden

Beim Modellieren einer Klasse gibt es häufig Werte, die immer da sein müssen und Werte, die nicht unbedingt immer vorhanden sind. Um lesbaren und wartbaren Code herzustellen können wir diese Situationen direkt im Code kenntlich machen.

class Fahrersitz {
};

class Anhaengerkupplung {
  private final String name;

  public Anhaengerkupplung(String name) {
    this.name = name;
  }
};

class Auto {
  private final Fahrersitz fahrersitz;
  private Anhaengerkupplung anhaengerkupplung;

  public Auto(Fahrersitz fahrersitz) {
    this.fahrersitz = fahrersitz;
  }

  public void setAnhaengerkupplung(Anhaengerkupplung anhaengerkupplung) {
    this.anhaengerkupplung = anhaengerkupplung;
  }
}

Die Klasse Auto soll stark vereinfacht ein Auto modellieren. Jedes Auto in unserer Modellwelt hat einen Fahrersitz. Aber nicht jedes Auto hat eine Anhängerkupplung. Das drücken wir in Java aus, indem wir den Fahrersitz final, und damit unveränderlich, machen und den Verwender der Klasse zwingen den Fahrersitz direkt im Konstruktor mit zu übergeben. Ebenso hat eine Anhängerkupplung immer einen Namen, den wir im Konstruktor mit übergeben müssen.

Die Anhängerkupplung ist hingegen optional. Manche Autos haben eine, manche nicht. Dieser Wert wird deshalb nicht über den Konstruktor übergeben, sondern kann später über einen Setter gesetzt werden. Solange keine Anhängerkupplung definiert wird, bleibt die Instanzvariable null.

Fahrersitz fahrersitz = new Fahrersitz();
Auto auto = new Auto(fahrersitz);
// Hier haben wir ein Auto mit einem Fahrersitz und ohne Anhängerkupplung
Anhaengerkupplung anhaengerkupplung = new Anhaengerkupplung("Meine super Kupplung");
auto.setAnhaengerkupplung(anhaengerkupplung);
// Jetzt hat das Auto auch eine Anhängerkupplung

Wir können nun also schon sehr deutlich formulieren, welche Attribute ein Objekt einer Klasse unbedingt haben muss und welche optional sind. Aber wie greifen wir nun auf optionale Attribute zu, wenn wir die null Checks vermeiden möchten?

Prinzip: Optionale Werte sind optional

Auch mit den neuen Optionals wird die Information „dieser Wert ist derzeit nicht vorhanden“ durch null gekennzeichnet. Aber der Zugriff darauf erfolgt nun über ein Optional. Anstelle eines klassischen Getters schreiben wir eine Methode, die ein Optional zurückliefert:

  public Optional<Anhaengerkupplung> anhaengerkupplung() {
    return Optional.ofNullable(anhaengerkupplung);
  }

Diese Methode erzeugt ein Objekt der Klasse Optional und gibt diesem Objekt den Wert, den wir kennen (also entweder eine Anhängerkupplung oder eben null) mit. Die Klasse Optional dient dazu ganz explizit auszudrücken, dass ein Wert optional ist und sehr deutlich darzustellen, ob wir einen Wert kennen oder nicht. Ein Optional ist selbst nie null und kann uns immer mitteilen, ob es einen Wert enthält oder nicht. Wir können nun also das Ergebnis der Methode ohne null-Check verwenden:

Optional<Anhaengerkupplung> anhaengerkupplungOptional = auto.anhaengerkupplung();
String name;
if(anhaengerkupplung.isPreset()) {
  Anhaengerkupplung anhaengerkupplung = anhaengerkupplungOptional.get();
  name = anhaengerkupplung.getName();
} else {
  name = "-";
}

Wenn man den Code so liest, sieht er eigentlich gar nicht nach einem Gewinn aus. Statt des null-Checks müssen wir nun einen anderen Check durchführen. Und das Optional Objekt müssen wir nun (mit dem get() Aufruf) auch noch auspacken. Das sieht überhaupt nicht besser aus als mit einem einfachen null-Check.

Aber schauen wir uns die Sache weiter an…

Optional<Anhaengerkupplung> anhaengerkupplungOptional = auto.anhaengerkupplung();
String name;
if(anhaengerkupplung.isPreset()) {
  Optional<String> nameOptional = anhaengerkupplungOptional.map(Anhaengerkupplung::getName);
  name = nameOptional.get();
} else {
  name = "-";
}

Hier haben wir das Auspacken des Optionals modifiziert. Der map() Methode übergeben wir eine Transformationsmethode. In diesem Fall übergeben wir eine Referenz auf die Methode getName() der Klasse Anhaengerkupplung. Die Wirkung ist interessant: Wenn das Optional tatsächlich eine Anhaengerkupplung enthält, wird für dieses Anhaengerkupplung Objekt die getName() Methode aufgerufen und ein neues Optional mit dem Ergebnis dieser Methode erzeugt. Aus einem Optional<Anhaengerkupplung> haben wir damit ein Optional<String> gemacht. Falls das Optional keinen Wert enthält, wird die getName() Methode nicht aufgerufen; und das Optional bleibt einfach leer.

Und nun vereinfachen wir das Ganze noch kräftig:

String name = auto.anhaengerkupplung().map(Anhaengerkupplung::getName).orElse("-");

Neu ist hier das orElse(). Falls das Optional einen Wert enthält, packt orElse() diesen Wert (mittels get()) aus und liefert ihn. Aus einem Optional<String> wird damit also der enthaltene String ausgepackt. Ganz ohne Aufruf von get(). Und falls das Optional keinen Wert enthält, wird einfach der Wert zurückgeliefert, den wir dem orElse() mitgeben.

Im Endeffekt erhalten wir in name also entweder den Wert der Instanzvariable name aus der Anhängerkupplung des auto Objekts oder einfach den String „-„, falls das Auto keine Anhängerkupplung hat. Und das alles nun ganz ohne null-Checks. Jetzt wird es interessant, oder?

Im nächsten Teil des Artikels loten wir die Möglichkeiten von Optionals weiter aus.

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…