|
|||||||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||||||||
|
Effective Java
|
||||||||||||||||||||||||||||||||
In unserer Reihe zum Thema Java 8 Neuerungen wollen wir uns diesmal die Klasse java.util.Optional<T> ansehen. Wir hatten dieses Thema bereits in unserem Artikel über Kollektoren (/ JAV8-6 /) kurz angeschnitten. Diesmal wollen wir ins Detail gehen. Einleitung
Optional
ist
eine Abstraktion, die in Java 8 zum JDK hinzugekommen ist und von einigen
Stream-Operationen als Returntyp verwendet wird. Ein
Optional
kann einen Wert enthalten oder aber leer sein, daher der Name: der Wert
ist
optional
. Da die Entwicklung der
Optional
-Klasse
von kontroversen Diskussionen begleitet wurde, sowohl im OpenJDK Forum
(/OJLD/) wie auch außerhalb (/JPNG/), wollen wir sie näher anschauen.
Genau genommen ist nicht nur die Klasse
Optional<T>
zum
JDK dazugekommen, sondern auch ihre
primitiven
Geschwisterklassen
OptionalInt
,
OptionalLong
und
OptionalDouble
. Wir werden uns
in dem Artikel aber auf
Optional<T>
konzentrieren, da die anderen Klassen nur Varianten für die primitiven
Typen
int
,
long
und
double
sind. Alles Gesagt gilt
so oder so ähnlich auch für die primitiven
Optional
-Klassen.
Wo es Abweichungen gibt, werden wir diese erwähnen.
Beginnen wir damit, wie Optional im JDK benutzt wird. Optional und Streams
Optional
wird
im JDK API von einigen wenigen Stream-Operationen als Returntyp genutzt.
Ein Beispiel dafür ist die Stream-Operation:
Optional<T> reduce(BinaryOperator<T> accumulator)
Warum gibt
reduce()
hier ein
Optional<T>
zurück? Zumal
es auch eine überladen Version von
reduce()
gibt, die kein
Optional<T>
sondern
ein
T
zurück gibt:
T reduce(T identity, BinaryOperator<T> accumulator)
Was macht den Unterschied? Schauen wir uns das an einem Beispiel an, zuerst die Version, die T zurückgibt:
int sum = Stream.of(-2, -1, 0, 1, 2) .reduce(0, (i, j) -> i + j);
System.out.println("sum is: " + sum);
Diese Version funktioniert so, dass sie als ersten Parameter einen Anfangswert und als zweiten Parameter die reduce-Funktionalität übergeben bekommt. Die Javadoc fordert, dass der Anfangswert bezüglich der reduce-Funktionalität das neutrale Element sein muss. Das ist bei uns der Fall: der Anfangswert ist 0, die reduce-Funktionalität ist die Integer-Addition und offensichtlich liefert die Addition mit 0 immer den Ausgangswert. Lässt man das Beispiel ablaufen, ist seine Ausgabe wie folgt:
sum is: 0
Das ist nicht besonders überraschend. Wenn wir uns die Stream-Elemente ansehen, so können wir feststellen, dass sich diese in der Tat zu 0 addieren. Falls man die Stream-Elemente aber nicht kennt, ist das Ergebnis 0 nicht so einfach zu interpretieren. Es könnte ja so sein, dass der Stream leer ist und wir einfach nur den Anfangswert 0 wieder zurückbekommen, ohne dass es überhaupt zur Addition kommt. Dies ist zum Beispiel hier der Fall:
int sum = Stream.of(-2, -1, 0, 1, 2) .filter(i -> i > 10) .reduce(0, (i, j) -> i + j);
System.out.println("sum is: " + sum);
Durch den Filter kommen nur die
Integer
-Werte,
die größer als 10 sind. Das ist in unserem Fall keines der Stream-Elemente.
Trotzdem ist die Ausgabe die gleiche wie oben.
Um diese Situation auflösen zu können und
entscheiden zu können, ob die Summe 0 ist oder ob nur der Anfangswert
wieder zurückgegeben wurde, kann man die reduce-Operation nutzen, die
keinen Anfangswert hat, dafür aber
Optional<T>
zurückgibt.
Optional<Integer> opt = Stream.of(-2, -1, 0, 1, 2)
.reduce((i, j) -> i + j);
if (opt.isPresent()) // Zeile 4 System.out.println("sum is: " + opt.get()); else
System.out.println("stream was empty");
Um festzustellen, ob das
Optional
überhaupt einen Wert enthält, nutzen wir die Methode
isPresent()
(Zeile 4). Und um auf den Wert zuzugreifen, nutzen wir die Methode
get()
(darauf folgende Zeile).
Die Ausgabe dieses Programms ist natürlich wieder die gleiche wie bisher. Wenn wir aber nun wieder die filter-Operation vor der reduce-Operation einbauen, ist die Ausgabe anders:
stream was empty
Damit haben wir unser Ziel erreicht und können
die beiden Situationen "Summe der Elemente gleich 0" oder "leerer Stream"
voneinander unterscheiden.
Bei allen anderen Stream-Operationen, die Optional<T> als Returnwert haben, ist die Situation ähnlich. Immer geht es darum zu unterscheiden, • gibt es ein Ergebnis und was ist dies, oder
•
gibt
es gar kein Ergebnis, weil der Stream leer ist.
Die weiteren Stream-Operationen, die Optional zurückgeben, sind findAny() , findFirst() , max() und min() . Dabei sind die letzten beiden Convenience-Operationen, die das reduce() ohne Anfangswert nutzen. API
Schauen wir uns nun die generischen Klasse
Optional<T>
genauer an. In den vorhergehenden Beispielen haben wir schon gesehen,
dass die Klasse im Wesentlichen ein Wrapper um ein
T
ist. Nach Außen stellt die Klasse die Funktionalität zur Verfügung,
•
um
zu erfahren, ob ein
T
enthalten ist oder
ob das
Optional
-Objekt leer ist (Methode:
isPresent()
),
und
•
um
auf das
T
zuzugreifen, falls es enthalten
ist (Methode:
get()
).
Was man noch wissen muss:
get()
wirft eine
NoSuchElementException
, wenn
das Objekt leer ist. Deshalb macht es Sinn (wie in unserem Beispiel oben,
Zeile 4), erst mit
isPresent()
zu prüfen,
ob das
Optional
nicht leer ist, bevor
man mit
get()
darauf zugreift.
Die Klasse
Optional<T>
ist
final
. Das heißt, man kann nicht
von ihr ableiten. Sie hat nur zwei
private
Konstruktoren und keine weiteren
public
Konstruktoren. Zum Erzeugen von Instanzen muss man deshalb eine der drei
statischen Factory-Methoden verwenden:
Optional<T> empty() Optional<T> of(T value)
Optional<T> ofNullable(T value)
Um zu verstehen, was die Factory-Methoden tun, hilft es, wenn man weiß, dass die Klasse nur ein privates Feld value vom Type T hat. Die Factory-Methode empty() , die ein leeres Objekt erzeugt, initialisiert das Feld mit null (die Details der Implementierung sind etwas komplizierter, aber das Resultat ist das gleiche). Die Factory-Methode of() initialisiert das Feld mit dem übergebenen Parameter value . Voraussetzung dabei ist, dass value != null ist, sonst wirft of() eine NullPointerException . Die Factory-Methode ofNullable() ist lediglich eine Convenience-Methode, die sich der beiden anderen Factory-Methoden bedient. Sie ist implementiert als: return value == null ? empty() : of(value) ;
Wir werden später sehen, wozu man die Methode
ofNullable()
benutzen kann (Abschnitt: Optional als "Bessere
n
ull
"
?).
Aus der Beschreibung der Factory-Methoden ergibt sich, dass get() niemals null zurückliefert kann, oder anders ausgedrückt: Optional<T> kann niemals ein Wrapper um null sein. Streams können im Allgemeinen aber null -Elemente enthalten, da Arrays und Collections null -Elemente enthalten können. Was passiert nun, wenn das Ergebnis einer Stream-Operation ein Optional von null sein müsste? Was kommt zum Beispiel hier raus (das erste Element des Streams, das von findFirst() zurückgeliefert wird, ist null ):
Optional<String> opt = Stream.of(null, "hello", "world").findFirst();
Diese Zeile erzeugt gar kein
Optional
<String>
,
sondern wirft vorher einer
NullPointerException
.
Die
NullPointerException
kommt intern
aus dem Stream-Framework, wenn die Factory-Methode
Optional.of()
mit dem ersten Stream-Element
null
als
Parameter aufgerufen wird.
Die primitiven Optional Klassen weichen etwas vom vorher Beschriebenen ab, da es für die primitiven Typen kein Äquivalent zu null gibt. Fluent API
Was wir uns bisher vom API angesehen haben,
reicht eigentlich vollständig aus, um den
Optional
-Typ
im imperativen Programmierstil zu benutzen. Wir können:
•
Instanzen
von
Optional
konstruieren,
•
prüfen
ob
Optional
leer ist oder nicht,
•
auf
den in
Optional
enthaltenen Wert zugreifen.
Da mit Java 8 aber Elemente der Funktionalen
Programmierung und des
fluent
Programmierstils in Java Einzug gehalten
haben, gibt es auch in
Optional
weitere
Methoden, die diesen Programmierstil unterstützen. Fangen wir an mit:
void ifPresent(Consumer<? super T> consumer)
Diese Methode erlaubt es, einen
Consumer
an ein
Optional
anzuhängen. Dieser
konsumiert den im
Optional
enthaltenen
Wert, falls das
Optional
nicht leer ist.
Ein Beispiel dazu:
Stream.of(-2, -1, 0, 1, 2) .reduce((i, j) -> i + j)
.
ifPresent
(sum
-> System.out.println("sum is: " + sum));
Die Ausgabe ist wieder:
sum is: 0
Wenn wir vor der reduce-Operation wieder
den Filter
filter(i -> i > 20)
einbauen,
bleibt die Ausgabe leer. Denn wie vorher schon gesagt, falls das
Optional
leer ist, wird der
Consumer
des
ifPresent()
nicht ausgeführt.
Zusätzlich gibt es drei Methoden, die darauf reagieren, wenn das Optional leer ist:
T orElse(T other) T orElseGet(Supplier<? extends T> other) T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X
Die ersten beiden Methoden dienen dazu, ein
Objekt vom Typ
T
zu erzeugen, welches
das fehlende Ergebnis ersetzt. Die dritte Methode erlaubt es, eine Exception
zu werfen, um so zu signalisieren, dass es kein Ergebnis gibt. Schauen
wir uns ein Beispiel an, das die erste Methode nutzt:
int sum = Stream.of(-2, -1, 0, 1, 2) .filter(i -> i > 10) // Zeile 2 .reduce((i, j) -> i + j)
.
orElse(0)
;
System.out.println("sum
is: " + sum);
Unabhängig davon, ob die Zeile 2 auskommentiert wird oder nicht , liefert die Ausgabe: sum is: 0
Das heißt, die Kombination von
reduce()
und
orElse()
in diesem Beispiel gibt
das gleiche Ergebnis wie:
reduce(0, (i, j) ->
i + j)
. Wenn die Zeile auskommentiert ist, ist 0 das Ergebnis der
Addition. Wenn die Zeile nicht auskommentiert ist, ist 0 der von
orElse()
gesetzte Wert.
Es gibt noch weitere Methoden im API, die den fluent-Programmierstil unterstützen. Wenn man mit Funktionaler Programmierung nicht besonders vertraut ist, versteht man sie am besten, wenn man sich kurz an unseren Einführungsartikel zu Streams erinnert (/ JAV8-4 /). Dort haben wir erläutert, dass die Stream-Operationen (wie filter , map , reduce , etc.) Funktionalität auf jedes Element des Streams anwenden. Dabei kann der Stream kein , ein oder mehrere Elemente enthalten. Die Funktionalität wird demgemäß nicht , einmal oder mehrmals angewandt. Ein Optional kann man sich ähnlich vorstellen wie einen Stream: es ist eine Abstraktion, die kein oder ein Element enthält. Das heißt, anders als der Stream kann das Optional nur höchstens ein Elemente enthalten und niemals mehrere. Analog zum Stream kann man auf dem Optional auch Operationen wie filter , map , etc. aufrufen. Sie wenden Funktionalität auf das im Optional enthaltene Element an; die Funktionalität wird dann nicht oder einmal ausgeführt, je nachdem, ob das Optional kein oder ein Element enthält. Die betreffenden Operationen im Optional -API sind:
Optional<T> filter(Predicate<? super T> predicate) Optional<U> map(Function<? super T, ? extends U> mapper) Optional<U> flatMap(Function<? super T, Optional<U>> mapper )
Wo wir gerade dabei sind: unter diesem
Blickwinkel entspricht natürlich das
ifPresent()
des
Optionals
dem
forEach()
des Streams.
Die primitiven
Optional
-Typen
enthalten übrigens die Methoden
filter()
,
map()
,
flatMap()
nicht. Der Grund dafür ist, dass man neben
map()
und
flatMap()
auf jeden Fall auch noch
mapToOb
ject()
und
flatMapToObj()
benötigt hätte.
Dies hätte das API weiter aufgebläht und deshalb hat man die gesamte
Funktionalität in den primitiven
Optional
-Typen
einfach weggelassen.
Schauen wir uns ein Beispiel an, dass die
map()
-Methode
von
Optional
nutzt:
Stream<String> myStream = ... Set<String> results = ... ... Optional<Boolean> res = myStream.findAny()
.
map
(results::add);
res gibt uns Auskunft darüber, wie die vorhergehenden Arbeitsschritte ausgegangen sind. Und zwar ist res : • Opitional.empty , wenn myStream leer war, • Optional[true] , wenn das gefundene Stream-Element noch nicht in dem Set results enthalten war,
•
Optional[false]
,
wenn das gefundene Stream-Element bereits im
Set
results
enthalten war.
Wir haben vorher gesagt, dass wir Methoden wie map() und ihre Funktionalität schon vom Stream API kennen. Dazu gibt es eine kleine Einschränkung: die Anforderungen, die es beim Stream API an die Funktionalität gibt, die man an die Methoden als Parameter übergibt, gilt natürlich jetzt nicht mehr. So darf die Function , die an das map() übergibt wird, beim Stream keinen Zustand (englisch: State ) haben (/ JAV8-7 /). Beim Optional darf sie aber Zustand haben. Das sieht man in unserem Beispiel: die an map() übergebene Function benutzt als Zustand den Set results . Optional als "Bessere null " ?
Die kontroversen Diskussionen um die Klasse
Optional
(/OJLD/ und /JPNG/) beruhten im Wesentlich darauf, dass es in anderen Programmiersprachen
bereits Typen gab, die ähnlich zu
Optional
sind, zum Beispiel
Option
in Scala.
Diese Situation führte dazu, dass erwartet wurde, dass
Optional
in Java den bisher in anderen Sprachen vorhandenen Typen möglichst nahe
kommt. Je nachdem wie hoch die Erwartung an dieser Stelle war, wurde
sie zumindest für einige Leute nicht erfüllt.
Die Erwartungen betrafen im Wesentlichen
zwei Punkte: die Implementierung und die idiomatische Benutzung (Schlagwort:
better
null
). Fangen wir mit dem Thema Implementierung an, weil es sich
schneller abhandeln lässt. Ja, die Implementierung von
Optional
in Java weicht stark von der Implementierung von
Option
in Scala ab. Man hätte diesen Unterschied wohl auch kleiner machen können.
Dann wäre aber
Optional
nicht so effizient
geworden, wie es heute ist, oder besser gesagt: wie es in Zukunft sein
soll (siehe unten, Absatz: Die Zukunft von
Optional
).
Beim Thema "
Optional
als bessere
null
"
(englisch:
better
null
)
müssen wir ein wenig weiter ausholen. Scala propagiert ein Programmieridiom,
bei dem es darum geht,
Option
anstelle
von
null
zu verwenden (/PROS/). Genauer
gesagt, es gibt in Scala einen Subtyp von
Option
,
der
None
heißt, und das Programmieridiom
schlägt vor, anstelle von
null
stets
ein Objekt vom Typ
None
zu verwenden.
Die Frage ist nun, ob und wie weit man das
auch in Java machen soll. Schauen wir uns die Details an. Es geht darum
(jetzt auf Java übertragen) ein leeres
Optional
anstelle von
null
zu verwenden. Betrachten
wir dazu ein praktisches Beispiel: eine Version der überladenen statische
Methode
getenv()
aus
java.lang.System
ist so deklariert:
String getenv(String name)
.
Die Javadoc sagt über den Returnwert:
the
string value of the variable, or
null
if the variable is not defined in the system environment
. Wenn wir
nun statt
null
ein leeres
Optional
Objekt zurückzugeben wollen, müssten wir den Returntyp der Methode ändern,
so dass sich folgende Signatur ergibt:
Optional<String>
getenv(String name)
.
Immer wenn ein Wert zu
name
in der Systemumgebung gefunden wird, kommt ein
Optional<String>
mit dem gefundenen Wert zurück, sonst ein leeres
Optional
.
Interessant wird es, wenn wir uns überlegen, ob wir auch den Typ des Parameters
name
in
Optional<String>
ändern müssen.
Die Javadoc sagt über den Parameter:
throws
NullPointerException
– if
name
is
null
.
Das heißt, als Parameter darf
null
nicht
verwendet werden. Deshalb passt
Optional<String>
als Parametertyp nicht und der Parametertyp bleibt
String
.
Ein Vorteil des Idioms „
Optional
als bessere
null
“ ist also, dass Typen
deskriptiver werden.
Optional<String>
benennt einen
String
-Typ, bei dem es
erlaubt ist, dass der String gar nicht vorhanden ist.
St
r
ing
hingegen bezeichnet einen Typ, bei dem man sicher sein kann, dass sein
Wert immer vorhanden ist. So kann man gleich am Typ erkennen, wo man
mit fehlenden Werten rechnen muss und wo nicht. Das führt dann hoffentlich
zu stabilerem Code und vermeidet
NullPointerException
s
zur Laufzeit.
Ein weiterer Vorteil des "
Optional
als bessere-
null
"
-Idioms
ist die Vermeidung von Uneindeutigkeiten, die es heute bei der Verwendung
von
null
geben kann. Das typische Beispiel
für eine solche Mehrdeutigkeit ist die
get()
-Methode
der
java.util.M
ap
.
Das Beispiel ist im Original auch hier (/PROS/) beschrieben. Worum geht
es? Die
get()
-Methode holt den mit
einem Schlüssel assoziierten Wert aus einer
Map
.
Die Signatur der Methode ist:
V get(Object
key)
. Nun ist es generell erlaubt, dass zu einem Schlüssel der
Wert
null
abgelegt wird. Ruft man die
get()
-Methode
mit einem solchen Schlüssel als Parameter auf, bekommt man natürlich
null
zurück. Gleichzeitig bedeutet der Returnwert
null
aber auch, dass es keine Assoziation zu dem Schlüssel gibt, mit dem man
get()
aufgerufen hat. Das heißt, der Returnwert
null
ist mehrdeutig. Um die Uneindeutigkeit aufzulösen, ruft man typischerweise
nach dem
get()
noch
containsKey()
auf, um zu prüfen, ob der Schlüssel in der
Map
vorhanden ist.
Wenn wir nun statt einer
Map<K,V>
eine
Map<K, Optional<V>>
verwenden
und als assoziierten Wert anstelle von
null
ein leeres
Optional
verwenden, dann ist
der Returnwert des
get()
Aufrufs immer
eindeutig!
Das sind die wesentlichen Vorteile des Idioms
„
Optional
als bessere
null
“.
Damit stellt sich die Frage, soll man dieses Idiom nun in Java benutzen?
Soll man vielleicht sogar bestehenden Code ändern und an dieses Idiom
anpassen? Genau das ist umstritten.
Ein wichtiger Aspekt in der Diskussion ist
die Tatsache, dass Java-Programmierung nicht erst mit Java 8 anfängt und
auch mit Java 8 nicht wieder von vorne anfängt. Es gibt in Java eine
mehr als achtzehnjährige Programmiertradition, die auf der Verwendung
von
null
basiert, und alle Java-Bibliotheken
verwenden diesen Ansatz. Wenn man mit dem „
Optional
als bessere
null
“-Idiom eine dieser
existierenden Bibliotheken nutzen möchte, muss man Parameter immer von
der
Optional
-Welt des eigenen Programms
in die
null
-Welt
der Bibliothek konvertieren. Returnwerte muss man natürlich entsprechend
in die andere Richtung konvertieren. Diese Konvertierungen gehen übrigens
relativ einfach und werden direkt vom
Optional
API unterstützt. Die Factory-Methode
ofNullable(
T
t
)
konvertiert von der
null
-Welt
in die
Optional
-Welt und der Aufruf
orElse(null)
auf der entsprechenden
Optional
-Instanz
konvertiert in die andere Richtung. Die Factory-Methode
ofNullable()
hatte man extra noch - zu einem späteren Zeitpunkt - zum API hinzugefügt,
um die Konvertierung direkt zu unterstützen.
Trotzdem gab es bei den Diskussionen im OpenJDK
Lambda-Forum viel Skepsis gegenüber dem „
Optional
als bessere
null
“-Ansatz. Das kann
unter anderem daran sehen, dass
Optional
im JDK nur sehr sparsam verwendet wird. Schauen wir uns einige Argumente
und Begründungen etwas detaillier an.
Die Nutzung von
Optional
bringt einen Performancenachteil, da die Instanzen von
Optional
zusätzliche Wrapper um den eigentlichen Wert sind. Die zusätzlichen
Wrapper kosten Allokation und Garbage Collection. Wie groß der Performancenachteil
relativ zur Gesamtperformance ist, hängt natürlich immer von der jeweiligen
Situation ab. Man war sich einig, dass der Nachteil bei der Verwendung
von
Optional
als Returntyp von Stream-Operationen
tolerierbar ist: die Stream-Operation kostet im Allgemeinen signifikant
mehr als des Erzeugen und Wegräumen der zusätzlichen
Optional
-Instanz.
Ein weiteres Argument gegen „Optional als
bessere
null
“ war, dass man
Optional
gar nicht braucht, um in Java Typen zu verwenden, die einem sagen, ob man
null
benutzen darf oder nicht. Der auf dem JSR 308 basierende Checker Framework
macht genau dasselbe mit Hilfe der Typ-Annotationen
@Nullable
und
@NonNull
. Der Vorteil des Checker
Frameworks ist sogar, dass er keinerlei Performancenachteile mit sich bringt,
da die Prüfungen statisch und nicht zur Laufzeit erfolgen. Details zum
JSR 308 und zum Checker Framework finde man hier /CFRW/.
Ob man Optional im eigenen Code als Ersatz für null einsetzen will, muss jeder für sich selbst entscheiden. Der JDK verwendet Optional derzeit sehr sparsam. Wo und wie Optional letztlich in der Java Community insgesamt verwendetet werden wird, bleibt abzuwarten. Die Zukunft von Optional
Damit wäre eigentlich alles zu
Optional
gesagt, wenn es da nicht diesen leicht kryptischen Satz in der Javadoc
von
Optional
gäbe:
This is a value-based
class; use of identity-sensitive operations (including reference equality
(==), identity hash code, or synchronization) on instances of
Optional
may have unpredictable results and should be avoided
. Wie soll man
diesen Satz verstehen? Er ist in gewisser Weise ein Vorgriff auf das,
was mit
Optional
in Zukunft vermutlich
passieren wird.
Eine wahrscheinliche Ergänzung des Java-Typsystems
werden
Value Types
sein. (Details finden sich hier /VATY/).
Was soll das sein - ein Value Type? Die Idee ist treffend umschrieben mit dem Mantra: “Codes like a class, works like an int!” Das heißt: Von der Syntax her wird ein Value Type eher wie eine Klasse aussehen, von der Umsetzung in der JVM her soll er ähnlich zu einem primitiven Typ dargestellt und behandelt werden.
Zum Hintergrund muss man sich das Typsystem in Java ansehen. Heute gibt es im Java-Typsystem zwei Arten von Typen: Referenztypen und primitive Typen. Die Referenztypen verursachen erheblichen Overhead gegenüber den primitiven Typen, u.a.: - Objekte von einem Referenztyp werden auf dem Heap angelegt; das kostet Allokation und Garbage Collection. - Sie haben einen Objekt-Header, der u.a. auf Dinge wie das zugehörige Class -Objekt verweist; das kostet Speicherplatz. - Jeder Zugriff auf den Objektinhalt erfordert eine Pointer-Dereferenzierung; das kostet Performance. - Referenztypen unterstützen Operationen wie Object.wait() , sie können für die Synchronisation als Locks verwendet werden, und vieles mehr; das macht sie teuer, denn es verhindert Optimierungen, die der JIT-Compiler andernfalls machen könnte.
Primitive Typen kommen komplett ohne den
ganzen Overhead aus. Sie können auf dem Stack abgelegt und in Register
geladen werden und sind - wenn sie unveränderlich sind - ideale Kandidaten
fürs Caching auf Prozessorebene.
Die Value Types sollen ebenso effizient wie
die primitiven Typen sein. Einen Value Type kann man sich vorstellen wie
einen benutzer-definierten primitiven Typ, der zudem unveränderlich ist.
Außerdem hat ein Value keine Identity (d.h. keine Adresse), genauso wie
ein
int
keine Adresse hat. Ein Beispiel
für einen Value Type wäre ein
Point
-Typ
mit zwei
final
int
-Koordinaten.
Wenn
Point
ein Value Type wäre, würde
man ihn einfach als Aggregation von zwei
int
s
darstellen, die man auf dem Stack anlegen kann, in Register laden kann,
usw.
Dieses neue Sprachmittel wird es nach Aussage von Brian Goetz auf der JAX 2014 nicht vor Java 10 geben. Dann aber wäre Optional ein idealer Kandidat für einen Value Type, denn Optional ist klein und unveränderlich und braucht keine Identity. Wenn man aber Optional in Java 10 von einer Klasse in einen Value Type verwandelt, dann funktionieren gewisse Dinge plötzlich nicht mehr - nämlich die im oben zitierten Javadoc-Auszug genannten identity-sensitive operations . Der Vorteil wäre jedoch, dass es keinen Performance-Overhead mehr bei der Benutzung von Optional gäbe und damit ein gewichtiger Nachteil von Optional weg fiele. Genau deshalb steht der zitierte Satz vorsorglich heute schon in der Javadoc; er soll Benutzer davon abzuhalten, Optional wie einen Referenztypen zu verwenden, damit hinterher der Umstieg auf den Value Type problemlos klappt. (Die Diskussion, wie es zu dem Disclaimer gekommen ist, findet sich hier /ADOT/). Zusammenfassung
In diesem Artikel haben wir haben uns die
Klasse
Optional
aus dem JDK 8 genauer
angesehen. Dabei handelt es sich um einen Wrapper Typ, wie man ihn in
ähnlicher Form auch aus anderen (meist funktionalen) Sprachen kennt.
Deshalb gibt es neben dem imperativen
check-and-get-
API (
isPresent()
,
get()
)
ein umfangreiches
fluent
-API, das einen funktionalen Programmierstil
unterstützt. Wie diese neue Klasse in der Java Community verwendet werden
wird, kann man wohl erst sehen, wenn der JDK 8 eine größere Verbreitung
bekommen hat. Performancenachteile, die sich aus der Benutzung von
Optional
ergeben, könnten in Zukunft, nach der Einführung von Value Types, wegfallen.
Literaturverweise
Die gesamte Serie über Java 8:
|
|||||||||||||||||||||||||||||||||
© Copyright 1995-2018 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/80.Java8.Optional-Result/80.Java8.Optional-Result.html> last update: 26 Oct 2018 |