Null ist keine Option…


Tony Hoare: „I call it my billion-dollar mistake. It was the invention of the null reference in 1965.“

Wer Sorgen hat, hat auch Likör, und wer Null-Referenzen hat, hat auch NullPointerExceptions. C++ hat null und deshalb hat Java auch null – kein wirklich überzeugender Grund für dieses „Feature“. Und auch die propagierte @NonNull Annotation greift meiner Meinung nach zu kurz. Dabei haben wir seit Java 1.5 die Möglichkeit, ausdrucksstarke Wrapper zu schreiben. Hier nun Scalas Lösung des Problems (stark vereinfacht) nach Java übersetzt:

import java.util.Iterator;
import java.util.NoSuchElementException;

public abstract class Option<T> implements Iterable<T> {
  private static final None NONE = new None();
  abstract boolean isEmpty();
  abstract T get();
  abstract T getOrElse(T other);
  abstract Option<T> or(Option<? extends T> other);
  abstract boolean contains(T t);

  private static final class None<T> extends Option<T> {
    private None() {}
    public boolean isEmpty() { return true; }
    public T get() { throw new UnsupportedOperationException(); }
    public T getOrElse(T other) { return other; }
    public Option<T> or(Option<? extends T> other) { return (Option<T>) other; };
    @Override public boolean equals(Object o) { return o instanceof None; }
    @Override public int hashCode() { return 0; }
    public boolean contains(T t) { return false; }
    public Iterator<T> iterator() {
      return new Iterator<T>() {
        public boolean hasNext() { return false; }
        public T next() { throw new NoSuchElementException();  }
        public void remove() { throw new UnsupportedOperationException(); }
      };
    }
  }

  private static final class Some<T> extends Option<T> {
    private final T t;
    private Some(T t) {
      if (t == null) {
        throw new IllegalArgumentException("null not allowed");
      }
      this.t = t;
    }
    public boolean isEmpty() { return false; }
    public T get() { return t; }
    public T getOrElse(T other) { return t; }
    public Option<T> or(Option<? extends T> other) { return this; }
    @Override public boolean equals(Object o) {
       return o instanceof Some ? t.equals(((Some) o).t) : false; }
    @Override public int hashCode() { return t.hashCode(); }
    public boolean contains(T t) { return this.t.equals(t); }
    public Iterator<T> iterator() {
      return new Iterator<T>() {
        private boolean finished = false;
        public boolean hasNext() { return ! finished; }
        public T next() {
          if (finished)
            throw new NoSuchElementException();
          else {
            finished = true;
            return t;
          }
        }
        public void remove() { throw new UnsupportedOperationException(); }
      };
    }
  }

  public static <T> Option<T> none() { return NONE; }
  public static <T> Option<T> some(T t) { return new Some<T>(t); }
  public static <T> Option<T> option(T t) { return t == null ? NONE : new Some<T>(t); }
}

Die Implemetierung ist ziemlich schlicht und könnte sicher noch verbessert werden (es fehlen auch ein paar @SupressWarnings). Eine komfortablere Version findet sich bei functional java: Option

Wie funktioniert das nun? Option<T> ist eine abstrakte Klasse mit den zwei Unterklassen Some<T> und None. Some ist ein einfacher Wrapper um unser Objekt und None steht für null. Es scheint so, als haben wir damit die Sache nur unnötig kompliziert und nichts gewonnen – aber das scheint wirklich nur so. None ist eine normale Singleton-Klasse und Unterklasse von Option mit allen Methoden. Und Option implementiert z.B. Iterable. Hier ein paar Code-Schnipsel zur Anwendung:

//Beispiel 1 - Strings ausgeben
 
//mit null
String s = sonstwoher.getString();
//so ist es richtig
if(s != null) {
  System.out.println(s); 
}  
//ohne Check --> der Compiler meckert nicht, NPE kann auftreten
System.out.println(s); 
 
//mit Option --> man ist *gezwungen*, den Wert auszupacken, keine NPE möglich
Option<String> opt= sonstwoher.getStringOption();
for(s : opt) { 
  System.out.println(s); 
}
 
//Beispiel 2 - Default-Werte mit getOrElse
 
//mit einer normalen Map, die Null-Werte für nicht vorhandene Schlüssel liefert
Map<String,Integer> map = new HashMap<String,Integer>();
...
public void wordCount(String word) {
   if (map.containsKey(word)) {
      map.put(word, map.get(word) + 1);
   } else {
      map.put(word, 1);
   }
}
 
//mit einer hypothetischen Map, die Options liefert (also None, wenn Schlüssel nicht vorhanden ist)
OptionMap<String, Integer> map = new OptionMap<String, Integer>();
 
public void wordCount(String word) {
   map.put(word, map.get(word).getOrElse(0) + 1);
}

Ich gebe zu, dass Options an einigen Stellen in Java „unbequemer“ zu benutzen sind. Allerdings sieht man jetzt sofort, wo Werte „leer“ sein können und wo nicht, man braucht keine überflüssigen Checks (wenn man konsequent auf null verzichtet), kann aber auch nie wieder einen Check vergessen. Options sind keine neue Idee: Haskell ist nun schon 15 Jahre alt, hat nie Null-Referenzen gehabt (Scala allerdings schon, wegen der Java-Kompatibilität) und kommt prächtig mit seinem „Maybe“ (so heißt es dort) aus.

Nulls sind böse und bei Licht betrachtet so notwendig wie ein Kropf. NullPointerExceptions sind besonders heimtückisch: Irgendwo wird eine Zeitbombe in Form einer Null-Referenz ins System geworfen, die dann irgendwo anders „explodiert“. Ich habe auch festgestellt, dass an vielen Stellen, wo man meint, unbedingt null zu benötigen, einfach ein Design-Fehler vorliegt. Option packt die Zeitbombe in einen sicheren Behälter voller Warnaufkleber: Will man den „gefährlichen“ Wert benutzen, ist man gewarnt. Die Java-Community hat einen anderen Weg gewählt: @NonNull ist der Versuch, unter Umgehung des Typsystems Schadensbegrenzung zu betreiben. Wir werden sehen, wie erfolgreich das ist.

Advertisements

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s