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

Problem

Du willst Objekte sortieren.

Lösung

Verwende die Methode sorted auf Stream mit einer Comparator, die entweder mit einem Lambda-Ausdruck implementiert oder von einer der statischen Methoden compare auf der Schnittstelle Compa⁠rator erzeugt wird.

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);  1
    return sampleStrings;
}

public List<String> defaultSortUsingStreams() {
    return sampleStrings.stream()
        .sorted()                     2
        .collect(Collectors.toList());
}
1

Standardsortierung ab Java 7 und darunter

2

Standardsortierung ab Java 8 und höher

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()) 1
        .collect(toList());
}

public List<String> lengthSortUsingComparator() {
    return sampleStrings.stream()
        .sorted(Comparator.comparingInt(String::length)) 2
        .collect(toList());
}
1

Ein Lambda für die Comparator verwenden, um nach Länge zu sortieren

2

Verwendung einer Comparator mit der Methode comparingInt

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)            1
                    .thenComparing(naturalOrder()))
        .collect(toList());
}
1

Nach Länge sortieren, dann gleich lange Strings alphabetisch sortieren

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

Problem

Nach der Stream-Verarbeitung möchtest du in eine List, Set oder eine andere lineare Sammlung konvertieren.

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.

Tipp

Die Java 8 API verwendet häufig eine statische Methode namens of als Factory-Methode.

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")  1
          .collect(Collectors.toSet());
}
1

Doppelter Name, der bei der Umwandlung in eine Set

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 Hash⁠Set 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); 1
}
1

Array-Konstruktor-Referenz als Supplier

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)); 1

actorMap.forEach((key,value) ->
    System.out.printf("%s played %s%n", key, value));
1

Funktionen zur Erzeugung von Schlüsseln und Werten

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

Suppliers 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

Problem

Du möchtest eine Sammlung von Objekten zu einer Map hinzufügen, wobei der Schlüssel eine der Objekteigenschaften und der Wert das Objekt selbst ist.

Lösung

Verwende die toMap Methode von Collectors, zusammen mit Function.identity.

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));              1

bookMap = books.stream()
    .collect(Collectors.toMap(Book::getId, Function.identity())); 2
1

Identitäts-Lambda: Gib ein Element zurück.

2

Die statische Methode identity in Function tut dasselbe

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

Problem

Du willst eine Map nach Schlüssel oder nach Wert sortieren.

Lösung

Verwende die neuen statischen Methoden in der Schnittstelle Map.Entry.

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.

Tabelle 4-1. Statische Methoden in Map.Entry (aus Java 8 docs)
Methode Beschreibung

comparingByKey()

Gibt einen Komparator zurück, der Map.Entryin natürlicher Reihenfolge nach Schlüssel vergleicht

comparingByKey(Comparator<? super K> cmp)

Gibt einen Komparator zurück, der Map.Entrynach Schlüssel vergleicht und dabei die angegebenen Comparator

comparingByValue()

Gibt einen Komparator zurück, der Map.Entryin natürlicher Reihenfolge auf Wert vergleicht

comparingByValue(Comparator<? super V> cmp)

Gibt einen Komparator zurück, der Map.Entrynach Wert vergleicht und dabei die angegebene Comparator

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 implementiert AutoCloseable, so dass Java, wenn der Try-Block beendet wird, die Methode close auf Stream aufruft, die wiederum die Methode close auf File aufruft.

  • Der Filter schränkt die weitere Verarbeitung auf Wörter mit einer Länge von mindestens 20 Zeichen ein.

  • Die Methode groupingBy von Collectors nimmt als erstes Argument ein Function, 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 eine Map, 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ätte groupingBy(String::length) eine Map<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 weiteren Collector, einen sogenannten Downstream Collector, angeben, der die Wortlisten nachbearbeitet. In diesem Fall ist der Rückgabetyp Map<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

Problem

Du willst eine Sammlung von Elementen in Kategorien unterteilen.

Lösung

