Kapitel 4. Komparatoren und Kollektoren
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Java 8 erweitert die Schnittstelle Comparator
um mehrere statische und Standardmethoden, die Sortiervorgänge viel einfacher machen. Es ist jetzt möglich, eine Sammlung von POJOs nach einer Eigenschaft zu sortieren, dann gleich nach einer zweiten, dann nach einer dritten und so weiter, einfach mit einer Reihe von Bibliotheksaufrufen.
Java 8 fügt außerdem eine neue Dienstleistungsklasse namens java.util.stream.Collectors
hinzu, die statische Methoden zur Konvertierung von Streams zurück in verschiedene Arten von Sammlungen bietet. Die Collectors können auch "nachgelagert" eingesetzt werden, das heißt, sie können eine Gruppierungs- oder Partitionierungsoperation nachbearbeiten.
Die Rezepte in diesem Kapitel veranschaulichen all diese Konzepte.
4.1 Sortieren mit Hilfe eines Komparators
Diskussion
Die Methode sorted
auf Stream
erzeugt einen neuen, sortierten Stream unter Verwendung der natürlichen Ordnung der Klasse. Die natürliche Ordnung wird durch die Implementierung der Schnittstelle java.util.Comparable
festgelegt.
Betrachte zum Beispiel die Sortierung einer Sammlung von Strings, wie in Beispiel 4-1 gezeigt.
Beispiel 4-1. Strings lexikografisch sortieren
private
List
<
String
>
sampleStrings
=
Arrays
.
asList
(
"this"
,
"is"
,
"a"
,
"list"
,
"of"
,
"strings"
)
;
public
List
<
String
>
defaultSort
(
)
{
Collections
.
sort
(
sampleStrings
)
;
return
sampleStrings
;
}
public
List
<
String
>
defaultSortUsingStreams
(
)
{
return
sampleStrings
.
stream
(
)
.
sorted
(
)
.
collect
(
Collectors
.
toList
(
)
)
;
}
Java hat eine Hilfsklasse namens Collections
, seit das Collections-Framework in Version 1.2 hinzugefügt wurde. Die statische Methode sort
auf Collections
nimmt ein List
als Argument, gibt aber void
zurück. Die Sortierung ist destruktiv und verändert die übergebene Sammlung. Dieser Ansatz entspricht nicht den von Java 8 unterstützten funktionalen Prinzipien, die die Unveränderlichkeit betonen.
Java 8 verwendet die Methode sorted
für Streams, um die gleiche Sortierung vorzunehmen, erzeugt aber einen neuen Stream, anstatt die ursprüngliche Sammlung zu verändern. In diesem Beispiel wird die zurückgegebene Liste nach dem Sortieren der Sammlung nach der natürlichen Reihenfolge der Klasse sortiert. Bei Zeichenketten ist die natürliche Ordnung lexikografisch, was sich auf alphabetisch reduziert, wenn alle Zeichenketten klein geschrieben sind, wie in diesem Beispiel.
Wenn du die Strings anders sortieren willst, gibt es eine überladene sorted
Methode, die Comparator
als Argument nimmt.
Beispiel 4-2 zeigt eine Längensortierung für Strings auf zwei verschiedene Arten.
Beispiel 4-2. Strings nach Länge sortieren
public
List
<
String
>
lengthSortUsingSorted
(
)
{
return
sampleStrings
.
stream
(
)
.
sorted
(
(
s1
,
s2
)
-
>
s1
.
length
(
)
-
s2
.
length
(
)
)
.
collect
(
toList
(
)
)
;
}
public
List
<
String
>
lengthSortUsingComparator
(
)
{
return
sampleStrings
.
stream
(
)
.
sorted
(
Comparator
.
comparingInt
(
String:
:
length
)
)
.
collect
(
toList
(
)
)
;
}
Das Argument der Methode sorted
ist eine java.util.Comparator
, die eine funktionale Schnittstelle ist. In lengthSortUsingSorted
wird ein Lambda-Ausdruck bereitgestellt, um die Methode compare
in Comparator
zu implementieren. In Java 7 und früher wurde die Implementierung normalerweise von einer anonymen inneren Klasse bereitgestellt, aber hier ist nur ein Lambda-Ausdruck erforderlich.
Hinweis
In Java 8 wurde sort(Comparator)
als default
Instanzmethode auf List
hinzugefügt, die der static void sort(List, Comparator)
Methode auf Collections
entspricht. Beides sind destruktive Sortierungen, die void
zurückgeben, daher ist der hier besprochene sorted(Comparator)
Ansatz für Streams (der einen neuen, sortierten Stream zurückgibt) immer noch vorzuziehen.
Die zweite Methode, lengthSortUsingComparator
, nutzt eine der statischen Methoden, die der Schnittstelle Comparator
hinzugefügt wurden. Die Methode comparingInt
nimmt ein Argument vom Typ ToIntFunction
entgegen, das die Zeichenkette in einen int umwandelt, der in der Dokumentation keyExtractor
genannt wird, und erzeugt einen Comparator
, der die Sammlung nach diesem Schlüssel sortiert.
Die zusätzlichen Standardmethoden in Comparator
sind äußerst nützlich. Du kannst zwar eine Comparator
schreiben, die nach Länge sortiert, aber wenn du nach mehr als einem Feld sortieren willst, kann das kompliziert werden. Du kannst die Zeichenketten nach der Länge sortieren und dann die Zeichenketten mit gleicher Länge alphabetisch. Mit den Standard- und statischen Methoden in Comparator
wird das fast trivial, wie in Beispiel 4-3 gezeigt.
Beispiel 4-3. Sortieren nach Länge, dann gleiche Längen lexikografisch
public
List
<
String
>
lengthSortThenAlphaSort
(
)
{
return
sampleStrings
.
stream
(
)
.
sorted
(
comparing
(
String:
:
length
)
.
thenComparing
(
naturalOrder
(
)
)
)
.
collect
(
toList
(
)
)
;
}
Comparator
bietet eine default
Methode namens thenComparing
. Genau wie comparing
nimmt auch sie ein Function
als Argument, das wiederum als keyExtractor
bezeichnet wird. Die Verkettung mit der Methode comparing
ergibt ein Comparator
, das die erste Menge mit der zweiten vergleicht und so weiter.
Statische Importe machen den Code oft einfacher zu lesen. Wenn du dich erst einmal an die statischen Methoden in Comparator
und Collectors
gewöhnt hast, ist dies eine einfache Möglichkeit, den Code zu vereinfachen. In diesem Fall wurden die Methoden comparing
und naturalOrder
statisch importiert.
Dieser Ansatz funktioniert bei jeder Klasse, auch wenn sie nicht Comparable
implementiert. Betrachte die Klasse Golfer
in Beispiel 4-4.
Beispiel 4-4. Eine Klasse für Golfer
public
class
Golfer
{
private
String
first
;
private
String
last
;
private
int
score
;
// ... other methods ...
}
Um eine Rangliste für ein Turnier zu erstellen, ist es sinnvoll, erst nach Punkten, dann nach Nachnamen und schließlich nach Vornamen zu sortieren. Beispiel 4-5 zeigt, wie man das macht.
Beispiel 4-5. Golfer sortieren
private
List
<
Golfer
>
golfers
=
Arrays
.
asList
(
new
Golfer
(
"Jack"
,
"Nicklaus"
,
68
),
new
Golfer
(
"Tiger"
,
"Woods"
,
70
),
new
Golfer
(
"Tom"
,
"Watson"
,
70
),
new
Golfer
(
"Ty"
,
"Webb"
,
68
),
new
Golfer
(
"Bubba"
,
"Watson"
,
70
)
);
public
List
<
Golfer
>
sortByScoreThenLastThenFirst
()
{
return
golfers
.
stream
()
.
sorted
(
comparingInt
(
Golfer:
:
getScore
)
.
thenComparing
(
Golfer:
:
getLast
)
.
thenComparing
(
Golfer:
:
getFirst
))
.
collect
(
toList
());
}
Die Ausgabe des Aufrufs von sortByScoreThenLastThenFirst
ist in Beispiel 4-6 zu sehen.
Beispiel 4-6. Sortierte Golfer
Golfer{first='Jack', last='Nicklaus', score=68} Golfer{first='Ty', last='Webb', score=68} Golfer{first='Bubba', last='Watson', score=70} Golfer{first='Tom', last='Watson', score=70} Golfer{first='Tiger', last='Woods', score=70}
Die Golfer werden nach Punkten sortiert, so dass Nicklaus und Webb vor Woods und den beiden Watsons stehen.1 Dann werden gleiche Punktzahlen nach Nachnamen sortiert, so dass Nicklaus vor Webb und Watson vor Woods liegt. Schließlich werden gleiche Punktzahlen und Nachnamen nach dem Vornamen sortiert, so dass Bubba Watson vor Tom Watson steht.
Die Standard- und statischen Methoden in Comparator
sowie die neue Methode sorted
auf Stream
machen die Erstellung komplexer Sortierungen einfach.
4.2 Einen Stream in eine Sammlung umwandeln
Lösung
Verwende die Methoden toList
, toSet
oder toCollection
in der Hilfsklasse Collectors
.
Diskussion
In Java 8 werden die Elemente eines Streams oft durch eine Pipeline von Zwischenoperationen geleitet, die mit einer abschließenden Operation endet. Eine abschließende Operation ist die Methode collect
, mit der eine Stream
in eine Sammlung umgewandelt wird.
Die Methode collect
in Stream
hat zwei überladene Versionen, wie in Beispiel 4-7 gezeigt.
Beispiel 4-7. Die collect-Methode in Stream<T>
<
R
,
A
>
R
collect
(
Collector
<?
super
T
,
A
,
R
>
collector
)
<
R
>
R
collect
(
Supplier
<
R
>
supplier
,
BiConsumer
<
R
,?
super
T
>
accumulator
,
BiConsumer
<
R
,
R
>
combiner
)
Dieses Rezept befasst sich mit der ersten Version, die ein Collector
als Argument annimmt. Collectors führen eine "veränderbare Reduktionsoperation" durch, bei der Elemente in einem Ergebniscontainer gesammelt werden. Hier wird das Ergebnis eine Sammlung sein.
Collector
ist eine Schnittstelle und kann daher nicht instanziiert werden. Die Schnittstelle enthält eine statische of
Methode, um sie zu erzeugen, aber es gibt oft einen besseren oder zumindest einfacheren Weg.
Hier werden die statischen Methoden der Klasse Collectors
verwendet, um Collector
Instanzen zu erzeugen, die als Argument für Stream.collect
verwendet werden, um eine Sammlung aufzufüllen.
Ein einfaches Beispiel, das eine List
erstellt, wird in Beispiel 4-8 gezeigt.2
Beispiel 4-8. Eine Liste erstellen
List
<
String
>
superHeroes
=
Stream
.
of
(
"Mr. Furious"
,
"The Blue Raja"
,
"The Shoveler"
,
"The Bowler"
,
"Invisible Boy"
,
"The Spleen"
,
"The Sphinx"
)
.
collect
(
Collectors
.
toList
());
Diese Methode erstellt und füllt eine ArrayList
mit den angegebenen Stream-Elementen. Das Erstellen einer Set
ist genauso einfach, wie in Beispiel 4-9.
Beispiel 4-9. Ein Set erstellen
Set
<
String
>
villains
=
Stream
.
of
(
"Casanova Frankenstein"
,
"The Disco Boys"
,
"The Not-So-Goodie Mob"
,
"The Suits"
,
"The Suzies"
,
"The Furriers"
,
"The Furriers"
)
.
collect
(
Collectors
.
toSet
(
)
)
;
}
Diese Methode erstellt eine Instanz von HashSet
und füllt sie aus, wobei alle Duplikate entfernt werden.
In beiden Beispielen wurden die Standard-Datenstrukturen verwendet:ArrayList
für List
und HashSet
für Set
. Wenn du eine bestimmte Datenstruktur angeben möchtest, solltest du die Methode Collectors.toCollection
verwenden, die ein Supplier
als Argument benötigt. Beispiel 4-10 zeigt den Beispielcode.
Beispiel 4-10. Erstellen einer verknüpften Liste
List
<
String
>
actors
=
Stream
.
of
(
"Hank Azaria"
,
"Janeane Garofalo"
,
"William H. Macy"
,
"Paul Reubens"
,
"Ben Stiller"
,
"Kel Mitchell"
,
"Wes Studi"
)
.
collect
(
Collectors
.
toCollection
(
LinkedList:
:
new
));
}
Das Argument der Methode toCollection
ist eine Sammlung Supplier
, daher wird hier der Konstruktorverweis auf LinkedList
angegeben. Die Methode collect
instanziiert eine LinkedList
und füllt sie dann mit den angegebenen Namen auf.
Die Klasse Collectors
enthält auch eine Methode zur Erstellung eines Arrays von Objekten. Es gibt zwei Überladungen der Methode toArray
:
Object
[]
toArray
();
<
A
>
A
[]
toArray
(
IntFunction
<
A
[]>
generator
);
Erstere gibt ein Array zurück, das die Elemente dieses Streams enthält, ohne jedoch den Typ anzugeben. Letztere nimmt eine Funktion, die ein neues Array des gewünschten Typs erzeugt, dessen Länge der Größe des Streams entspricht, und ist am einfachsten mit einer Array-Konstruktor-Referenz zu verwenden, wie in Beispiel 4-11 gezeigt.
Beispiel 4-11. Ein Array erstellen
String
[
]
wannabes
=
Stream
.
of
(
"The Waffler"
,
"Reverse Psychologist"
,
"PMS Avenger"
)
.
toArray
(
String
[
]
:
:
new
)
;
}
Das zurückgegebene Array hat den angegebenen Typ, dessen Länge der Anzahl der Elemente im Stream entspricht.
Für die Umwandlung in eine Map
benötigt die Methode Collectors.toMap
zwei Function
Instanzen - eine für die Schlüssel und eine für die Werte.
Betrachte eine Actor
POJO, die eine name
und eine role
umhüllt. Wenn du eine Set
von Actor
Instanzen aus einem bestimmten Film hast, erstellt der Code in Beispiel 4-12 eine Map
aus ihnen.
Beispiel 4-12. Eine Karte erstellen
Set
<
Actor
>
actors
=
mysteryMen
.
getActors
(
)
;
Map
<
String
,
String
>
actorMap
=
actors
.
stream
(
)
.
collect
(
Collectors
.
toMap
(
Actor:
:
getName
,
Actor:
:
getRole
)
)
;
actorMap
.
forEach
(
(
key
,
value
)
-
>
System
.
out
.
printf
(
"%s played %s%n"
,
key
,
value
)
)
;
Die Ausgabe ist
Janeane Garofalo played The Bowler Greg Kinnear played Captain Amazing William H. Macy played The Shoveler Paul Reubens played The Spleen Wes Studi played The Sphinx Kel Mitchell played Invisible Boy Geoffrey Rush played Casanova Frankenstein Ben Stiller played Mr. Furious Hank Azaria played The Blue Raja
Ein ähnlicher Code funktioniert für ConcurrentMap
mit der Methode toConcurrentMap
.
Siehe auch
Supplier
s werden in Rezept 2.2 behandelt. Konstruktorreferenzen findest du in Rezept 1.3. Die Methode toMap
wird auch in Rezept 4.3 vorgestellt.
4.3 Hinzufügen einer linearen Sammlung zu einer Karte
Diskussion
Dies ist ein kurzer, sehr fokussierter Anwendungsfall, aber wenn er in der Praxis auftritt, kann die Lösung hier sehr praktisch sein.
Angenommen, du hast eine List
von Book
Instanzen, wobei Book
ein einfaches POJO ist, das eine ID, einen Namen und einen Preis hat. Eine verkürzte Form der Klasse Book
ist in Beispiel 4-13 zu sehen.
Beispiel 4-13. Ein einfaches POJO, das ein Buch darstellt
public
class
Book
{
private
int
id
;
private
String
name
;
private
double
price
;
// ... other methods ...
}
Nehmen wir nun an, du hast eine Sammlung von Book
Instanzen, wie in Beispiel 4-14 gezeigt.
Beispiel 4-14. Eine Sammlung von Büchern
List
<
Book
>
books
=
Arrays
.
asList
(
new
Book
(
1
,
"Modern Java Recipes"
,
49.99
),
new
Book
(
2
,
"Java 8 in Action"
,
49.99
),
new
Book
(
3
,
"Java SE8 for the Really Impatient"
,
39.99
),
new
Book
(
4
,
"Functional Programming in Java"
,
27.64
),
new
Book
(
5
,
"Making Java Groovy"
,
45.99
)
new
Book
(
6
,
"Gradle Recipes for Android"
,
23.76
)
);
In vielen Situationen möchtest du statt einer List
eine Map
verwenden, bei der die Schlüssel die Buch-IDs und die Werte die Bücher selbst sind. Das ist ganz einfach mit der Methode toMap
in Collectors
zu erreichen, wie in Beispiel 4-15 auf zwei verschiedene Arten gezeigt wird.
Beispiel 4-15. Hinzufügen der Bücher zu einer Karte
Map
<
Integer
,
Book
>
bookMap
=
books
.
stream
(
)
.
collect
(
Collectors
.
toMap
(
Book:
:
getId
,
b
-
>
b
)
)
;
bookMap
=
books
.
stream
(
)
.
collect
(
Collectors
.
toMap
(
Book:
:
getId
,
Function
.
identity
(
)
)
)
;
Die Methode toMap
in Collectors
nimmt zwei Function
Instanzen als Argumente, von denen die erste einen Schlüssel und die zweite den Wert aus dem angegebenen Objekt generiert. In diesem Fall wird der Schlüssel von der Methode getId
in Book
zugeordnet, und der Wert ist das Buch selbst.
Das erste toMap
in Beispiel 4-15 verwendet die Methode getId
, um auf den Schlüssel zu mappen und einen expliziten Lambda-Ausdruck, der einfach seinen Parameter zurückgibt. Das zweite Beispiel verwendet die statische Methode identity
in Function
, um das Gleiche zu tun.
Siehe auch
Funktionen werden in Rezept 2.4 behandelt, in dem auch unäre und binäre Operatoren besprochen werden.
4.4 Karten sortieren
Diskussion
Die Schnittstelle Map
enthält seit jeher eine öffentliche, statische, innere Schnittstelle namens Map.Entry
, die ein Schlüssel-Wert-Paar darstellt. Die Methode Map.entrySet
gibt ein Set
von Map.Entry
Elementen zurück. Vor Java 8 wurden in dieser Schnittstelle hauptsächlich die Methoden getKey
und getValue
verwendet, die genau das tun, was du erwartest.
In Java 8 wurden die statischen Methoden in Tabelle 4-1 hinzugefügt.
Methode | Beschreibung |
---|---|
|
Gibt einen Komparator zurück, der |
|
Gibt einen Komparator zurück, der |
|
Gibt einen Komparator zurück, der |
|
Gibt einen Komparator zurück, der |
Um zu zeigen, wie man sie benutzt, erzeugt Beispiel 4-18 eine Map
der Wortlängen zur Anzahl der Wörter in einem Wörterbuch. Jedes Unix-System enthält eine Datei im Verzeichnis usr/share/dict/words, die den Inhalt des Wörterbuchs Webster's 2nd Edition enthält, mit einem Wort pro Zeile. Die Methode Files.lines
kann verwendet werden, um eine Datei zu lesen und einen Strom von Zeichenketten zu erzeugen, der diese Zeilen enthält. In diesem Fall enthält der Stream jedes Wort aus dem Wörterbuch.
Beispiel 4-18. Einlesen der Wörterbuchdatei in eine Map
System
.
out
.
println
(
"\nNumber of words of each length:"
);
try
(
Stream
<
String
>
lines
=
Files
.
lines
(
dictionary
))
{
lines
.
filter
(
s
->
s
.
length
()
>
20
)
.
collect
(
Collectors
.
groupingBy
(
String:
:
length
,
Collectors
.
counting
()))
.
forEach
((
len
,
num
)
->
System
.
out
.
printf
(
"%d: %d%n"
,
len
,
num
));
}
catch
(
IOException
e
)
{
e
.
printStackTrace
();
}
Dieses Beispiel wird in Rezept 7.1 besprochen, aber um es zusammenzufassen:
-
Die Datei wird innerhalb eines
try-with-resources
Blocks gelesen.Stream
implementiertAutoCloseable
, so dass Java, wenn der Try-Block beendet wird, die Methodeclose
aufStream
aufruft, die wiederum die Methodeclose
aufFile
aufruft. -
Der Filter schränkt die weitere Verarbeitung auf Wörter mit einer Länge von mindestens 20 Zeichen ein.
-
Die Methode
groupingBy
vonCollectors
nimmt als erstes Argument einFunction
, das den Klassifikator darstellt. In diesem Fall ist der Klassifikator die Länge der einzelnen Strings. Wenn du nur ein Argument angibst, ist das Ergebnis eineMap
, bei der die Schlüssel die Werte des Klassifizierers und die Werte die Listen der Elemente sind, die dem Klassifizierer entsprechen. In dem Fall, den wir gerade untersuchen, hättegroupingBy(String::length)
eineMap<Integer,List<String>>
erzeugt, bei der die Schlüssel die Wortlängen und die Werte Listen von Wörtern dieser Länge sind. -
In diesem Fall kannst du mit der Zwei-Argument-Version von
groupingBy
einen weiterenCollector
, einen sogenannten Downstream Collector, angeben, der die Wortlisten nachbearbeitet. In diesem Fall ist der RückgabetypMap<Integer,Long>
, wobei die Schlüssel die Wortlängen und die Werte die Anzahl der Wörter dieser Länge im Wörterbuch sind.
Das Ergebnis ist:
Number of words of each length: 21: 82 22: 41 23: 17 24: 5
Mit anderen Worten: Es gibt 82 Wörter der Länge 21, 41 Wörter der Länge 22, 17 Wörter der Länge 23 und 5 Wörter der Länge 24.3
Die Ergebnisse zeigen, dass die Karte in aufsteigender Reihenfolge der Wortlänge gedruckt wird. Um sie in absteigender Reihenfolge zu sehen, verwendet Map.Entry.comparingByKey
wie in Beispiel 4-19.
Beispiel 4-19. Sortieren der Karte nach Schlüssel
System
.
out
.
println
(
"\nNumber of words of each length (desc order):"
);
try
(
Stream
<
String
>
lines
=
Files
.
lines
(
dictionary
))
{
Map
<
Integer
,
Long
>
map
=
lines
.
filter
(
s
->
s
.
length
()
>
20
)
.
collect
(
Collectors
.
groupingBy
(
String:
:
length
,
Collectors
.
counting
()));
map
.
entrySet
().
stream
()
.
sorted
(
Map
.
Entry
.
comparingByKey
(
Comparator
.
reverseOrder
()))
.
forEach
(
e
->
System
.
out
.
printf
(
"Length %d: %2d words%n"
,
e
.
getKey
(),
e
.
getValue
()));
}
catch
(
IOException
e
)
{
e
.
printStackTrace
();
}
Nach der Berechnung der Map<Integer,Long>
extrahiert diese Operation die entrySet
und erzeugt einen Stream. Die Methode sorted
auf Stream
wird verwendet, um einen sortierten Stream mit Hilfe des mitgelieferten Komparators zu erzeugen.
In diesem Fall erzeugt Map.Entry.comparingByKey
einen Komparator, der nach den Schlüsseln sortiert, und mit der Überladung, die einen Komparator annimmt, kann der Code angeben, dass wir ihn in umgekehrter Reihenfolge haben wollen.
Hinweis
Die Methode sorted
auf Stream
erzeugt einen neuen, sortierten Stream, der die Quelle nicht verändert. Der ursprüngliche Map
bleibt davon unberührt.
Das Ergebnis ist:
Number of words of each length (desc order): Length 24: 5 words Length 23: 17 words Length 22: 41 words Length 21: 82 words
Die anderen in Tabelle 4-1 aufgeführten Sortiermethoden werden ähnlich verwendet.
Siehe auch
Ein weiteres Beispiel für das Sortieren einer Map
nach Schlüsseln oder Werten findest du in Anhang A. Nachgeschaltete Collectors werden in Rezept 4.6 behandelt. Dateioperationen mit dem Wörterbuch sind Teil von Rezept 7.1.
4.5 Partitionierung und Gruppierung
Diskussion
Angenommen, du hast eine Sammlung von Zeichenketten. Wenn du sie in solche mit gerader Länge und solche mit ungerader Länge aufteilen willst, kannst du Collectors.partitioningBy
verwenden, wie in Beispiel 4-20.
Beispiel 4-20. Strings nach gerader oder ungerader Länge unterteilen
List
<
String
>
strings
=
Arrays
.
asList
(
"this"
,
"is"
,
"a"
,
"long"
,
"list"
,
"of"
,
"strings"
,
"to"
,
"use"
,
"as"
,
"a"
,
"demo"
)
;
Map
<
Boolean
,
List
<
String
>
>
lengthMap
=
strings
.
stream
(
)
.
collect
(
Collectors
.
partitioningBy
(
s
-
>
s
.
length
(
)
%
2
=
=
0
)
)
;
lengthMap
.
forEach
(
(
key
,
value
)
-
>
System
.
out
.
printf
(
"%5s: %s%n"
,
key
,
value
)
)
;
//
// false: [a, strings, use, a]
// true: [this, is, long, list, of, to, as, demo]
Die Signatur der beiden partitioningBy
Methoden sind:
static
<
T
>
Collector
<
T
,?,
Map
<
Boolean
,
List
<
T
>>>
partitioningBy
(
Predicate
<?
super
T
>
predicate
)
static
<
T
,
D
,
A
>
Collector
<
T
,?,
Map
<
Boolean
,
D
>>
partitioningBy
(
Predicate
<?
super
T
>
predicate
,
Collector
<?
super
T
,
A
,
D
>
downstream
)
Die Rückgabetypen sehen aufgrund der Generika ziemlich unschön aus, aber in der Praxis musst du dich nur selten mit ihnen befassen. Stattdessen wird das Ergebnis einer der beiden Operationen zum Argument für die Methode collect
, die den generischen Collector verwendet, um die durch das dritte generische Argument definierte Output-Map zu erstellen.
Die erste Methode partitioningBy
nimmt ein einzelnes Predicate
als Argument. Sie unterteilt die Elemente in solche, die die Predicate
erfüllen, und solche, die sie nicht erfüllen. Als Ergebnis erhältst du immer eine Map
, die genau zwei Einträge hat: eine Liste von Werten, die die Predicate
erfüllen, und eine Liste von Werten, die das nicht tun.
Die überladene Version der Methode nimmt ein zweites Argument vom Typ Collector
entgegen, das als Downstream Collector bezeichnet wird. Damit kannst du die von der Partition zurückgegebenen Listen nachbearbeiten, was in Rezept 4.6 beschrieben wird.
Die Methode groupingBy
führt eine Operation durch, die einer "group by"-Anweisung in SQL entspricht. Sie gibt eine Map
zurück, wobei die Schlüssel die Gruppen und die Werte die Listen der Elemente in jeder Gruppe sind.
Hinweis
Wenn du deine Daten aus einer Datenbank abrufst, kannst du dort auf jeden Fall alle Gruppierungsoperationen durchführen. Die neuen API-Methoden sind Komfortmethoden für Daten im Speicher.
Die Signatur für die Methode groupingBy
lautet:
static
<
T
,
K
>
Collector
<
T
,?,
Map
<
K
,
List
<
T
>>>
groupingBy
(
Function
<?
super
T
,?
extends
K
>
classifier
)
Das Argument Function
nimmt jedes Element des Streams und extrahiert eine Eigenschaft, nach der gruppiert werden soll. Anstatt die Strings einfach in zwei Kategorien aufzuteilen, solltest du sie dieses Mal nach ihrer Länge trennen, wie in Beispiel 4-21.
Beispiel 4-21. Strings nach Länge gruppieren
List
<
String
>
strings
=
Arrays
.
asList
(
"this"
,
"is"
,
"a"
,
"long"
,
"list"
,
"of"
,
"strings"
,
"to"
,
"use"
,
"as"
,
"a"
,
"demo"
)
;
Map
<
Integer
,
List
<
String
>
>
lengthMap
=
strings
.
stream
(
)
.
collect
(
Collectors
.
groupingBy
(
String:
:
length
)
)
;
lengthMap
.
forEach
(
(
k
,
v
)
-
>
System
.
out
.
printf
(
"%d: %s%n"
,
k
,
v
)
)
;
//
// 1: [a, a]
// 2: [is, of, to, as]
// 3: [use]
// 4: [this, long, list, demo]
// 7: [strings]
Die Schlüssel in der resultierenden Map sind die Längen der Strings (1, 2, 3, 4 und 7) und die Werte sind Listen von Strings jeder Länge.
Siehe auch
Rezept 4.6 ist eine Erweiterung des Rezepts, das wir uns gerade angesehen haben, und zeigt, wie man die Listen, die von einer groupingBy
oder partitioningBy
Operation zurückgegeben werden, nachbearbeitet.
4.6 Nachgeschaltete Kollektoren
Diskussion
In Rezept 4.5 haben wir uns angesehen, wie man Elemente in mehrere Kategorien aufteilt. Die Methoden partitioningBy
und groupingBy
geben eine Map
zurück, deren Schlüssel die Kategorien sind (Boolesche true
und false
für partitioningBy
, Objekte für groupingBy
) und deren Werte Listen von Elementen sind, die jeder Kategorie entsprechen. Erinnere dich an das Beispiel zur Partitionierung von Zeichenketten nach gerader und ungerader Länge in Beispiel 4-20, das der Einfachheit halber in Beispiel 4-22 wiederholt wird.
Beispiel 4-22. Strings nach gerader oder ungerader Länge unterteilen
List
<
String
>
strings
=
Arrays
.
asList
(
"this"
,
"is"
,
"a"
,
"long"
,
"list"
,
"of"
,
"strings"
,
"to"
,
"use"
,
"as"
,
"a"
,
"demo"
);
Map
<
Boolean
,
List
<
String
>>
lengthMap
=
strings
.
stream
()
.
collect
(
Collectors
.
partitioningBy
(
s
->
s
.
length
()
%
2
==
0
));
lengthMap
.
forEach
((
key
,
value
)
->
System
.
out
.
printf
(
"%5s: %s%n"
,
key
,
value
));
//
// false: [a, strings, use, a]
// true: [this, is, long, list, of, to, as, demo]
Anstelle der eigentlichen Listen interessiert dich vielleicht eher, wie viele Elemente in jede Kategorie fallen. Mit anderen Worten: Anstatt eine Map
zu erzeugen, deren Werte List<String>
sind, möchtest du vielleicht nur die Anzahl der Elemente in jeder der Listen wissen. Die Methode partitioningBy
hat eine überladene Version, deren zweites Argument vom Typ Collector
ist:
static
<
T
,
D
,
A
>
Collector
<
T
,?,
Map
<
Boolean
,
D
>>
partitioningBy
(
Predicate
<?
super
T
>
predicate
,
Collector
<?
super
T
,
A
,
D
>
downstream
)
An dieser Stelle wird die statische Methode Collectors.counting
nützlich. Beispiel 4-23 zeigt, wie sie funktioniert.
Beispiel 4-23. Zählen der partitionierten Strings
Map
<
Boolean
,
Long
>
numberLengthMap
=
strings
.
stream
(
)
.
collect
(
Collectors
.
partitioningBy
(
s
-
>
s
.
length
(
)
%
2
=
=
0
,
Collectors
.
counting
(
)
)
)
;
numberLengthMap
.
forEach
(
(
k
,
v
)
-
>
System
.
out
.
printf
(
"%5s: %d%n"
,
k
,
v
)
)
;
//
// false: 4
// true: 8
Dies wird als nachgelagerter Collector bezeichnet, weil er die resultierenden Listen nach der Partitionierung nachbearbeitet.
Die Methode groupingBy
hat auch eine Überlastungsfunktion, die einen nachgeschalteten Collector nimmt:
/**
* @param <T> the type of the input elements
* @param <K> the type of the keys
* @param <A> the intermediate accumulation type of the downstream collector
* @param <D> the result type of the downstream reduction
* @param classifier a classifier function mapping input elements to keys
* @param downstream a {@code Collector} implementing the downstream reduction
* @return a {@code Collector} implementing the cascaded group-by operation
*/
static
<
T
,
K
,
A
,
D
>
Collector
<
T
,?,
Map
<
K
,
D
>>
groupingBy
(
Function
<?
super
T
,?
extends
K
>
classifier
,
Collector
<?
super
T
,
A
,
D
>
downstream
)
Ein Teil des Javadoc-Kommentars aus dem Quellcode ist in der Signatur enthalten, aus dem hervorgeht, dass T
der Typ des Elements in der Sammlung, K
der Schlüsseltyp für die resultierende Map, A
ein Akkumulator und D
der Typ des nachgeschalteten Collectors ist. Das ?
steht für "unbekannt". In Anhang A findest du weitere Informationen über Generics in Java 8.
Mehrere Methoden in Stream
haben Entsprechungen in der Klasse Collectors
. Tabelle 4-2 zeigt, wie sie aufeinander abgestimmt sind.
Stream | Sammler |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Auch hier besteht der Zweck eines nachgelagerten Collectors darin, die Sammlung von Objekten nachzubearbeiten, die durch eine vorgelagerte Operation wie Partitionierung oder Gruppierung entstanden ist.
Siehe auch
Rezept 7.1 zeigt ein Beispiel für einen nachgeschalteten Sammler bei der Ermittlung der längsten Wörter in einem Wörterbuch. In Rezept 4.5 werden die Methoden partitionBy
und groupingBy
ausführlicher behandelt. Das ganze Thema der Generika wird in Anhang A behandelt.
4.7 Maximal- und Minimalwerte finden
Lösung
Du hast mehrere Möglichkeiten: die Methoden maxBy
und minBy
auf BinaryOperator
, die Methoden max
und min
auf Stream
oder die Utility-Methoden maxBy
und minBy
auf Collectors
.
Diskussion
Eine BinaryOperator
ist eine der Funktionsschnittstellen im Paket java.util.function
. Sie erweitert BiFunction
und gilt, wenn sowohl die Argumente der Funktion als auch der Rückgabewert aus derselben Klasse stammen.
Die Schnittstelle BinaryOperator
fügt zwei statische Methoden hinzu:
static
<
T
>
BinaryOperator
<
T
>
maxBy
(
Comparator
<?
super
T
>
comparator
)
static
<
T
>
BinaryOperator
<
T
>
minBy
(
Comparator
<?
super
T
>
comparator
)
Jede dieser gibt eine BinaryOperator
zurück, die den angegebenen Comparator
verwendet.
Um die verschiedenen Möglichkeiten zu demonstrieren, wie man den maximalen Wert aus einem Stream erhält, betrachten wir ein POJO namens Employee
, das drei Attribute enthält: name
, salary
, und department
, wie in Beispiel 4-24.
Beispiel 4-24. Mitarbeiter POJO
public
class
Employee
{
private
String
name
;
private
Integer
salary
;
private
String
department
;
// ... other methods ...
}
List
<
Employee
>
employees
=
Arrays
.
asList
(
new
Employee
(
"Cersei"
,
250_000
,
"Lannister"
)
,
new
Employee
(
"Jamie"
,
150_000
,
"Lannister"
)
,
new
Employee
(
"Tyrion"
,
1_000
,
"Lannister"
)
,
new
Employee
(
"Tywin"
,
1_000_000
,
"Lannister"
)
,
new
Employee
(
"Jon Snow"
,
75_000
,
"Stark"
)
,
new
Employee
(
"Robb"
,
120_000
,
"Stark"
)
,
new
Employee
(
"Eddard"
,
125_000
,
"Stark"
)
,
new
Employee
(
"Sansa"
,
0
,
"Stark"
)
,
new
Employee
(
"Arya"
,
1_000
,
"Stark"
)
)
;
Employee
defaultEmployee
=
new
Employee
(
"A man (or woman) has no name"
,
0
,
"Black and White"
)
;
Wenn du eine Sammlung von Mitarbeitern hast, kannst du die Methode reduce
auf Stream
verwenden, die ein BinaryOperator
als Argument benötigt. Der Ausschnitt in Beispiel 4-25 zeigt, wie du den Mitarbeiter mit dem höchsten Gehalt findest.
Beispiel 4-25. BinaryOperator.maxBy verwenden
Optional
<
Employee
>
optionalEmp
=
employees
.
stream
()
.
reduce
(
BinaryOperator
.
maxBy
(
Comparator
.
comparingInt
(
Employee:
:
getSalary
)));
System
.
out
.
println
(
"Emp with max salary: "
+
optionalEmp
.
orElse
(
defaultEmployee
));
Die Methode reduce
erfordert eine BinaryOperator
. Die statische Methode maxBy
erzeugt diese BinaryOperator
auf der Grundlage der gelieferten Comparator
, die in diesem Fall die Angestellten nach Gehalt vergleicht.
Das funktioniert, aber es gibt auch eine praktische Methode namens max
, die direkt auf angewendet werden kann:
Optional
<
T
>
max
(
Comparator
<?
super
T
>
comparator
)
Die direkte Anwendung dieser Methode wird in Beispiel 4-26 gezeigt.
Beispiel 4-26. Stream.max verwenden
optionalEmp
=
employees
.
stream
()
.
max
(
Comparator
.
comparingInt
(
Employee:
:
getSalary
));
Das Ergebnis ist das gleiche.
Beachte, dass es auch eine Methode namens max
für die primitiven Streams (IntStream
, LongStream
und DoubleStream
) gibt, die keine Argumente benötigt. Beispiel 4-27 zeigt diese Methode in Aktion.
Beispiel 4-27. Den höchsten Lohn finden
OptionalInt
maxSalary
=
employees
.
stream
()
.
mapToInt
(
Employee:
:
getSalary
)
.
max
();
System
.
out
.
println
(
"The max salary is "
+
maxSalary
);
In diesem Fall wird die Methode mapToInt
verwendet, um den Strom von Arbeitnehmern in einen Strom von ganzen Zahlen umzuwandeln, indem die Methode getSalary
aufgerufen wird, und der zurückgegebene Strom ist ein IntStream
. Die Methode max
gibt dann ein OptionalInt
zurück.
Es gibt auch eine statische Methode namens maxBy
in der Hilfsklasse Collectors
. Du kannst sie hier direkt verwenden, wie in Beispiel 4-28.
Beispiel 4-28. Collectors.maxBy verwenden
optionalEmp
=
employees
.
stream
()
.
collect
(
Collectors
.
maxBy
(
Comparator
.
comparingInt
(
Employee:
:
getSalary
)));
Dies ist jedoch umständlich und kann durch die Methode max
auf Stream
ersetzt werden, wie im vorangegangenen Beispiel gezeigt. Die Methode maxBy
auf Collectors
ist hilfreich, wenn sie als nachgelagerter Collector verwendet wird (d.h. wenn eine Gruppierungs- oder Partitionierungsoperation nachbearbeitet wird). Der Code in Beispiel 4-29 verwendet groupingBy
auf Stream
, um eine Map
von Abteilungen zu Listen von Arbeitnehmern zu erstellen, ermittelt dann aber den Arbeitnehmer mit dem höchsten Gehalt in jeder Abteilung.
Beispiel 4-29. Collectors.maxBy als nachgeschalteten Collector verwenden
Map
<
String
,
Optional
<
Employee
>>
map
=
employees
.
stream
()
.
collect
(
Collectors
.
groupingBy
(
Employee:
:
getDepartment
,
Collectors
.
maxBy
(
Comparator
.
comparingInt
(
Employee:
:
getSalary
))));
map
.
forEach
((
house
,
emp
)
->
System
.
out
.
println
(
house
+
": "
+
emp
.
orElse
(
defaultEmployee
)));
Die Methode minBy
funktioniert in jeder dieser Klassen auf die gleiche Weise.
Siehe auch
Die Funktionen werden in Rezept 2.4 behandelt. Nachgeschaltete Kollektoren findest du in Rezept 4.6.
4.8 Unveränderliche Sammlungen erstellen
Diskussion
Mit ihrem Fokus auf Parallelisierung und Klarheit bevorzugt die funktionale Programmierung unveränderliche Objekte, wo immer es möglich ist. Das Collections-Framework, das mit Java 1.2 eingeführt wurde, verfügte schon immer über Methoden, um unveränderliche Sammlungen aus bestehenden Sammlungen zu erstellen, wenn auch auf etwas umständliche Weise.
Die Hilfsklasse Collections
hat die Methoden unmodifiableList
, unmodifiableSet
und unmodifiableMap
(sowie einige andere Methoden mit den Präfixen und unmodifiable
), wie in Beispiel 4-30 gezeigt.
Beispiel 4-30. Unveränderbare Methoden in der Klasse Collections
static
<
T
>
List
<
T
>
unmodifiableList
(
List
<?
extends
T
>
list
)
static
<
T
>
Set
<
T
>
unmodifiableSet
(
Set
<?
extends
T
>
s
)
static
<
K
,
V
>
Map
<
K
,
V
>
unmodifiableMap
(
Map
<?
extends
K
,?
extends
V
>
m
)
In jedem Fall ist das Argument der Methode eine existierende Liste, Menge oder Karte und die resultierende Liste, Menge oder Karte hat die gleichen Elemente wie das Argument, aber mit einem wichtigen Unterschied: Alle Methoden, die die Sammlung verändern könnten, wie add
oder remove
, werfen nun ein UnsupportedOperationException
aus.
Wenn du vor Java 8 die einzelnen Werte als Argument erhalten hast, indem du eine variable Argumentliste verwendet hast, hast du eine unveränderbare Liste oder Menge erzeugt, wie in Beispiel 4-31 gezeigt.
Beispiel 4-31. Unveränderbare Listen oder Sets vor Java 8 erstellen
@SafeVarargs
public
final
<
T
>
List
<
T
>
createImmutableListJava7
(
T
.
.
.
elements
)
{
return
Collections
.
unmodifiableList
(
Arrays
.
asList
(
elements
)
)
;
}
@SafeVarargs
public
final
<
T
>
Set
<
T
>
createImmutableSetJava7
(
T
.
.
.
elements
)
{
return
Collections
.
unmodifiableSet
(
new
HashSet
<
>
(
Arrays
.
asList
(
elements
)
)
)
;
}
Du versprichst, den Typ des Eingabefeldes nicht zu verfälschen. Siehe Anhang A für Details.
Die Idee ist in jedem Fall, mit den eingehenden Werten zu beginnen und sie in eine List
umzuwandeln. Du kannst die resultierende Liste mit unmodifiableList
einpacken oder, im Falle einer Set
, die Liste als Argument für einen Set-Konstruktor verwenden, bevor du unmodifiableSet
benutzt.
In Java 8, mit der neuen Stream
API, kannst du stattdessen die statische Methode Collectors.collectingAndThen
nutzen, wie in Beispiel 4-32.
Beispiel 4-32. Unveränderbare Listen oder Sets in Java 8 erstellen
import
static
java
.
util
.
stream
.
Collectors
.
collectingAndThen
;
import
static
java
.
util
.
stream
.
Collectors
.
toList
;
import
static
java
.
util
.
stream
.
Collectors
.
toSet
;
// ... define a class with the following methods ...
@SafeVarargs
public
final
<
T
>
List
<
T
>
createImmutableList
(
T
.
.
.
elements
)
{
return
Arrays
.
stream
(
elements
)
.
collect
(
collectingAndThen
(
toList
(
)
,
Collections:
:
unmodifiableList
)
)
;
}
@SafeVarargs
public
final
<
T
>
Set
<
T
>
createImmutableSet
(
T
.
.
.
elements
)
{
return
Arrays
.
stream
(
elements
)
.
collect
(
collectingAndThen
(
toSet
(
)
,
Collections:
:
unmodifiableSet
)
)
;
}
Die Methode Collectors.collectingAndThen
nimmt zwei Argumente entgegen: einen nachgeschalteten Collector
und einen Function
genannten Finisher. Die Idee ist, die Eingabeelemente zu streamen und sie dann in einem List
oder Set
zu sammeln, und dann wickelt die unveränderbare Funktion die resultierende Sammlung ein.
Die Umwandlung einer Reihe von Eingabeelementen in eine unveränderbare Map
ist nicht so eindeutig, unter anderem weil nicht klar ist, welche der Eingabeelemente als Schlüssel und welche als Werte angenommen werden. Der in Beispiel 4-33 gezeigte Code4 zeigt, erstellt eine unveränderbare Map
auf sehr umständliche Weise, indem er einen Instanzinitialisierer verwendet.
Beispiel 4-33. Eine unveränderliche Map erstellen
Map
<
String
,
Integer
>
map
=
Collections
.
unmodifiableMap
(
new
HashMap
<
String
,
Integer
>()
{{
put
(
"have"
,
1
);
put
(
"the"
,
2
);
put
(
"high"
,
3
);
put
(
"ground"
,
4
);
}});
Leserinnen und Leser, die mit Java 9 vertraut sind, wissen jedoch bereits, dass dieses gesamte Rezept durch eine sehr einfache Reihe von Fabrikmethoden ersetzt werden kann: List.of
, Set.of
, und Map.of
.
Siehe auch
Rezept 10.3 zeigt die neuen Fabrikmethoden in Java 9, die automatisch unveränderliche Sammlungen erstellen.
4.9 Implementierung der Collector-Schnittstelle
Lösung
Gib Lambda-Ausdrücke oder Methodenreferenzen für die Supplier
, Akkumulator-, Combiner- und Finisher-Funktionen an, die von den Collector.of
factory-Methoden verwendet werden, zusammen mit allen gewünschten Eigenschaften.
Diskussion
Die Hilfsklasse java.util.stream.Collectors
hat mehrere praktische statische Methoden, deren Rückgabetyp Collector
ist. Beispiele sind toList
, toSet
, toMap
und sogar toCollection
, die alle an anderer Stelle in diesem Buch erläutert werden. Instanzen von Klassen, die Collector
implementieren, werden als Argumente an die Methode collect
auf Stream
gesendet. In Beispiel 4-34 zum Beispiel akzeptiert die Methode String-Argumente und gibt eine List
zurück, die nur solche enthält, deren Länge gerade ist.
Beispiel 4-34. Collect verwenden, um eine Liste zurückzugeben
public
List
<
String
>
evenLengthStrings
(
String
.
.
.
strings
)
{
return
Stream
.
of
(
strings
)
.
filter
(
s
-
>
s
.
length
(
)
%
2
=
=
0
)
.
collect
(
Collectors
.
toList
(
)
)
;
}
Wenn du jedoch deine eigenen Collectors schreiben musst, ist das Verfahren etwas komplizierter. Collectors verwenden fünf Funktionen, die zusammenarbeiten, um Einträge in einem veränderbaren Container zu sammeln und das Ergebnis optional umzuwandeln. Die fünf Funktionen heißen supplier
, accumulator
, combiner
, finisher
, und characteristics
.
Nehmen wir zuerst die Funktion characteristics
. stellt eine unveränderliche Set
von Elementen eines enum
Typs Collector.Characteristics
dar. Die drei möglichen Werte sind CONCURRENT
, IDENTITY_FINISH
und UNORDERED
. CONCURRENT
bedeutet, dass der Ergebniscontainer die Akkumulatorfunktion unterstützen kann, die von mehreren Threads aus gleichzeitig auf dem Ergebniscontainer aufgerufen wird. UNORDERED
besagt, dass die Sammeloperation die Begegnungsreihenfolge der Elemente nicht beibehalten muss. IDENTITY_FINISH
bedeutet, dass die Abschlussfunktion ihr Argument unverändert zurückgibt.
Beachte, dass du keine Eigenschaften angeben musst, wenn die Standardeinstellungen deinen Wünschen entsprechen.
Der Zweck jeder der erforderlichen Methoden ist:
supplier()
-
Erstelle den Akkumulator-Container mit einer
Supplier<A>
accumulator()
-
Füge dem Akkumulator-Container ein einzelnes neues Datenelement hinzu, indem du eine
BiConsumer<A,T>
combiner()
-
Zwei Akkumulatorcontainer zusammenführen mit einer
BinaryOperator<A>
finisher()
-
Transformiere den Akkumulator-Container in den Ergebnis-Container mit Hilfe einer
Function<A,R>
characteristics()
-
Eine
Set<Collector.Characteristics>
, die aus den Enum-Werten ausgewählt wird
Wie üblich wird alles klarer, wenn du die funktionalen Schnittstellen verstehst, die im Paket java.util.function
definiert sind. Ein Supplier
wird verwendet, um den Container zu erstellen, in dem Zwischenergebnisse akkumuliert werden. Ein BiConsumer
fügt dem Akkumulator ein einzelnes Element hinzu. Ein BinaryOperator
bedeutet, dass beide Eingabetypen und der Ausgabetyp gleich sind, also geht es hier darum, zwei Akkumulatoren zu einem zu kombinieren. Ein Function
verwandelt den Akkumulator schließlich in den gewünschten Ergebniscontainer.
Jede dieser Methoden wird während des Sammelvorgangs aufgerufen, der z. B. durch die Methode collect
auf Stream
ausgelöst wird. Vom Konzept her entspricht der Sammelprozess dem (generischen) Code in Beispiel 4-35, das aus den Javadocs stammt.
Beispiel 4-35. Wie die Collector-Methoden verwendet werden
R
container
=
collector
.
supplier
.
get
(
)
;
for
(
T
t
:
data
)
{
collector
.
accumulator
(
)
.
accept
(
container
,
t
)
;
}
return
collector
.
finisher
(
)
.
apply
(
container
)
;
Auffallend ist, dass die Funktion combiner
nicht erwähnt wird. Wenn dein Stream sequentiell ist, brauchst du sie nicht - der Algorithmus läuft wie beschrieben ab. Wenn du jedoch mit einem parallelen Datenstrom arbeitest, wird die Arbeit in mehrere Regionen aufgeteilt, von denen jede ihren eigenen Akkumulationscontainer erzeugt. Der Combiner wird dann während des Join-Prozesses verwendet, um die Akkumulator-Container zu einem einzigen zusammenzuführen, bevor die Finisher-Funktion angewendet wird.
Ein Codebeispiel, das dem in Beispiel 4-34 ähnlich ist, findest du in Beispiel 4-36.
Beispiel 4-36. Collect verwenden, um ein unveränderbares SortedSet zurückzugeben
public
SortedSet
<
String
>
oddLengthStringSet
(
String
.
.
.
strings
)
{
Collector
<
String
,
?
,
SortedSet
<
String
>
>
intoSet
=
Collector
.
of
(
TreeSet
<
String
>
:
:
new
,
SortedSet:
:
add
,
(
left
,
right
)
-
>
{
left
.
addAll
(
right
)
;
return
left
;
}
,
Collections:
:
unmodifiableSortedSet
)
;
return
Stream
.
of
(
strings
)
.
filter
(
s
-
>
s
.
length
(
)
%
2
!
=
0
)
.
collect
(
intoSet
)
;
}
Das Ergebnis ist eine sortierte, unveränderbare Menge von Zeichenketten, die lexikografisch geordnet sind.
In diesem Beispiel wurde eine der beiden überladenen Versionen der Methode static
of
verwendet, um Kollektoren zu erzeugen, deren Signaturen sind:
static
<
T
,
A
,
R
>
Collector
<
T
,
A
,
R
>
of
(
Supplier
<
A
>
supplier
,
BiConsumer
<
A
,
T
>
accumulator
,
BinaryOperator
<
A
>
combiner
,
Function
<
A
,
R
>
finisher
,
Collector
.
Characteristics
...
characteristics
)
static
<
T
,
R
>
Collector
<
T
,
R
,
R
>
of
(
Supplier
<
R
>
supplier
,
BiConsumer
<
R
,
T
>
accumulator
,
BinaryOperator
<
R
>
combiner
,
Collector
.
Characteristics
...
characteristics
)
Angesichts der praktischen Methoden in der Klasse Collectors
, die Kollektoren für dich erzeugen, musst du auf diese Weise nur selten einen eigenen erstellen. Trotzdem ist es eine nützliche Fähigkeit und zeigt einmal mehr, wie die funktionalen Schnittstellen im java.util.function
Paket zusammenkommen, um interessante Objekte zu erstellen.
Siehe auch
Die Funktion finisher
ist ein Beispiel für einen nachgeschalteten Collector, der in Rezept 4.6 näher erläutert wird. Die Funktionsschnittstellen Supplier
, Function
und BinaryOperator
werden in verschiedenen Rezepten in Kapitel 2 behandelt. Die statischen Utility-Methoden in Collectors
werden in Rezept 4.2 besprochen.
1 Ty Webb ist natürlich aus dem Film Caddyshack. Richter Smails: "Ty, was hast du heute geschossen?" Ty Webb: "Oh, Herr Richter, ich zähle nicht mit." Smails: "Wie misst du dich dann mit anderen Golfern?" Webb: "Nach Größe." Das Hinzufügen einer Sortierung nach Größe ist eine einfache Übung für den Leser.
2 Die Namen in diesem Rezept stammen aus Mystery Men, einem der großen, übersehenen Filme der 90er Jahre. (Mr. Furious: "Lance Hunt ist Captain Amazing." Der Schaufler: "Lance Hunt trägt eine Brille. Captain Amazing trägt keine Brille." Mr. Furious: "Er nimmt sie ab, wenn er sich verwandelt." Der Schaufler: "Das macht doch keinen Sinn! Dann könnte er nicht sehen!")
3 Fürs Protokoll: Die fünf längsten Wörter sind Formaldehydsulphoxylat, pathologisch-psychologisch, wissenschaftlich-philosophisch, Tetraiodophenolphthalein und thyroparathyroidektomieren. Viel Glück damit, Rechtschreibprüfung.
4 Aus dem Blogbeitrag von Carl Martensen "Java 9's Immutable Collections Are Easier To Create But Use With Caution".
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.