Handbuch zu den Sammlern von Java 8

Leitfaden für Java 8-Collectors

1. Überblick

In diesem Tutorial werden wir die Collectors von Java 8 durchgehen, die im letzten Schritt der Verarbeitung vonStream verwendet werden.

Wenn Sie mehr über die API vonStreamelbst erfahren möchten, überprüfen Siethis article.

2. DieStream.collect()-Methode

Stream.collect() ist eine der Terminalmethoden von Java 8Stream API. Es ermöglicht die Ausführung veränderbarer Falzoperationen (Umpacken von Elementen in einige Datenstrukturen und Anwenden einer zusätzlichen Logik, Verketten dieser Elemente usw.) auf Datenelemente, die in einerStream-Instanz gehalten werden.

Die Strategie für diese Operation wird über die Schnittstellenimplementierung vonCollectorbereitgestellt.

3. Collectors

Alle vordefinierten Implementierungen befinden sich in der KlasseCollectors. Es ist üblich, einen folgenden statischen Import zu verwenden, um die Lesbarkeit zu verbessern:

import static java.util.stream.Collectors.*;

oder nur einzelne Importsammler Ihrer Wahl:

import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;

In den folgenden Beispielen werden wir die folgende Liste wiederverwenden:

List givenList = Arrays.asList("a", "bb", "ccc", "dd");

3.1. Collectors.toList()

Der KollektorToListkann zum Sammeln allerStream-Elemente in einerList-Instanz verwendet werden. Wichtig ist, dass wir mit dieser Methode keine bestimmteList-Implementierung annehmen können. Wenn Sie mehr Kontrolle darüber haben möchten, verwenden Sie stattdessentoCollection.

Erstellen wir eineStream-Instanz, die eine Folge von Elementen darstellt, und sammeln sie in einerList-Instanz:

List result = givenList.stream()
  .collect(toList());

3.2. Collectors.toSet()

Der KollektorToSetkann zum Sammeln allerStream-Elemente in einerSet-Instanz verwendet werden. Wichtig ist, dass wir mit dieser Methode keine bestimmteSet-Implementierung annehmen können. Wenn wir mehr Kontrolle darüber haben wollen, können wir stattdessentoCollection verwenden.

Erstellen wir eineStream-Instanz, die eine Folge von Elementen darstellt, und sammeln sie in einerSet-Instanz:

Set result = givenList.stream()
  .collect(toSet());

ASet enthält keine doppelten Elemente. Wenn unsere Sammlung gleich große Elemente enthält, erscheinen sie nur einmal in den resultierendenSet:

List listWithDuplicates = Arrays.asList("a", "bb", "c", "d", "bb");
Set result = listWithDuplicates.stream().collect(toSet());
assertThat(result).hasSize(4);

3.3. Collectors.toCollection()

Wie Sie wahrscheinlich bereits bemerkt haben, können Sie bei Verwendung vontoSet and toList-Sammlern keine Annahmen über deren Implementierung treffen. Wenn Sie eine benutzerdefinierte Implementierung verwenden möchten, müssen Sie den KollektortoCollectionmit einer bereitgestellten Sammlung Ihrer Wahl verwenden.

Erstellen wir eineStream-Instanz, die eine Folge von Elementen darstellt, und sammeln sie in einerLinkedList-Instanz:

List result = givenList.stream()
  .collect(toCollection(LinkedList::new))

Beachten Sie, dass dies bei unveränderlichen Sammlungen nicht funktioniert. In diesem Fall müssten Sie entweder eine benutzerdefinierteCollector-Implementierung schreiben odercollectingAndThen verwenden.

3.4. Collectors.toMap()

Der KollektorToMapkann verwendet werden, umStream Elemente in einerMap Instanz zu sammeln. Dazu müssen wir zwei Funktionen bereitstellen:

  • keyMapper

  • valueMapper

keyMapper wird zum Extrahieren einesMap-Schlüssels aus einemStream-Element verwendet, undvalueMapper wird zum Extrahieren eines Werts verwendet, der einem bestimmten Schlüssel zugeordnet ist.