Die Methode Collectors.partitioningBy unterteilt Elemente in solche, die eine Predicate erfüllen, und solche, die das nicht tun. Die Methode Collectors.groupingBy erzeugt eine Map von Kategorien, wobei die Werte die Elemente in jeder Kategorie sind.

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)); 1

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]
1

Partitionierung nach gerader oder ungerader Länge

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 Predic⁠ate 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)); 1

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]
1

Strings nach Länge gruppieren

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 partitioning​By Operation zurückgegeben werden, nachbearbeitet.

4.6 Nachgeschaltete Kollektoren

Problem

Du möchtest die von einer groupingBy oder partitioningBy Operation zurückgegebenen Sammlungen nachbearbeiten.

Lösung

Verwende eine der statischen Hilfsmethoden aus der Klasse java.util.stream.Collectors.

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()));  1

numberLengthMap.forEach((k,v) -> System.out.printf("%5s: %d%n", k, v));
//
// false: 4
//  true: 8
1

Nachgeschalteter Kollektor

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.

Tabelle 4-2. Collectors-Methoden ähnlich den Stream-Methoden
Stream Sammler

count

counting

map

mapping

min

minBy

max

maxBy

IntStream.sum

summingInt

DoubleStream.sum

summingDouble

LongStream.sum

summingLong

IntStream.summarizing

summarizingInt

DoubleStream.summarizing

summarizingDouble

LongStream.summarizing

summarizingLong

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

Problem

Du willst den maximalen oder minimalen Wert in einem Stream bestimmen.

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(                  1
        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 =                                2
    new Employee("A man (or woman) has no name", 0, "Black and White");
1

Sammlung von Arbeitnehmern

2

Standard, wenn der Stream leer ist

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

Problem

Du möchtest eine unveränderliche Liste, Menge oder Karte mit der Stream API erstellen.

Lösung

Verwende die neue statische Methode collectingAndThen in der Klasse Collectors.

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  1
public final <T> List<T> createImmutableListJava7(T... elements) {
    return Collections.unmodifiableList(Arrays.asList(elements));
}

@SafeVarargs  1
public final <T> Set<T> createImmutableSetJava7(T... elements) {
    return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(elements)));
}
1

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 unmodi⁠fiable​Set benutzt.

In Java 8, mit der neuen Stream API, kannst du stattdessen die statische Methode Col⁠lectors.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));  1
}

@SafeVarargs
public final <T> Set<T> createImmutableSet(T... elements) {
    return Arrays.stream(elements)
        .collect(collectingAndThen(toSet(),
                    Collections::unmodifiableSet));   1
}
1

"Finisher" verpackt die erzeugten Sammlungen

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

Problem

Du musst java.util.stream.Collector manuell implementieren, denn keine der Factory-Methoden in der Klasse java.util.stream.Collectors gibt dir genau das, was du brauchst.

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());  1
}
1

Sammle Zeichenketten mit gerader Länge in einer List

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 CON⁠CURRENT, 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 Bi​Consumer<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();           1
for (T t : data) {
    collector.accumulator().accept(container, t); 2
}
return collector.finisher().apply(container);     3
1

Erstelle den Akkumulator-Container

2

Füge jedes Element zum Akkumulatorenbehälter hinzu

3

Umwandlung des Akkumulatorkontainers in den Ergebniskontainer mit Hilfe des Finishers

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,           1
                        SortedSet::add,                      2
                        (left, right) -> {                   3
                              left.addAll(right);
                              return left;
                        },
                        Collections::unmodifiableSortedSet); 4
        return Stream.of(strings)
                .filter(s -> s.length() % 2 != 0)
                .collect(intoSet);
    }
1

Supplier um eine neue TreeSet

2

BiConsumer um jeden String zu den TreeSet

3

BinaryOperator um zwei SortedSet Instanzen zu einer einzigen zu kombinieren

4

finisher Funktion, um eine unveränderbare Menge zu erstellen

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.