Java hat eine besondere Referenz, die Entwicklern die Haare zu Berge stehen lässt und die Grund für lange Debug-Stunden ist: die null-Referenz. Eigentlich sagt null nur aus: „nicht initialisiert“. Doch was null so problematisch macht, ist die NullPointerException, die durch referenzierte null-Ausdrücke ausgelöst wird.
Beispiel: Entwickler haben vergessen, das Attribut location mit einem Objekt zu initialisieren, sodass setLocation(…) fehlschlagen wird:
class Place {
private Point2D location;
public void setLocation( double longitude, double latitude ) {
location.setLocation( longitude, latitude ); // BANG! NullPointerException
}
}
Einsatz von null
Fehler dieser Art sind durch Tests relativ leicht aufzuspüren. Aber hier liegt nicht das Problem. Das eigentliche Problem ist, dass Entwickler allzu gerne die typenlose null[1] als magischen Sonderwert sehen, sodass sie neben „nicht initialisiert“ noch etwas anderes bedeutet:
- Erlaubt die API in Argumenten für Methoden/Konstruktoren null, heißt das meistens „nutze einen Default-Wert“ oder „nichts gegeben, ignorieren“.
- In Rückgaben von Methoden steht null oftmals für „nichts gemacht“ oder „keine Rückgabe“. Im Gegensatz dazu kodieren andere Methoden wiederum mit der Rückgabe null, dass eine Operation erfolgreich durchlaufen wurde, und würden sonst zum Beispiel Fehlerobjekte zurückgegeben.[2]
Beispiel 1
Die mit Javadoc dokumentiere Methode getTask(out, fileManager, diagnosticListener, options, classes, compilationUnits) in der Schnittstelle JavaCompiler ist so ein Beispiel:
- out: „a writer for additional output from the compiler; use system.err if null“
- fileManager: „a file manager; if null use the compiler’s standard filemanager“
- diagnosticListener: „a diagnostic listener; if null use the compiler’s default method for reporting diagnostics“
- options: „compiler options, null means no options“
- classes: „names of classes to be processed by annotation processing, null means no class names“
- compilationUnits: „the compilation units to compile, null means no compilation units“
Alle Argumente können null sein, getTask(null, null, null, null, null, null) ist ein korrekter Aufruf. Schön ist die API nicht, und besser wäre sie wohl mit einem Builder-Pattern gelöst.
Beispiel 2
Der BufferedReader erlaubt das zeilenweise Einlesen aus Datenquellen, und readLine() liefert null, wenn es keine Zeile mehr zu lesen gibt.
Beispiel 3
Viel Irritation gibt es mit der API vom Assoziativspeicher. Eine gewöhnliche HashMap kann als assoziierten Wert null bekommen, doch get(key) liefert auch dann null, wenn es keinen assoziierten Wert gibt. Das führt zu einer Mehrdeutigkeit, da die Rückgabe von get(…) nicht verrät, ob es eine Abbildung auf null gibt oder ob der Schlüssel nicht vorhanden ist.
Map<Integer,String> map = new HashMap<>();
map.put( 0, null );
System.out.println( map.containsKey( 0 ) ); // true
System.out.println( map.containsValue( null ) ); // true
System.out.println( map.get( 0 ) ); // null
System.out.println( map.get( 1 ) ); // null
Kann die Map null-Werte enthalten, muss es immer ein Paar der Art if(map.containsKey(key)), gefolgt von map.get(key) geben. Am besten verzichten Entwickler auf null in Datenstrukturen.
Da null so viele Einsatzfälle hat und das Lesen der API-Dokumentation gerne übersprungen wird, sollte es zu einigen null-Einsätzen Alternativen geben. Manches Mal ist das einfach, etwa wenn die Rückgabe Sammlungen sind. Dann gibt es mit einer leeren Sammlung eine gute Alternative zu null. Das ist ein Spezialfall des so genannten Null-Object-Patterns.
Fehler, die aufgrund einer NullPointerException entstehen, ließen sich natürlich komplett vermeiden, wenn immer ordentlich auf null-Referenzen getestet würde. Aber gerade die null-Prüfungen werden von Entwicklern gerne vergessen, da ihnen nicht bewusst ist oder sie nicht erwarten, dass eine Rückgabe null sein kann. Gewünscht ist ein Programmkonstrukt, bei dem explizit wird, dass ein Wert nicht vorhanden sein kann, sodass nicht null diese Rolle übernehmen muss. Wenn im Code lesbar ist, dass ein Wert optional ist, also vorhanden sein kann oder nicht, reduziert das Fehler.
Geschichte
Tony Hoare gilt als „Erfinder“ der null-Referenz. Heute bereut er es und nennt die Entscheidung „my billion-dollar mistake“.[3]
Optional-Typ
Die Java-Bibliothek bietet eine Art Container, der ein Element enthalten kann oder nicht. Wenn der Container ein Element enthält, ist es nie null. Dieser Container kann befragt werden, ob er ein Element enthält oder nicht. Eine null als Kennung ist somit überflüssig.
Beispiel
Optional<String> opt1 = Optional.of( "Aitazaz Hassan Bangash" );
System.out.println( opt1.isPresent() ); // true
System.out.println( opt1.get() ); // Aitazaz Hassan Bangash
Optional<String> opt2 = Optional.empty();
System.out.println( opt2.isPresent() ); // false
// opt2.get() -> java.util.NoSuchElementException: No value present
Optional<String> opt3 = Optional.ofNullable( "Malala" );
System.out.println( opt3.isPresent() ); // true
System.out.println( opt3.get() ); // Malala
Optional<String> opt4 = Optional.ofNullable( null );
System.out.println( opt4.isPresent() ); // false
// opt4.get() -> java.util.NoSuchElementException: No value present
final class java.util.Optional<T>
- static<T>Optional<T>empty()
Liefert ein leeres Optional-Objekt.
- booleanisPresent()
Liefert wahr, wenn dieses Optional einen Wert hat, sonst ist wie im Fall von empty() die Rückgabe false.
- static<T>Optional<T>of(Tvalue)
Baut ein neues Optional mit einem Wert auf, der nicht null sein darf; andernfalls gibt es eine NullPointerException. null in das Optional hineinzubekommen, geht also nicht.
- static<T>Optional<T>ofNullable(Tvalue)
Liefert ein Optional mit dem Wert, wenn dieser ungleich null ist, bei null ist die Rückgabe ein empty().
- Tget()
Liefert den Wert. Enthält das Optional keinen Wert, weil kein Wert isPresent() ist, folgt eine NoSuchElementException.
- TorElse(Tother)
Ist ein Wert isPresent(), liefere den Wert. Ist das Optional leer, liefere other.
- Stream<T> stream()
Konvertiere das Optional in den Datentyp Stream. Neu in Java 9.
Des Weiteren überschreibt Optional die Methoden equals(…), toString() und hashCode() – 0, wenn kein Wert gegeben ist, sonst Hashcode vom Element – und ein paar weitere Methoden, die wir uns später anschauen.
Hinweis: Intern null zu verwenden hat zum Beispiel den Vorteil, dass die Objekte serialisiert werden können. Optional implementiert Serializable nicht, daher sind Optional-Attribute nicht serialisierbar, können also etwa nicht im Fall von Remote-Aufrufen mit RMI übertragen werden. Auch die Abbildung auf XML oder auf Datenbanken ist umständlicher, wenn nicht JavaBean-Properties herangezogen werden, sondern die internen Attribute.
Ehepartner oder nicht?
Optional wird also dazu verwendet, im Code explizit auszudrücken, ob ein Wert vorhanden ist oder nicht. Das gilt auf beiden Seiten: Der Erzeuger muss explizit ofXXX(…) aufrufen und der Nutzer explizit isPresent() oder get(). Beide Seiten sind sich bewusst, dass sie es mit einem Wert zu tun haben, der optional ist, also existieren kann oder nicht. Wir wollen das in einem Beispiel nutzen, und zwar für eine Person, die einen Ehepartner haben kann:
public class Person {
private Person spouse;
public void setSpouse( Person spouse ) {
this.spouse = Objects.requireNonNull( spouse );
}
public void removeSpouse() {
spouse = null;
}
public Optional<Person> getSpouse() {
return Optional.ofNullable( spouse );
}
}
In diesem Beispiel ist null für die interne Referenz auf den Partner möglich; diese Kodierung soll aber nicht nach außen gelangen. Daher liefert getSpouse() nicht direkt die Referenz, sondern es kommt Optional zum Einsatz und drückt aus, ob eine Person einen Ehepartner hat oder nicht. Auch bei setSpouse(…) akzeptieren wir kein null, denn null-Argumente sollten so weit wie möglich vermieden werden. Ein Optional ist hier nicht angemessen, weil es ein Fehler ist, null zu übergeben. Zusätzlich sollte natürlich die Javadoc an setSpouse(…) dokumentieren, dass ein null-Argument zu einer NullPointerException führt. Daher passt Optional als Parametertyp nicht.
Person heinz = new Person();
System.out.println( heinz.getSpouse().isPresent() ); // false
Person eva = new Person();
heinz.setSpouse( eva );
System.out.println( heinz.getSpouse().isPresent() ); // true
System.out.println( heinz.getSpouse().get() ); // com/…/Person
heinz.removeSpouse();
System.out.println( heinz.getSpouse().isPresent() ); // false
Primitive optionale Typen
Während Referenzen null sein können und auf diese Weise das Nichtvorhandensein anzeigen, ist das bei primitiven Datentypen nicht so einfach. Wenn eine Methode ein boolean zurückgibt, bleibt neben true und false nicht viel übrig, und ein „nicht zugewiesen“ wird dann doch gerne wieder über einen Boolean verpackt und auf null getestet. Gerade bei Ganzzahlen gibt es immer wieder Rückgaben wie –1.[4] Das ist bei den folgenden Beispielen der Fall:
- Wenn bei InputStreams read(…) keine Eingaben mehr kommen, wird -1 zurückgegeben.
- indexOf(Object) von List liefert -1, wenn das gesuchte Objekt nicht in der Liste ist und folglich auch keine Position vorhanden ist.
- Bei einer unbekannten Bytelänge einer MIDI-Datei (Typ MidiFileFormat) hat getByteLength() als Rückgabe -1.
Diese magischen Werte sollten vermieden werden, und daher kann auch der optionale Typ wieder erscheinen.
Als generischer Typ kann Optional beliebige Typen kapseln, und primitive Werte könnten in Wrapper verpackt werden. Allerdings bietet Java für drei primitive Typen spezielle Optional-Typen an: OptionalInt, OptionalLong, OptionalDouble:
Optional<T> |
OptionalInt |
OptionalLong |
OptionalDouble |
static <T>
Optional<T>
empty() |
static
OptionalInt
empty() |
static
OptionalLong
empty() |
static
Optional-Double
empty() |
T get() |
int getAsInt() |
long getAsLong() |
double getAsDouble() |
boolean isPresent() |
static <T>
Optional<T>
of(T value) |
static OptionalInt
of(int value) |
static OptionalLong
of(long value) |
static OptionalDouble
of(double value) |
static <T>
Optional<T>
ofNullable(T
value) |
nicht übertragbar |
T orElse(T other) |
int orElse(int other) |
long orElse(long other) |
double orElse(
double other) |
boolean equals(Object obj) |
int hashCode() |
String toString() |
Tabelle: Methodenvergleich zwischen den vier OptionalXXX-Klassen
Die Optional-Methode ofNullable(…) fällt in den primitiven Optional-Klassen natürlich raus. Die optionalen Typen für die drei primitiven Typen haben insgesamt weniger Methoden, und die obere Tabelle ist nicht ganz vollständig. Wir kommen im Rahmen der funktionalen Programmierung in Java noch auf die verbleibenden Methoden wie isPresent(…) zurück.
Best Practice: OptionalXXX-Typen eignen sich hervorragend als Rückgabetyp, sind als Parametertyp denkbar, doch wenig attraktiv für interne Attribute. Intern ist null eine akzeptable Wahl, der „Typ“ ist schnell und speicherschonend.
Erstmal funktional mit Optional
Neben den vorgestellten Methoden wie ofXXX(…) und isPresent() gibt es weitere, die auf funktionale Schnittstellen zurückgreifen:
final class java.lang.Optional<T>
- voidifPresent(Consumer<?superT>consumer)
Repräsentiert das Optional einen Wert, rufe den Consumer mit diesem Wert auf, andernfalls mache nichts.
- void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction)
Repräsentiert das Optional einen Wert, rufe den Consumer mit diesem Wert auf, andernfalls führe emptyAction Das Runnable muss hier als Typ aus java.lang herhalten, weil es im java.util.function-Paket keine Schnittstelle gibt, die keine Parameter hat und auch keine Rückgabe liefert. Neu in Java 9.
- Optional<T>filter(Predicate<?superT>predicate)
Enthält das Optional einen Wert und ist das Prädikat predicate auf dem Wert wahr, ist die Rückgabe das eigene Optional (also this), sonst ist die Rückgabe empty().
- <U>Optional<U>map(Function<?superT,?extendsU>mapper)
Repräsentiert das Optional einen Wert, dann wende die Funktion an und verpacke das Ergebnis (wenn es ungleich null ist) wieder in ein Optional. Ist das Optional ohne Wert, dann ist die Rückgabe empty(), genauso, wenn die Funktion null liefert.
- <U>Optional<U>flatMap(Function<?superT,Optional<U>>mapper)
Wie map(…), nur dass die Funktion ein Optional statt eines direkten Werts gibt. Liefert die Funktion mapper ein leeres Optional, so ist das Ergebnis von flatMap(…) auch empty().
- Optional<T> or(Supplier<? extends Optional<? extends T>> supplier)
Repräsentiert das Optional einen Wert, so liefere ihn. Ist das Optional leer, beziehe den Wert aus dem anderen Optional. Neu in Java 9.
- TorElseGet(Supplier<?extendsT>other)
Repräsentiert das Optional einen Wert, so liefere ihn; ist das Optional leer, so beziehe den Alternativwert aus dem Supplier.
- <XextendsThrowable>TorElseThrow(Supplier<?extendsX>exceptionSupplier)
Repräsentiert das Optional einen Wert, so liefere ihn, andernfalls hole mit Supplier das Ausnahme-Objekt, und löse es aus.
Beispiel: Wenn das Optional keinen Wert hat, soll eine NullPointerException statt der NoSuchElementException ausgelöst werden.
String s = optionalString.orElseThrow( NullPointerException::new );
Beispiel für NullPointerException-sichere Kaskadierung von Aufrufen mit Optional
Die beiden XXXmap(…)-Methoden sind besonders interessant und ermöglichen einen ganz neuen Programmierstil. Warum, soll ein Beispiel zeigen.
Der folgende Zweizeiler gibt auf meinem System „MICROSOFT KERNELDEBUGGER-NETZWERKADAPTER“ aus:
String s = NetworkInterface.getByIndex( 2 ).getDisplayName().toUpperCase();
System.out.println( s );
Allerdings ist der Programmcode alles andere als gut, denn NetworkInterface.getByIndex(int) kann null zurückgeben und getDisplayName() auch. Um ohne eine NullPointerException um die Klippen zu schiffen, müssen wir schreiben:
NetworkInterface networkInterface = NetworkInterface.getByIndex( 2 );
if ( networkInterface != null ) {
String displayName = networkInterface.getDisplayName();
if ( displayName != null )
System.out.println( displayName.toUpperCase() );
}
Von der Eleganz des Zweizeilers ist nicht mehr viel geblieben. Integrieren wir Optional (was ja eigentlich ein toller Rückgabetyp für getByIndex() und getDisplayName() wäre):
Optional<NetworkInterface> networkInterface = Optional.ofNullable( NetworkInterface.getByIndex( 2 ) );
if ( networkInterface.isPresent() ) {
Optional<String> name = Optional.ofNullable( networkInterface.get().getDisplayName() );
if ( name.isPresent() )
System.out.println( name.get().toUpperCase() );
}
Mit Optional wird es nicht sofort besser, doch statt if können wir einen Lambda-Ausdruck nehmen und bei ifPresent(…) einsetzen:
Optional<NetworkInterface> networkInterface = Optional.ofNullable( NetworkInterface.getByIndex( 2 ) );
networkInterface.ifPresent( ni -> {
Optional<String> displayName = Optional.ofNullable( ni.getDisplayName() );
displayName.ifPresent( name -> {
System.out.println( name.toUpperCase() );
} );
} );
Wenn wir die lokalen Variablen networkInterface und displayName entfernen, landen wir bei:
Optional.ofNullable( NetworkInterface.getByIndex( 2 ) ).ifPresent( ni -> {
Optional.ofNullable( ni.getDisplayName() ).ifPresent( name -> {
System.out.println( name.toUpperCase() );
} );
} );
Von der Struktur her ist das mit der if-Abfrage identisch und über die Einrückungen auch zu erkennen. Fallunterscheidungen mit Optional und ifPresent(…) umzuschreiben bringt keinen Vorteil.
In Fallunterscheidungen zu denken hilft hier nicht weiter. Was wir uns bei NetworkInterface.getByIndex( 2 ).getDisplayName().toUpperCase() vor Augen halten müssen, ist eine Kette von Abbildungen. NetworkInterface.getByIndex(int) bildet auf NetworkInterface ab, getDisplayName() von NetworkInterface bildet auf String ab, und toUpperCase() bildet von einem String auf einen anderen String ab. Wir verketten drei Abbildungen und müssten ausdrücken können: Wenn eine Abbildung fehlschlägt, dann höre mit der Abbildung auf. Und genau hier kommen Optional und map(…) ins Spiel. In Code:
Optional<String> s = Optional.ofNullable( NetworkInterface.getByIndex( 2 ) )
.map( ni -> ni.getDisplayName() )
.map( name -> name.toUpperCase() );
s.ifPresent( System.out::println );
Die Klasse Optional hilft uns bei zwei Dingen: Erstens wird map(…) beim Empfangen einer null-Referenz auf ein Optional.empty() abbilden, und zweitens ist das Verketten von leeren Optionals kein Problem, es passiert einfach nichts – Optional.empty().map(…) führt nichts aus, und die Rückgabe ist einfach nur ein leeres Optional. Am Ende der Kette steht nicht mehr String (wie am Anfang des Beispiels), sondern Optional<String>.
Umgeschrieben mit Methodenreferenzen und weiter verkürzt ist der Code sehr gut lesbar und Null-Pointer-Exception-sicher:
Optional.ofNullable( NetworkInterface.getByIndex( 2 ) )
.map( NetworkInterface::getDisplayName )
.map( String::toUpperCase )
.ifPresent( System.out::println );
Die Logik kommt ohne externe Fallunterscheidungen aus und arbeitet nur mit optionalen Abbildungen. Das ist ein schönes Beispiel für funktionale Programmierung.
Primitiv-Optionales mit
Die eigentliche Optional-Klasse ist generisch und kapselt jeden Referenztyp. Auch für die primitiven Typen int, long und double gibt es in drei speziellen Klassen OptionalInt, OptionalLong, OptionalDouble Methoden zur funktionalen Programmierung. Stellen wir die Methoden der vier OptionalXXX-Klassen gegenüber:
Optional<T> |
OptionalInt |
OptionalLong |
OptionalDouble |
static <T>
Optional<T>
empty() |
static
OptionalInt
empty() |
static
OptionalLong
empty() |
static
OptionalDouble
empty() |
T get() |
int getAsInt() |
long getAsLong() |
double
getAsDouble() |
boolean isPresent() |
static <T>
Optional<T> of(T value) |
static OptionalInt
of(int value) |
static OptionalLong
of(long value) |
static Optional
Double
of(double value) |
static <T>
Optional<T>
ofNullable(T value) |
nicht übertragbar |
T orElse(T other) |
int orElse(int other) |
long orElse(long other) |
double orElse(double other) |
Stream<T> stream() |
IntStream stream() |
LongStream stream() |
DoubleStream stream() |
boolean equals(Object obj) |
int hashCode() |
String toString() |
void ifPresent(
Consumer<? super T> consumer) |
void ifPresent(
IntConsumer
consumer) |
void ifPresent(
LongConsumer
consumer) |
void ifPresent(
DoubleConsumer
consumer) |
void ifPresentOrElse( Consumer<? super T> action, Runnable emptyAction) |
void ifPresentOrElse( IntConsumer action, Runnable emptyAction) |
void ifPresentOrElse( LongConsumer action, Runnable emptyAction) |
void ifPresentOrElse( DoubleConsumer action, Runnable emptyAction) |
T orElseGet( Supplier<? extends T> other) |
int orElseGet( IntSupplier other) |
long orElseGet( LongSupplier other) |
double orElseGet( DoubleSupplier other) |
<X extends Throwable> T orElseThrow( Supplier<? extends X> exceptionSupplier) |
<X extends Throwable> int orElseThrow( Supplier<? extends X> exceptionSupplier) |
<X extends Throwable> long orElseThrow( Supplier<? extends X> exceptionSupplier) |
<X extends Throwable> double
orElseThrow( Supplier<? extends X>
exceptionSupplier) |
Optional<T> filter(Predicate<? super T> predicate) |
nicht vorhanden |
<U> Optional<U>
flatMap(Function
<? super T,Optional<U>> mapper) |
<U> Optional<U> map(Function<? super T,? extends U> mapper) |
Tabelle 1.13: Vergleich von Optional mit den primitiven OptionalXXX-Klassen
[1] null instanceof Typ ist immer false .
[2] Zum Glück wird null selten als Fehler-Identifikator genutzt, die Zeiten sind vorbei. Hier sind Ausnahmen die bessere Wahl, denn Fehler sind Ausnahmen im Programm.
[3] Er sagt dazu: „It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.“ Unter http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare gibt es ein Video mit ihm und Erklärungen.
[4] Unter http://docs.oracle.com/javase/8/docs/api/constant-values.html lassen sich alle Konstantendeklarationen einsehen.