Sammeln wir diese Elemente inMap, in denen Zeichenfolgen als Schlüssel und ihre Längen als Werte gespeichert sind:

Map result = givenList.stream()
  .collect(toMap(Function.identity(), String::length))

Function.identity() ist nur eine Verknüpfung zum Definieren einer Funktion, die denselben Wert akzeptiert und zurückgibt.

Was passiert, wenn unsere Sammlung doppelte Elemente enthält? Im Gegensatz zutoSet filterttoMap Duplikate nicht stillschweigend. Es ist verständlich - wie sollte herausgefunden werden, welcher Wert für diesen Schlüssel ausgewählt werden soll?

List listWithDuplicates = Arrays.asList("a", "bb", "c", "d", "bb");
assertThatThrownBy(() -> {
    listWithDuplicates.stream().collect(toMap(Function.identity(), String::length));
}).isInstanceOf(IllegalStateException.class);

Beachten Sie, dasstoMap nicht einmal bewertet, ob die Werte auch gleich sind. Wenn doppelte Schlüssel angezeigt werden, wird sofort einIllegalStateException ausgelöst.

In solchen Fällen mit Schlüsselkollision sollten wirtoMap mit einer anderen Signatur verwenden:

Map result = givenList.stream()
  .collect(toMap(Function.identity(), String::length, (item, identicalItem) -> item));

Das dritte Argument hier istBinaryOperator, wo wir angeben können, wie Kollisionen behandelt werden sollen. In diesem Fall wählen wir einfach einen dieser beiden kollidierenden Werte aus, da wir wissen, dass dieselben Zeichenfolgen auch immer dieselbe Länge haben.

3.5. Collectors.c_ollectingAndThen () _

CollectingAndThen ist ein spezieller Kollektor, mit dem direkt nach dem Ende des Sammelns eine weitere Aktion für ein Ergebnis ausgeführt werden kann.

Sammeln wirStream Elemente in eineList Instanz und konvertieren das Ergebnis dann in eineImmutableList Instanz:

List result = givenList.stream()
  .collect(collectingAndThen(toList(), ImmutableList::copyOf))

3.6. Collectors.j_oining () _

Der KollektorJoiningkann zum Verbinden der Elemente vonStream<String>verwendet werden.

Wir können sie zusammenfügen, indem wir Folgendes tun:

String result = givenList.stream()
  .collect(joining());

was dazu führt, dass:

"abbcccdd"

Sie können auch benutzerdefinierte Trennzeichen, Präfixe und Postfixes angeben:

String result = givenList.stream()
  .collect(joining(" "));

was dazu führt, dass:

"a bb ccc dd"

oder du kannst schreiben:

String result = givenList.stream()
  .collect(joining(" ", "PRE-", "-POST"));

was dazu führt, dass:

"PRE-a bb ccc dd-POST"

3.7. Collectors.c_ounting () _

Counting ist ein einfacher Kollektor, mit dem alleStream-Elemente einfach gezählt werden können.

Jetzt können wir schreiben:

Long result = givenList.stream()
  .collect(counting());

3.8. Collectors.s_ummarizingDouble/Long/Int()_

SummarizingDouble/Long/Int ist ein Kollektor, der eine spezielle Klasse zurückgibt, die statistische Informationen zu numerischen Daten inStream extrahierter Elemente enthält.

Informationen zu Stringlängen erhalten Sie, indem Sie folgende Schritte ausführen:

DoubleSummaryStatistics result = givenList.stream()
  .collect(summarizingDouble(String::length));

In diesem Fall gilt Folgendes:

assertThat(result.getAverage()).isEqualTo(2);
assertThat(result.getCount()).isEqualTo(4);
assertThat(result.getMax()).isEqualTo(3);
assertThat(result.getMin()).isEqualTo(1);
assertThat(result.getSum()).isEqualTo(8);

3.9. Collectors.averagingDouble/Long/Int()

AveragingDouble/Long/Int ist ein Kollektor, der einfach einen Durchschnitt der extrahierten Elemente zurückgibt.

Wir können die durchschnittliche Länge einer Zeichenkette erhalten, indem wir Folgendes tun:

Double result = givenList.stream()
  .collect(averagingDouble(String::length));

