Kapitel 1. Die Grundlagen
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Die größte Veränderung in Java 8 ist die Aufnahme von Konzepten aus der funktionalen Programmierung in die Sprache. Insbesondere wurden Lambda-Ausdrücke, Methodenreferenzen und Streams in die Sprache aufgenommen.
Wenn du die neuen funktionalen Funktionen noch nicht genutzt hast, wirst du wahrscheinlich überrascht sein, wie anders dein Code im Vergleich zu früheren Java-Versionen aussehen wird. Die Änderungen in Java 8 sind die größten Änderungen an der Sprache überhaupt. In vielerlei Hinsicht fühlt es sich so an, als würdest du eine völlig neue Sprache lernen.
Dann stellt sich die Frage: Warum tun wir das? Warum so drastische Änderungen an einer Sprache vornehmen, die bereits zwanzig Jahre alt ist und die Abwärtskompatibilität beibehalten will? Warum so drastische Änderungen an einer Sprache, die nach allem, was man hört, äußerst erfolgreich war? Warum zu einem funktionalen Paradigma wechseln, nachdem sie all die Jahre eine der erfolgreichsten objektorientierten Sprachen aller Zeiten war?
Die Antwort ist, dass sich die Welt der Softwareentwicklung verändert hat und Sprachen, die in Zukunft erfolgreich sein wollen, sich ebenfalls anpassen müssen. Mitte der 90er Jahre, als Java glänzte und neu war, war das Mooresche Gesetz1 noch voll in Kraft. Du musstest nur ein paar Jahre warten und dein Computer wurde doppelt so schnell.
Die heutige Hardware verlässt sich nicht mehr auf die zunehmende Chipdichte, um schneller zu werden. Stattdessen haben sogar die meisten Handys mehrere Kerne, was bedeutet, dass Software so geschrieben werden muss, dass sie in einer Multiprozessorumgebung ausgeführt werden kann. Die funktionale Programmierung mit ihrem Schwerpunkt auf "reinen" Funktionen (die bei gleichen Eingaben das gleiche Ergebnis liefern, ohne Seiteneffekte) und Unveränderlichkeit vereinfacht die Programmierung in parallelen Umgebungen. Wenn du keinen gemeinsamen, veränderbaren Zustand hast und dein Programm in Sammlungen einfacher Funktionen zerlegt werden kann, ist es einfacher, sein Verhalten zu verstehen und vorherzusagen.
Dies ist jedoch kein Buch über Haskell, Erlang, Frege oder eine der anderen funktionalen Programmiersprachen. In diesem Buch geht es um Java und die Änderungen, die an der Sprache vorgenommen wurden, um funktionale Konzepte in eine im Grunde immer noch objektorientierte Sprache einzufügen.
Java unterstützt jetzt Lambda-Ausdrücke, die im Wesentlichen Methoden sind, die wie Objekte erster Klasse behandelt werden. Die Sprache verfügt auch über Methodenreferenzen, mit denen du eine vorhandene Methode überall dort verwenden kannst, wo ein Lambda-Ausdruck erwartet wird. Um die Vorteile von Lambda-Ausdrücken und Methodenreferenzen nutzen zu können, hat die Sprache auch ein Stream-Modell eingeführt, das Elemente erzeugt und sie durch eine Pipeline von Transformationen und Filtern leitet, ohne die ursprüngliche Quelle zu verändern.
Die Rezepte in diesem Kapitel beschreiben die grundlegende Syntax für Lambda-Ausdrücke, Methodenreferenzen und funktionale Schnittstellen sowie die neue Unterstützung für statische und Standardmethoden in Schnittstellen. Streams werden in Kapitel 3 ausführlich behandelt.
1.1 Lambda-Ausdrücke
Lösung
Verwende eine der Varianten der Lambda-Ausdruckssyntax und weise das Ergebnis einer Referenz vom Typ funktionale Schnittstelle zu.
Diskussion
Eine funktionale Schnittstelle ist eine Schnittstelle mit einer einzigen abstrakten Methode (SAM). Eine Klasse implementiert eine beliebige Schnittstelle, indem sie Implementierungen für alle Methoden der Schnittstelle bereitstellt. Dies kann mit einer Top-Level-Klasse, einer inneren Klasse oder sogar einer anonymen inneren Klasse geschehen.
Nehmen wir zum Beispiel die Schnittstelle Runnable
, die es in Java seit Version 1.0 gibt. Sie enthält eine einzige abstrakte Methode namens run
, die keine Argumente annimmt und void
zurückgibt. Der Konstruktor der Klasse Thread
nimmt ein Runnable
als Argument an. Die anonyme Implementierung der inneren Klasse wird in Beispiel 1-1 gezeigt.
Beispiel 1-1. Anonyme Implementierung einer inneren Klasse von Runnable
public
class
RunnableDemo
{
public
static
void
main
(
String
[
]
args
)
{
new
Thread
(
new
Runnable
(
)
{
@Override
public
void
run
(
)
{
System
.
out
.
println
(
"inside runnable using an anonymous inner class"
)
;
}
}
)
.
start
(
)
;
}
}
Die Syntax der anonymen inneren Klasse besteht aus dem Wort new
, gefolgt von dem Schnittstellennamen Runnable
und Klammern, was bedeutet, dass du eine Klasse ohne expliziten Namen definierst, die diese Schnittstelle implementiert. Der Code in den geschweiften Klammern ({}
) setzt dann die Methode run
außer Kraft, die einfach einen String auf der Konsole ausgibt.
Der Code in Beispiel 1-2 zeigt das gleiche Beispiel unter Verwendung eines Lambda-Ausdrucks.
Beispiel 1-2. Verwendung eines Lambda-Ausdrucks in einem Thread-Konstruktor
new
Thread
(()
->
System
.
out
.
println
(
"inside Thread constructor using lambda"
)).
start
();
Die Syntax verwendet einen Pfeil, um die Argumente (da es hier keine Argumente gibt, wird nur ein Paar leere Klammern verwendet) vom Textkörper zu trennen. In diesem Fall besteht der Körper aus einer einzigen Zeile, sodass keine Klammern erforderlich sind. Dies ist ein sogenanntes Ausdruckslambda. Der Wert, den der Ausdruck auswertet, wird automatisch zurückgegeben. Da println
in diesem Fall void
zurückgibt, ist die Rückgabe des Ausdrucks auch void
, was dem Rückgabetyp der Methode run
entspricht.
Ein Lambda-Ausdruck muss mit den Argumenttypen und dem Rückgabetyp in der Signatur der einzelnen abstrakten Methode in der Schnittstelle übereinstimmen. Dies wird als Kompatibilität mit der Signatur der Methode bezeichnet. Der Lambda-Ausdruck ist also die Implementierung der Schnittstellenmethode und kann auch einer Referenz dieses Schnittstellentyps zugewiesen werden.
Zur Veranschaulichung zeigt Beispiel 1-3 das Lambda, das einer Variablen zugewiesen ist.
Beispiel 1-3. Zuweisung eines Lambda-Ausdrucks an eine Variable
Runnable
r
=
()
->
System
.
out
.
println
(
"lambda expression implementing the run method"
);
new
Thread
(
r
).
start
();
Hinweis
In der Java-Bibliothek gibt es keine Klasse namens Lambda
. Lambda-Ausdrücke können nur funktionalen Schnittstellenreferenzen zugewiesen werden.
Ein Lambda der funktionalen Schnittstelle zuzuordnen ist dasselbe wie zu sagen, dass das Lambda die Implementierung der einzelnen abstrakten Methode innerhalb der Schnittstelle ist. Du kannst dir das Lambda als den Körper einer anonymen inneren Klasse vorstellen, die die Schnittstelle implementiert. Deshalb muss das Lambda mit der abstrakten Methode kompatibel sein; seine Argumenttypen und sein Rückgabetyp müssen mit der Signatur dieser Methode übereinstimmen. Der Name der Methode, die implementiert wird, ist jedoch nicht wichtig. Er wird in der Syntax des Lambda-Ausdrucks nirgends erwähnt.
Dieses Beispiel war besonders einfach, weil die Methode run
keine Argumente benötigt und void
zurückgibt. Betrachte stattdessen die funktionale Schnittstelle java.io.FilenameFilter
, die ebenfalls seit Version 1.0 Teil der Java-Standardbibliothek ist. Instanzen von FilenameFilter
werden als Argumente für die Methode File.list
verwendet, um die zurückgegebenen Dateien auf diejenigen zu beschränken, die der Methode genügen.
Aus den Javadocs geht hervor, dass die Klasse FilenameFilter
eine einzige abstrakte Methode accept
mit der folgenden Signatur enthält:
boolean
accept
(
File
dir
,
String
name
)
Das Argument File
ist das Verzeichnis, in dem die Datei gefunden wird, und String
ist der Name der Datei.
Der Code in Beispiel 1-4 implementiert FilenameFilter
mit einer anonymen inneren Klasse, um nur Java-Quelldateien zurückzugeben.
Beispiel 1-4. Eine anonyme innere Klassenimplementierung von FilenameFilter
File
directory
=
new
File
(
"./src/main/java"
)
;
String
[
]
names
=
directory
.
list
(
new
FilenameFilter
(
)
{
@Override
public
boolean
accept
(
File
dir
,
String
name
)
{
return
name
.
endsWith
(
".java"
)
;
}
}
)
;
System
.
out
.
println
(
Arrays
.
asList
(
names
)
)
;
In diesem Fall gibt die Methode accept
true zurück, wenn der Dateiname mit .java endet, und false, wenn nicht.
Die Version des Lambda-Ausdrucks wird in Beispiel 1-5 gezeigt.
Beispiel 1-5. Lambda-Ausdruck, der FilenameFilter implementiert
File
directory
=
new
File
(
"./src/main/java"
)
;
String
[
]
names
=
directory
.
list
(
(
dir
,
name
)
-
>
name
.
endsWith
(
".java"
)
)
;
System
.
out
.
println
(
Arrays
.
asList
(
names
)
)
;
}
Der resultierende Code ist viel einfacher. Diesmal stehen die Argumente in Klammern, aber es sind keine Typen deklariert. Zur Kompilierzeit weiß der Compiler, dass die Methode list
ein Argument des Typs FilenameFilter
annimmt, und kennt daher die Signatur ihrer einzigen abstrakten Methode (accept
). Er weiß daher, dass die Argumente von accept
vom Typ File
und String
sind, so dass die kompatiblen Argumente des Lambda-Ausdrucks diesen Typen entsprechen müssen. Der Rückgabetyp von accept
ist ein boolescher Wert, also muss der Ausdruck rechts vom Pfeil ebenfalls einen booleschen Wert zurückgeben.
Wenn du die Datentypen im Code angeben möchtest, kannst du das tun, wie in Beispiel 1-6.
Beispiel 1-6. Lambda-Ausdruck mit expliziten Datentypen
File
directory
=
new
File
(
"./src/main/java"
)
;
String
[
]
names
=
directory
.
list
(
(
File
dir
,
String
name
)
-
>
name
.
endsWith
(
".java"
)
)
;
Wenn die Implementierung des Lambdas mehr als eine Zeile benötigt, musst du geschweifte Klammern und eine explizite Rückgabeanweisung verwenden, wie in Beispiel 1-7 gezeigt.
Beispiel 1-7. Ein Block-Lambda
File
directory
=
new
File
(
"./src/main/java"
)
;
String
[
]
names
=
directory
.
list
(
(
File
dir
,
String
name
)
-
>
{
return
name
.
endsWith
(
".java"
)
;
}
)
;
System
.
out
.
println
(
Arrays
.
asList
(
names
)
)
;
Dies ist ein sogenanntes Block-Lambda. In diesem Fall besteht der Textkörper immer noch aus einer einzigen Zeile, aber die geschweiften Klammern ermöglichen jetzt mehrere Anweisungen. Das Schlüsselwort return
ist jetzt erforderlich.
Lambda-Ausdrücke existieren nie allein. Es gibt immer einen Kontext für den Ausdruck, der die funktionale Schnittstelle angibt, der der Ausdruck zugeordnet ist. Ein Lambda kann ein Argument für eine Methode sein, ein Rückgabetyp einer Methode oder einer Referenz zugewiesen werden. In jedem Fall muss der Typ der Zuweisung eine funktionale Schnittstelle sein.
1.2 Referenzen zur Methode
Lösung
- Verwende die Doppelpunktschreibweise, um eine Instanzreferenz oder einen Klassennamen von der Methode zu trennen.((("
-
(Doppelpunkt) Notation in Methodenreferenzen")))
Diskussion
Wenn ein Lambda-Ausdruck eine Methode so behandelt, als wäre sie ein Objekt, dann behandelt eine Methodenreferenz eine bestehende Methode so, als wäre sie ein Lambda.
Zum Beispiel nimmt die Methode forEach
in Iterable
ein Consumer
als Argument. Beispiel 1-8 zeigt, dass die Methode Consumer
entweder als Lambda-Ausdruck oder als Methodenreferenz implementiert werden kann.
Beispiel 1-8. Verwendung einer Methodenreferenz für den Zugriff auf println
Stream
.
of
(
3
,
1
,
4
,
1
,
5
,
9
)
.
forEach
(
x
-
>
System
.
out
.
println
(
x
)
)
;
Stream
.
of
(
3
,
1
,
4
,
1
,
5
,
9
)
.
forEach
(
System
.
out
:
:
println
)
;
Consumer
<
Integer
>
printer
=
System
.
out
:
:
println
;
Stream
.
of
(
3
,
1
,
4
,
1
,
5
,
9
)
.
forEach
(
printer
)
;
Die Doppelpunktschreibweise gibt den Verweis auf die Methode println
der Instanz System.out
an, die eine Referenz vom Typ PrintStream
ist. Am Ende der Methodenreferenz werden keine Klammern gesetzt. In dem gezeigten Beispiel wird jedes Element des Streams auf der Standardausgabe ausgegeben.2
Tipp
Wenn du einen Lambda-Ausdruck schreibst, der aus einer Zeile besteht, die eine Methode aufruft, solltest du stattdessen die entsprechende Methodenreferenz verwenden.
Die Methodenreferenz bietet einige (kleine) Vorteile gegenüber der Lambda-Syntax. Erstens ist sie in der Regel kürzer, und zweitens enthält sie oft den Namen der Klasse, die die Methode enthält. Beides macht den Code einfacher zu lesen.
Methodenreferenzen können auch mit statischen Methoden verwendet werden, wie in Beispiel 1-9 gezeigt.
Beispiel 1-9. Verwendung einer Methodenreferenz für eine statische Methode
Stream
.
generate
(
Math:
:
random
)
.
limit
(
10
)
.
forEach
(
System
.
out
:
:
println
)
;
Die Methode generate
auf Stream
nimmt Supplier
als Argument an, eine funktionale Schnittstelle, deren einzige abstrakte Methode keine Argumente annimmt und ein einziges Ergebnis liefert. Die Methode random
in der Klasse Math
ist mit dieser Signatur kompatibel, da sie ebenfalls keine Argumente entgegennimmt und einen einzelnen, gleichmäßig verteilten Pseudozufallswert zwischen 0 und 1 erzeugt. Die Methodenreferenz Math::random
verweist auf diese Methode als Implementierung der Schnittstelle Supplier
.
Da Stream.generate
einen unendlichen Strom erzeugt, wird die Methode limit
verwendet, um sicherzustellen, dass nur 10 Werte erzeugt werden, die dann mit der Methodenreferenz System.out::println
als Implementierung von Consumer
auf die Standardausgabe ausgegeben werden.
Syntax
Es gibt drei Formen der Methodenreferenz Syntax, von denen eine etwas irreführend ist:
object::instanceMethod
-
Verweise auf eine Instanzmethode mit einer Referenz auf das übergebene Objekt, wie in
System.out::println
Class::staticMethod
-
Verweis auf statische Methode, wie in
Math::max
Class::instanceMethod
-
Rufe die Instanzmethode mit einem Verweis auf ein vom Kontext geliefertes Objekt auf, wie in
String::length
Das letzte Beispiel ist das verwirrende, denn als Java-Entwickler sind wir es gewohnt, dass nur statische Methoden über einen Klassennamen aufgerufen werden. Erinnere dich daran, dass Lambda-Ausdrücke und Methodenreferenzen nie in einem Vakuum existieren - es gibt immer einen Kontext. Im Falle einer Objektreferenz liefert der Kontext die Argumente für die Methode. Im Fall des Druckens lautet der entsprechende Lambda-Ausdruck (wie im Kontext in Beispiel 1-8 gezeigt):
// equivalent to System.out::println
x
->
System
.
out
.
println
(
x
)
Der Kontext liefert den Wert von x
, der als Argument für die Methode verwendet wird.
Ähnlich verhält es sich mit der statischen Methode max
:
// equivalent to Math::max
(
x
,
y
)
->
Math
.
max
(
x
,
y
)
Jetzt muss der Kontext zwei Argumente liefern, und der Lambda gibt das größere zurück.
Die Syntax "Instanzmethode durch den Klassennamen" wird anders interpretiert. Das entsprechende Lambda ist:
// equivalent to String::length
x
->
x
.
length
()
Wenn der Kontext x
bereitstellt, wird es diesmal als Ziel der Methode verwendet und nicht als Argument.
Tipp
Wenn du dich über den Klassennamen auf eine Methode beziehst, die mehrere Argumente entgegennimmt, wird das erste vom Kontext gelieferte Element zum Ziel und die übrigen Elemente sind Argumente für die Methode.
Beispiel 1-10 zeigt den Beispielcode.
Beispiel 1-10. Aufrufen einer Instanzmethode mit mehreren Argumenten aus einer Klassenreferenz
List
<
String
>
strings
=
Arrays
.
asList
(
"this"
,
"is"
,
"a"
,
"list"
,
"of"
,
"strings"
)
;
List
<
String
>
sorted
=
strings
.
stream
(
)
.
sorted
(
(
s1
,
s2
)
-
>
s1
.
compareTo
(
s2
)
)
.
collect
(
Collectors
.
toList
(
)
)
;
List
<
String
>
sorted
=
strings
.
stream
(
)
.
sorted
(
String:
:
compareTo
)
.
collect
(
Collectors
.
toList
(
)
)
;
Die Methode sorted
auf Stream
nimmt ein Comparator<T>
als Argument, dessen einzige abstrakte Methode int compare(String other)
ist. Die Methode sorted
übergibt jedes String-Paar an den Komparator und sortiert es nach dem Vorzeichen der zurückgegebenen Ganzzahl. In diesem Fall ist der Kontext ein Paar von Strings. Die Methodenreferenzsyntax mit dem Klassennamen String
ruft die Methode compareTo
für das erste Element auf (s1
im Lambda-Ausdruck) und verwendet das zweite Element s2
als Argument für die Methode.
Bei der Stream-Verarbeitung greifst du häufig über den Klassennamen in einer Methodenreferenz auf eine Instanzmethode zu, wenn du eine Reihe von Eingaben verarbeitest. Der Code in Beispiel 1-11 zeigt den Aufruf der Methode length
für jede einzelne String
im Stream.
Beispiel 1-11. Aufrufen der length-Methode für String mit einer Methodenreferenz
Stream
.
of
(
"this"
,
"is"
,
"a"
,
"stream"
,
"of"
,
"strings"
)
.
map
(
String:
:
length
)
.
forEach
(
System
.
out
:
:
println
)
;
Dieses Beispiel wandelt jede Zeichenkette in eine ganze Zahl um, indem es die Methode length
aufruft, und gibt dann jedes Ergebnis aus.
Eine Methodenreferenz ist im Wesentlichen eine verkürzte Syntax für ein Lambda. Lambda-Ausdrücke sind allgemeiner, da jede Methodenreferenz einen entsprechenden Lambda-Ausdruck hat, aber nicht umgekehrt. Die entsprechenden Lambdas für die Methodenreferenzen aus Beispiel 1-11 sind in Beispiel 1-12 dargestellt.
Beispiel 1-12. Lambda-Ausdruck-Äquivalente für Methodenreferenzen
Stream
.
of
(
"this"
,
"is"
,
"a"
,
"stream"
,
"of"
,
"strings"
)
.
map
(
s
->
s
.
length
())
.
forEach
(
x
->
System
.
out
.
println
(
x
));
Wie bei jedem Lambda-Ausdruck ist der Kontext wichtig. Du kannst auch this
oder super
als linke Seite einer Methodenreferenz verwenden, wenn es Unklarheiten gibt.
Siehe auch
Du kannst Konstruktoren auch mit der Syntax der Methodenreferenz aufrufen. Konstruktorreferenzen werden in Rezept 1.3 gezeigt. Das Paket der funktionalen Schnittstellen, einschließlich der in diesem Rezept behandelten Schnittstelle Supplier
, wird in Kapitel 2 behandelt.
1.3 Konstruktor-Referenzen
Diskussion
Wenn die Leute über die neue Syntax von Java 8 sprechen, erwähnen sie Lambda-Ausdrücke, Methodenreferenzen und Streams. Nehmen wir an, du hast eine Liste von Personen und möchtest sie in eine Liste von Namen umwandeln. Eine Möglichkeit wäre das in Beispiel 1-13 gezeigte Snippet.
Beispiel 1-13. Eine Liste von Personen in eine Liste von Namen umwandeln
List
<
String
>
names
=
people
.
stream
(
)
.
map
(
person
-
>
person
.
getName
(
)
)
.
collect
(
Collectors
.
toList
(
)
)
;
// or, alternatively,
List
<
String
>
names
=
people
.
stream
(
)
.
map
(
Person:
:
getName
)
.
collect
(
Collectors
.
toList
(
)
)
;
Was ist, wenn du den umgekehrten Weg gehen willst? Was ist, wenn du eine Liste von Strings hast und daraus eine Liste von Person
Referenzen erstellen willst? In diesem Fall kannst du eine Methodenreferenz verwenden, aber dieses Mal mit dem Schlüsselwort new
. Diese Syntax wird Konstruktorreferenz genannt.
Um zu zeigen, wie es verwendet wird, fangen wir mit der Klasse Person
an, die so ziemlich das einfachste Plain Old Java Object (POJO) ist, das man sich vorstellen kann. Sie umhüllt lediglich ein einfaches String-Attribut, das in Beispiel 1-14 name
heißt.
Beispiel 1-14. Eine Klasse Person
public
class
Person
{
private
String
name
;
public
Person
()
{}
public
Person
(
String
name
)
{
this
.
name
=
name
;
}
// getters and setters ...
// equals, hashCode, and toString methods ...
}
Wenn du eine Sammlung von Strings hast, kannst du jeden einzelnen in eine Person
abbilden, indem du entweder einen Lambda-Ausdruck oder den Konstruktorverweis in Beispiel 1-15 verwendest.
Beispiel 1-15. Strings in Person-Instanzen umwandeln
List
<
String
>
names
=
Arrays
.
asList
(
"Grace Hopper"
,
"Barbara Liskov"
,
"Ada Lovelace"
,
"Karen Spärck Jones"
)
;
List
<
Person
>
people
=
names
.
stream
(
)
.
map
(
name
-
>
new
Person
(
name
)
)
.
collect
(
Collectors
.
toList
(
)
)
;
// or, alternatively,
List
<
Person
>
people
=
names
.
stream
(
)
.
map
(
Person:
:
new
)
.
collect
(
Collectors
.
toList
(
)
)
;
Die Syntax Person::new
bezieht sich auf den Konstruktor in der Klasse Person
. Wie bei allen Lambda-Ausdrücken bestimmt der Kontext, welcher Konstruktor ausgeführt wird. Da der Kontext einen String liefert, wird der Ein-Arg-Konstruktor String
verwendet.
Konstruktor kopieren
Ein Kopierkonstruktor nimmt ein Person
Argument und gibt ein neues Person
mit den gleichen Attributen zurück, wie in Beispiel 1-16 gezeigt.
Beispiel 1-16. Ein Kopierkonstruktor für Person
public
Person
(
Person
p
)
{
this
.
name
=
p
.
name
;
}
Das ist nützlich, wenn du den Streaming-Code von den ursprünglichen Instanzen isolieren willst. Wenn du zum Beispiel bereits eine Liste von Personen hast, die Liste in einen Stream umwandelst und dann wieder in eine Liste, sind die Referenzen dieselben (siehe Beispiel 1-17).
Beispiel 1-17. Konvertierung einer Liste in einen Stream und zurück
Person
before
=
new
Person
(
"Grace Hopper"
)
;
List
<
Person
>
people
=
Stream
.
of
(
before
)
.
collect
(
Collectors
.
toList
(
)
)
;
Person
after
=
people
.
get
(
0
)
;
assertTrue
(
before
=
=
after
)
;
before
.
setName
(
"Grace Murray Hopper"
)
;
assertEquals
(
"Grace Murray Hopper"
,
after
.
getName
(
)
)
;
Mit einem Kopierkonstruktor kannst du diese Verbindung unterbrechen, wie in Beispiel 1-18.
Beispiel 1-18. Den Kopierkonstruktor verwenden
people
=
Stream
.
of
(
before
)
.
map
(
Person:
:
new
)
.
collect
(
Collectors
.
toList
(
)
)
;
after
=
people
.
get
(
0
)
;
assertFalse
(
before
=
=
after
)
;
assertEquals
(
before
,
after
)
;
before
.
setName
(
"Rear Admiral Dr. Grace Murray Hopper"
)
;
assertFalse
(
before
.
equals
(
after
)
)
;
Dieses Mal ist der Kontext beim Aufruf der Methode map
ein Strom von Person
Instanzen. Deshalb ruft die Person::new
Syntax den Konstruktor auf, der eine Person
Instanz nimmt und eine neue, aber gleichwertige Instanz zurückgibt, und hat die Verbindung zwischen der Vorher-Referenz und der Nachher-Referenz unterbrochen.3
Varargs-Konstruktor
Betrachte nun einen varargs-Konstruktor , der der Person
POJO hinzugefügt wurde, wie in Beispiel 1-19 gezeigt.
Beispiel 1-19. Ein Person-Konstruktor, der eine variable Argumentliste von String annimmt
public
Person
(
String
...
names
)
{
this
.
name
=
Arrays
.
stream
(
names
)
.
collect
(
Collectors
.
joining
(
" "
));
}
Dieser Konstruktor nimmt null oder mehr String-Argumente und verkettet sie mit einem einzelnen Leerzeichen als Trennzeichen.
Wie kann dieser Konstruktor aufgerufen werden? Jeder Client, der null oder mehr durch Kommas getrennte String-Argumente übergibt, ruft ihn auf. Eine Möglichkeit, das zu tun, ist, die Methode split
auf String
zu nutzen, die ein Trennzeichen annimmt und ein Array String
zurückgibt:
String
[]
split
(
String
delimiter
)
Deshalb zerlegt der Code in Beispiel 1-20 jeden String in der Liste in einzelne Wörter und ruft den varargs-Konstruktor auf.
Beispiel 1-20. Verwendung des varargs-Konstruktors
names
.
stream
(
)
.
map
(
name
-
>
name
.
split
(
" "
)
)
.
map
(
Person:
:
new
)
.
collect
(
Collectors
.
toList
(
)
)
;
Dieses Mal ist der Kontext für die Methode map
, die die Person::new
Konstruktorreferenz enthält, ein Stream von String-Arrays, also wird der varargs-Konstruktor aufgerufen. Wenn du eine einfache Druckanweisung zu diesem Konstruktor hinzufügst:
System
.
out
.
println
(
"Varargs ctor, names="
+
Arrays
.
asList
(
names
));
dann ist das Ergebnis:
Varargs ctor, names=[Grace, Hopper] Varargs ctor, names=[Barbara, Liskov] Varargs ctor, names=[Ada, Lovelace] Varargs ctor, names=[Karen, Spärck, Jones]
Arrays
Konstruktorreferenzen können auch mit Arrays verwendet werden. Wenn du ein Array von Person
Instanzen, Person[]
, anstelle einer Liste haben möchtest, kannst du die Methode toArray
auf Stream
verwenden, deren Signatur lautet:
<
A
>
A
[]
toArray
(
IntFunction
<
A
[]>
generator
)
Diese Methode verwendet A
, um den generischen Typ des zurückgegebenen Arrays mit den Elementen des Streams zu repräsentieren, der mit der bereitgestellten Generatorfunktion erstellt wird. Das Tolle daran ist, dass dafür auch eine Konstruktorreferenz verwendet werden kann, wie in Beispiel 1-21.
Beispiel 1-21. Ein Array von Personenreferenzen erstellen
Person
[
]
people
=
names
.
stream
(
)
.
map
(
Person:
:
new
)
.
toArray
(
Person
[
]
:
:
new
)
;
Das Argument der Methode toArray
erstellt ein Array von Person
Referenzen in der richtigen Größe und füllt es mit den instanziierten Person
Instanzen.
Konstruktorreferenzen sind nur Methodenreferenzen unter einem anderen Namen, die das Wort new
verwenden, um einen Konstruktor aufzurufen. Welcher Konstruktor das ist, wird wie üblich durch den Kontext bestimmt. Diese Technik bietet eine große Flexibilität bei der Verarbeitung von Streams.
Siehe auch
Methodenreferenzen werden in Rezept 1.2 besprochen.
1.4 Funktionale Schnittstellen
Diskussion
Eine funktionale Schnittstelle in Java 8 ist eine Schnittstelle mit einer einzelnen, abstrakten Methode. Als solche kann sie das Ziel für einen Lambda-Ausdruck oder eine Methodenreferenz sein.
Die Verwendung des Begriffs abstract
ist hier von Bedeutung. Vor Java 8 galten alle Methoden in Schnittstellen standardmäßig als abstrakt - du musstest das Schlüsselwort nicht einmal hinzufügen.
Hier ist zum Beispiel die Definition einer Schnittstelle namens PalindromeChecker
, die in Beispiel 1-22 gezeigt wird.
Beispiel 1-22. Ein Palindrom Checker Interface
@FunctionalInterface
public
interface
PalindromeChecker
{
boolean
isPalidrome
(
String
s
);
}
Alle Methoden in einer Schnittstelle sind public
,4 Du kannst also den Zugriffsmodifikator weglassen, genauso wie du das Schlüsselwort abstract
weglassen kannst.
Da diese Schnittstelle nur eine einzige, abstrakte Methode hat, ist sie eine funktionale Schnittstelle. Java 8 bietet eine Annotation namens @FunctionalInterface
im Paket java.lang
, die auf die Schnittstelle angewendet werden kann, wie im Beispiel gezeigt.
Diese Anmerkung ist nicht erforderlich, aber aus zwei Gründen eine gute Idee. Erstens wird zur Kompilierzeit überprüft, ob die Schnittstelle tatsächlich die Anforderung erfüllt. Wenn die Schnittstelle entweder keine oder mehr als eine abstrakte Methode hat, bekommst du einen Compilerfehler.
Ein weiterer Vorteil der @FunctionalInterface
Annotation ist, dass sie eine Aussage in den Javadocs wie folgt erzeugt:
Functional Interface: This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference.
Funktionale Schnittstellen können auch default
und static
Methoden haben. Sowohl Standard- als auch statische Methoden haben Implementierungen, sodass sie nicht gegen die Anforderung einer einzigen abstrakten Methode verstoßen. Beispiel 1-23 zeigt den Beispielcode.
Beispiel 1-23. MyInterface ist eine funktionale Schnittstelle mit statischen und Standardmethoden
@FunctionalInterface
public
interface
MyInterface
{
int
myMethod
(
)
;
// int myOtherMethod();
default
String
sayHello
(
)
{
return
"Hello, World!"
;
}
static
void
myStaticMethod
(
)
{
System
.
out
.
println
(
"I'm a static method in an interface"
)
;
}
}
Wenn die kommentierte Methode myOtherMethod
enthalten wäre, würde die Schnittstelle nicht mehr die Anforderungen an eine funktionale Schnittstelle erfüllen. Die Annotation würde einen Fehler der Form "multiple non-overriding abstract methods found" erzeugen.
Schnittstellen können andere Schnittstellen erweitern, sogar mehr als eine. Die Annotation prüft die aktuelle Schnittstelle. Wenn also eine Schnittstelle eine bestehende funktionale Schnittstelle erweitert und eine weitere abstrakte Methode hinzufügt, ist sie selbst keine funktionale Schnittstelle. Siehe Beispiel 1-24.
Beispiel 1-24. Erweiterung einer funktionalen Schnittstelle - nicht mehr funktional
public
interface
MyChildInterface
extends
MyInterface
{
int
anotherMethod
(
)
;
}
Die MyChildInterface
ist keine funktionale Schnittstelle, weil sie zwei abstrakte Methoden hat: myMethod
, die sie von MyInterface
erbt; und anotherMethod
, die sie deklariert. Ohne die @FunctionalInterface
Annotation lässt sie sich kompilieren, weil sie eine Standardschnittstelle ist. Sie kann jedoch nicht das Ziel eines Lambda-Ausdrucks sein.
Ein Kanten-Fall sollte ebenfalls beachtet werden. Die Schnittstelle Comparator
wird für die Sortierung verwendet, die in anderen Rezepten behandelt wird. Wenn du dir die Javadocs für diese Schnittstelle ansiehst und die Registerkarte Abstrakte Methoden auswählst, siehst du die in Abbildung 1-1 dargestellten Methoden.
Moment, was? Wie kann dies eine funktionale Schnittstelle sein, wenn es zwei abstrakte Methoden gibt, insbesondere wenn eine davon tatsächlich in java.lang.Object
implementiert ist?
Die Besonderheit hier ist, dass die gezeigte Methode equals
von Object
stammt und daher bereits eine Standardimplementierung hat. In der ausführlichen Dokumentation heißt es, dass du aus Leistungsgründen deine eigene equals
Methode bereitstellen kannst, die denselben Vertrag erfüllt, dass es aber "immer sicher ist, diese Methode nicht zu überschreiben" (Hervorhebung im Original).
Die Regeln für funktionale Schnittstellen besagen, dass die Methoden von Object
nicht auf die Grenze von einer abstrakten Methode angerechnet werden. Comparator
ist also immer noch eine funktionale Schnittstelle.
Siehe auch
Standardmethoden in Schnittstellen werden in Rezept 1.5 und statische Methoden in Schnittstellen in Rezept 1.6 behandelt.
1.5 Standardmethoden in Schnittstellen
Lösung
Verwende das Schlüsselwort default
für die Schnittstellenmethode und füge die Implementierung auf normale Weise hinzu.
Diskussion
Der traditionelle Grund, warum Java nie Mehrfachvererbung unterstützt hat, ist das so genannte Rautenproblem. Angenommen, du hast eine Vererbungshierarchie, wie sie in der (vage an die UML angelehnten) Abbildung 1-2 dargestellt ist.
Die Klasse Animal
hat zwei Unterklassen, Bird
und Horse
, die jeweils die Methode speak
von Animal
überschreiben, um in Horse
"wiehern" und in Bird
"zwitschern" zu sagen. Was sagt dann Pegasus
(das mehrfach von Horse
und Bird
erbt)?5 sagen? Was ist, wenn du eine Referenz vom Typ Animal
einer Instanz von Pegasus
zugewiesen hast? Was sollte die Methode speak
dann zurückgeben?
Animal
animal
=
new
Pegaus
();
animal
.
speak
();
// whinny, chirp, or other?
Verschiedene Sprachen gehen unterschiedlich mit diesem Problem um. In C++ zum Beispiel ist Mehrfachvererbung erlaubt, aber wenn eine Klasse widersprüchliche Implementierungen erbt, wird sie nicht kompiliert.6 In Eiffel,7 lässt der Compiler dir die Wahl, welche Implementierung du möchtest.
Javas Ansatz war es, Mehrfachvererbung zu verbieten, und Schnittstellen wurden als Abhilfe eingeführt, wenn eine Klasse eine "ist eine Art von"-Beziehung zu mehr als einem Typ hat. Da Schnittstellen nur abstrakte Methoden haben, gibt es keine Implementierungen, die in Konflikt geraten könnten. Bei Schnittstellen ist Mehrfachvererbung erlaubt, aber auch das funktioniert, weil nur die Methodensignaturen vererbt werden.
Das Problem ist, dass du nie eine Methode in einer Schnittstelle implementieren kannst, was zu umständlichen Designs führt. Zu den Methoden der Schnittstelle java.util.Collection
gehören zum Beispiel:
boolean
isEmpty
()
int
size
()
Die Methode isEmpty
gibt true zurück, wenn keine Elemente in der Sammlung vorhanden sind, und false, wenn nicht. Die Methode size
gibt die Anzahl der Elemente in den Sammlungen zurück. Unabhängig von der zugrundeliegenden Implementierung kannst du die Methode isEmpty
sofort in Form von size
implementieren, wie in Beispiel 1-25.
Beispiel 1-25. Implementierung von isEmpty in Bezug auf die Größe
public
boolean
isEmpty
()
{
return
size
()
==
0
;
}
Da Collection
eine Schnittstelle ist, kannst du dies nicht in der Schnittstelle selbst tun. Stattdessen enthält die Standardbibliothek eine abstrakte Klasse namens java.util.AbstractCollection
, die neben anderem Code genau die hier gezeigte Implementierung von isEmpty
enthält. Wenn du deine eigene Sammelimplementierung erstellst und noch keine Oberklasse hast, kannst du AbstractCollection
erweitern und erhältst die Methode isEmpty
kostenlos. Wenn du bereits eine Oberklasse hast, musst du stattdessen die Schnittstelle Collection
implementieren und dich daran erinnern, deine eigene Implementierung von isEmpty
sowie size
bereitzustellen.
Erfahrene Java-Entwickler sind mit all dem vertraut, aber mit Java 8 ändert sich die Situation. Jetzt kannst du Implementierungen zu Schnittstellenmethoden hinzufügen. Alles, was du tun musst, ist, das Schlüsselwort default
zu einer Methode hinzuzufügen und eine Implementierung anzugeben. Der Code in Beispiel 1-26 zeigt eine Schnittstelle mit abstrakten und Standardmethoden.
Beispiel 1-26. Eine Mitarbeiterschnittstelle mit einer Standardmethode
public
interface
Employee
{
String
getFirst
(
)
;
String
getLast
(
)
;
void
convertCaffeineToCodeForMoney
(
)
;
default
String
getName
(
)
{
return
String
.
format
(
"%s %s"
,
getFirst
(
)
,
getLast
(
)
)
;
}
}
Die Methode getName
hat das Schlüsselwort default
und ihre Implementierung erfolgt durch die anderen, abstrakten Methoden der Schnittstelle, getFirst
und getLast
.
Viele der bestehenden Schnittstellen in Java wurden um Standardmethoden erweitert, um die Abwärtskompatibilität zu gewährleisten. Wenn du eine neue Methode zu einer Schnittstelle hinzufügst, machst du normalerweise alle bestehenden Implementierungen kaputt. Wenn man eine neue Methode als Standardmethode hinzufügt, erben alle bestehenden Implementierungen die neue Methode und funktionieren weiterhin. Auf diese Weise konnten die Betreuer der Bibliothek neue Standardmethoden im gesamten JDK hinzufügen, ohne bestehende Implementierungen zu zerstören.
Zum Beispiel enthält java.util.Collection
jetzt die folgenden Standardmethoden:
default
boolean
removeIf
(
Predicate
<?
super
E
>
filter
)
default
Stream
<
E
>
stream
()
default
Stream
<
E
>
parallelStream
()
default
Spliterator
<
E
>
spliterator
()
Die Methode removeIf
entfernt alle Elemente aus der Sammlung, die dem Predicate
8 Argument erfüllen, und gibt true
zurück, wenn irgendwelche Elemente entfernt wurden. Die Methoden stream
und parallelStream
sind Factory-Methoden zur Erstellung von Streams. Die Methode spliterator
gibt ein Objekt einer Klasse zurück, die die Schnittstelle Spliterator
implementiert, also ein Objekt zum Durchlaufen und Aufteilen von Elementen aus einer Quelle.
Standardmethoden werden genauso wie alle anderen Methoden verwendet, wie Beispiel 1-27 zeigt.
Beispiel 1-27. Standardmethoden verwenden
List
<
Integer
>
nums
=
new
ArrayList
<
>
(
)
;
nums
.
add
(
-
3
)
;
nums
.
add
(
1
)
;
nums
.
add
(
4
)
;
nums
.
add
(
-
1
)
;
nums
.
add
(
5
)
;
nums
.
add
(
9
)
;
boolean
removed
=
nums
.
removeIf
(
n
-
>
n
<
=
0
)
;
System
.
out
.
println
(
"Elements were "
+
(
removed
?
""
:
"NOT"
)
+
" removed"
)
;
nums
.
forEach
(
System
.
out
:
:
println
)
;
Was passiert, wenn eine Klasse zwei Schnittstellen mit der gleichen Standardmethode implementiert? Das ist das Thema von Rezept 5.5, aber die kurze Antwort lautet: Wenn die Klasse die Methode selbst implementiert, ist alles in Ordnung. Siehe Rezept 5.5 für weitere Details.
Siehe auch
Rezept 5.5 zeigt die Regeln, die gelten, wenn eine Klasse mehrere Schnittstellen mit Standardmethoden implementiert.
1.6 Statische Methoden in Interfaces
Lösung
Mache die Methode static
und stelle die Implementierung auf die übliche Weise zur Verfügung.
Diskussion
Statische Mitglieder von Java-Klassen sind auf Klassenebene angesiedelt, d.h. sie sind mit der Klasse als Ganzes und nicht mit einer bestimmten Instanz verbunden. Das macht ihre Verwendung in Schnittstellen vom Standpunkt des Designs aus gesehen problematisch. Einige Fragen dazu sind:
-
Was bedeutet ein Member auf Klassenebene, wenn die Schnittstelle von vielen verschiedenen Klassen implementiert wird?
-
Muss eine Klasse eine Schnittstelle implementieren, um eine statische Methode verwenden zu können?
-
Statische Methoden in Klassen werden über den Klassennamen aufgerufen. Wenn eine Klasse eine Schnittstelle implementiert, wird eine statische Methode dann über den Klassennamen oder den Schnittstellennamen aufgerufen?
Die Designer von Java hätten diese Fragen auf verschiedene Arten entscheiden können. Vor Java 8 war die Entscheidung, statische Mitglieder in Schnittstellen überhaupt nicht zuzulassen.
Leider führte das aber zur Schaffung von Utility-Klassen: Klassen, die nur statische Methoden enthalten. Ein typisches Beispiel ist java.util.Collections
, das Methoden zum Sortieren und Suchen, zum Einpacken von Sammlungen in synchronisierte oder unveränderbare Typen und mehr enthält. Im NIO-Paket ist java.nio.file.Paths
ein weiteres Beispiel. Es enthält nur statische Methoden, die Path
Instanzen aus Strings oder URIs parsen.
In Java 8 kannst du jetzt statische Methoden zu Schnittstellen hinzufügen, wann immer du willst. Die Voraussetzungen sind:
-
Füge das Schlüsselwort
static
zu der Methode hinzu. -
eine Implementierung bereitstellen (die nicht überschrieben werden kann). Auf diese Weise sind sie wie die Methoden von
default
und werden in der Standard-Registerkarte in den Javadocs aufgeführt. -
Rufe die Methode über den Namen der Schnittstelle auf. Klassen müssen eine Schnittstelle nicht implementieren, um ihre statischen Methoden zu verwenden.
Ein Beispiel für eine praktische statische Methode in einer Schnittstelle ist die Methode comparing
in java.util.Comparator
, zusammen mit ihren primitiven Varianten comparingInt
, comparingLong
und comparingDouble
. Die Schnittstelle Comparator
hat auch statische Methoden naturalOrder
und reverseOrder
. Beispiel 1-28 zeigt, wie sie verwendet werden.
Beispiel 1-28. Zeichenketten sortieren
List
<
String
>
bonds
=
Arrays
.
asList
(
"Connery"
,
"Lazenby"
,
"Moore"
,
"Dalton"
,
"Brosnan"
,
"Craig"
)
;
List
<
String
>
sorted
=
bonds
.
stream
(
)
.
sorted
(
Comparator
.
naturalOrder
(
)
)
.
collect
(
Collectors
.
toList
(
)
)
;
// [Brosnan, Connery, Craig, Dalton, Lazenby, Moore]
sorted
=
bonds
.
stream
(
)
.
sorted
(
Comparator
.
reverseOrder
(
)
)
.
collect
(
Collectors
.
toList
(
)
)
;
// [Moore, Lazenby, Dalton, Craig, Connery, Brosnan]
sorted
=
bonds
.
stream
(
)
.
sorted
(
Comparator
.
comparing
(
String:
:
toLowerCase
)
)
.
collect
(
Collectors
.
toList
(
)
)
;
// [Brosnan, Connery, Craig, Dalton, Lazenby, Moore]
sorted
=
bonds
.
stream
(
)
.
sorted
(
Comparator
.
comparingInt
(
String:
:
length
)
)
.
collect
(
Collectors
.
toList
(
)
)
;
// [Moore, Craig, Dalton, Connery, Lazenby, Brosnan]
sorted
=
bonds
.
stream
(
)
.
sorted
(
Comparator
.
comparingInt
(
String:
:
length
)
.
thenComparing
(
Comparator
.
naturalOrder
(
)
)
)
.
collect
(
Collectors
.
toList
(
)
)
;
// [Craig, Moore, Dalton, Brosnan, Connery, Lazenby]
Das Beispiel zeigt, wie man mehrere statische Methoden in Comparator
verwendet, um die Liste der Schauspieler zu sortieren, die im Laufe der Jahre James Bond gespielt haben.9 Vergleicher werden in Rezept 4.1 näher erläutert.
Statische Methoden in Schnittstellen machen es überflüssig, separate Hilfsklassen zu erstellen, obwohl diese Option immer noch zur Verfügung steht, wenn ein Entwurf dies erfordert.
Die wichtigsten Punkte, an die du dich erinnern solltest, sind:
-
Statische Methoden müssen eine Implementierung haben
-
Du kannst eine statische Methode nicht außer Kraft setzen
-
Statische Methoden über den Schnittstellennamen aufrufen
-
Du brauchst eine Schnittstelle nicht zu implementieren, um ihre statischen Methoden zu nutzen
Siehe auch
Statische Methoden von Schnittstellen werden in diesem Buch durchgängig verwendet, aber Rezept 4.1 behandelt die hier verwendeten statischen Methoden von Comparator
.
1 Der Begriff wurde von Gordon Moore, einem der Mitbegründer von Fairchild Semiconductor und Intel, geprägt und basiert auf der Beobachtung, dass sich die Anzahl der Transistoren, die in einem integrierten Schaltkreis untergebracht werden können, etwa alle 18 Monate verdoppelt. Weitere Informationen findest du im Wikipedia-Eintrag zu Moores Gesetz.
2 Es ist schwierig, Lambdas oder Methodenreferenzen zu besprechen, ohne auf Streams einzugehen, denen später ein eigenes Kapitel gewidmet ist. Es genügt zu sagen, dass ein Stream eine Reihe von Elementen nacheinander erzeugt, sie nirgendwo speichert und die ursprüngliche Quelle nicht verändert.
3 Ich will nicht respektlos sein, wenn ich Admiral Hopper wie ein Objekt behandle. Ich bezweifle nicht, dass sie mir immer noch in den Hintern treten könnte, und sie ist 1992 verstorben.
4 Zumindest bis Java 9, wo private
Methoden auch in Interfaces erlaubt sind. Siehe Rezept 10.2 für Details.
5 "Ein prächtiges Pferd mit dem Gehirn eines Vogels." (Disneys Herkules-Film, der Spaß macht, wenn du so tust, als wüsstest du nichts über die griechische Mythologie und hättest noch nie von Herkules gehört).
6 Das kann durch virtuelle Vererbung gelöst werden, aber trotzdem.
7 Hier ist eine obskure Referenz für dich, aber Eiffel war eine der grundlegenden Sprachen der objektorientierten Programmierung. Siehe Bertrand Meyer's Object-Oriented Software Construction, Second Edition (Prentice Hall, 1997).
8 Predicate
ist eine der neuen funktionalen Schnittstellen im Paket java.util.function
, die in Rezept 2.3 ausführlich beschrieben werden.
9 Die Versuchung, Idris Elba auf die Liste zu setzen, ist fast überwältigend, aber das ist bisher nicht gelungen.
Get Moderne Java-Rezepte now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.