3.10. Collectors.s_ummingDouble/Long/Int()_

SummingDouble/Long/Int ist ein Kollektor, der einfach eine Summe extrahierter Elemente zurückgibt.

Wir können eine Summe aller Stringlängen erhalten, indem wir Folgendes tun:

Double result = givenList.stream()
  .collect(summingDouble(String::length));

3.11. Collectors.maxBy()/minBy()

MaxBy/MinBy collectors return the biggest/the smallest element of a Stream according to a provided Comparator instance.

Wir können das größte Element auswählen, indem wir Folgendes tun:

Optional result = givenList.stream()
  .collect(maxBy(Comparator.naturalOrder()));

Beachten Sie, dass der zurückgegebene Wert in eineOptional-Instanz eingeschlossen wird. Dies zwingt die Benutzer, den leeren Sammeleckenkoffer zu überdenken.

3.12. Collectors.groupingBy()

Der KollektorGroupingBywird zum Gruppieren von Objekten nach einer Eigenschaft und zum Speichern von Ergebnissen in einerMap-Instanz verwendet.

Wir können sie nach Zeichenfolgenlänge gruppieren und Gruppierungsergebnisse inSet Instanzen speichern:

Map> result = givenList.stream()
  .collect(groupingBy(String::length, toSet()));

Dies führt dazu, dass Folgendes zutrifft:

assertThat(result)
  .containsEntry(1, newHashSet("a"))
  .containsEntry(2, newHashSet("bb", "dd"))
  .containsEntry(3, newHashSet("ccc"));

Beachten Sie, dass das zweite Argument dergroupingBy-MethodeCollector ist und SieCollector Ihrer Wahl verwenden können.

3.13. Collectors.partitioningBy()

PartitioningBy ist ein Spezialfall vongroupingBy, der einePredicate-Instanz akzeptiert undStream-Elemente in einerMap-Instanz sammelt, in derBoolean-Werte als Schlüssel gespeichert sind und Sammlungen als Werte. Unter dem Schlüssel "true" finden Sie eine Sammlung von Elementen, die mit den angegebenenPredicateübereinstimmen, und unter dem Schlüssel "false" finden Sie eine Sammlung von Elementen, die nicht mit den angegebenenPredicateübereinstimmen.

Du kannst schreiben:

Map> result = givenList.stream()
  .collect(partitioningBy(s -> s.length() > 2))

Was zu einer Karte führt, die Folgendes enthält:

{false=["a", "bb", "dd"], true=["ccc"]}

3.14. Collectors.teeing()

Lassen Sie uns die maximalen und minimalen Zahlen aus einem bestimmtenStream mithilfe der Kollektoren ermitteln, die wir bisher gelernt haben:

List numbers = Arrays.asList(42, 4, 2, 24);
Optional min = numbers.stream().collect(minBy(Integer::compareTo));
Optional max = numbers.stream().collect(maxBy(Integer::compareTo));
// do something useful with min and max

Hier verwenden wir zwei verschiedene Sammler und kombinieren dann das Ergebnis dieser beiden, um etwas Sinnvolles zu schaffen. Vor Java 12 mussten wir, um solche Anwendungsfälle abzudecken, die angegebenenStream zweimal bearbeiten, die Zwischenergebnisse in temporären Variablen speichern und diese Ergebnisse anschließend kombinieren.

Glücklicherweise bietet Java 12 einen eingebauten Collector, der diese Schritte für uns erledigt: Wir müssen nur die beiden Collectors und die Combiner-Funktion bereitstellen.

Da dieser neue Kollektorteesden gegebenen Strom in zwei verschiedene Richtungen bewegt, wird erteeing: genannt

numbers.stream().collect(teeing(
  minBy(Integer::compareTo), // The first collector
  maxBy(Integer::compareTo), // The second collector
  (min, max) -> // Receives the result from those collectors and combines them
));

Dieses Beispiel ist auf GitHub im Projektcore-java-12verfügbar.

4. Benutzerdefinierte Sammler

Wenn Sie Ihre Collector-Implementierung schreiben möchten, müssen Sie die Collector-Schnittstelle implementieren und ihre drei generischen Parameter angeben:

public interface Collector {...}
  1. T - der Typ der Objekte, die zur Sammlung verfügbar sein werden,

  2. A - der Typ eines veränderlichen Akkumulatorobjekts,

  3. R - der Typ eines Endergebnisses.

Schreiben wir einen Beispielkollektor zum Sammeln von Elementen in einerImmutableSet-Instanz. Wir beginnen mit der Angabe der richtigen Typen:

private class ImmutableSetCollector
  implements Collector, ImmutableSet> {...}

Da wir eine veränderbare Sammlung für die Behandlung interner Sammlungsvorgänge benötigen, können wir hierfür nichtImmutableSet verwenden. Wir müssen eine andere veränderbare Sammlung oder eine andere Klasse verwenden, die vorübergehend Objekte für uns ansammeln könnte. In diesem Fall fahren wir mitImmutableSet.Builder fort und müssen nun 5 Methoden implementieren:

  • Lieferant >supplier ()

  • BiConsumer , T>accumulator ()

  • BinaryOperator >combiner ()

  • Funktion , ImmutableSet >finisher ()

  • Setze characteristics ()

Die MethodeThe supplier()gibt eineSupplier-Instanz zurück, die eine leere Akkumulatorinstanz generiert. In diesem Fall können wir also einfach schreiben:

@Override
public Supplier> supplier() {
    return ImmutableSet::builder;
}

Die MethodeThe accumulator()gibt eine Funktion zurück, die zum Hinzufügen eines neuen Elements zu einem vorhandenenaccumulator-Objekt verwendet wird. Verwenden Sie also einfach dieadd-Methodeadd.

@Override
public BiConsumer, T> accumulator() {
    return ImmutableSet.Builder::add;
}

Die MethodeThe combiner()gibt eine Funktion zurück, mit der zwei Akkumulatoren zusammengeführt werden:

@Override
public BinaryOperator> combiner() {
    return (left, right) -> left.addAll(right.build());
}

Die Methode vonThe finisher()gibt eine Funktion zurück, die zum Konvertieren eines Akkumulators in den Endergebnistyp verwendet wird. In diesem Fall verwenden wir nur diebuild-Methode vonBuilder:

@Override
public Function, ImmutableSet> finisher() {
    return ImmutableSet.Builder::build;
}

Die MethodeThe characteristics()wird verwendet, um Stream einige zusätzliche Informationen bereitzustellen, die für interne Optimierungen verwendet werden. In diesem Fall achten wir nicht auf die Reihenfolge der Elemente inSet, sodass wirCharacteristics.UNORDERED verwenden. Weitere Informationen zu diesem Thema erhalten Sie in JavaDoc vonCharacteristics.

@Override public Set characteristics() {
    return Sets.immutableEnumSet(Characteristics.UNORDERED);
}

Hier ist die vollständige Implementierung zusammen mit der Verwendung:

public class ImmutableSetCollector
  implements Collector, ImmutableSet> {

@Override
public Supplier> supplier() {
    return ImmutableSet::builder;
}

@Override
public BiConsumer, T> accumulator() {
    return ImmutableSet.Builder::add;
}

@Override
public BinaryOperator> combiner() {
    return (left, right) -> left.addAll(right.build());
}

@Override
public Function, ImmutableSet> finisher() {
    return ImmutableSet.Builder::build;
}

@Override
public Set characteristics() {
    return Sets.immutableEnumSet(Characteristics.UNORDERED);
}

public static  ImmutableSetCollector toImmutableSet() {
    return new ImmutableSetCollector<>();
}

und hier in Aktion:

List givenList = Arrays.asList("a", "bb", "ccc", "dddd");

ImmutableSet result = givenList.stream()
  .collect(toImmutableSet());

5. Fazit

In diesem Artikel haben wir uns eingehend mitCollectorsvon Java 8 befasst und gezeigt, wie man eines implementiert. Stellen Sie sicher, dasscheck one of my projects which enhances the capabilities of parallel processing in Java.

Alle Codebeispiele sind aufGitHub verfügbar. Sie können weitere interessante Artikelon my site lesen.