Anleitung zu den Collectors von Java 8

1. Übersicht

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

Wenn Sie mehr über die Stream- API selbst erfahren möchten, lesen Sie diesen Artikel.

Wenn Sie erfahren möchten, wie Sie die Leistung von Collectors für die parallele Verarbeitung nutzen können, überprüfen Sie dieses Projekt.

2. Die Stream.collect () Methode

Stream.collect () ist eine der Terminalmethoden der Stream-API von Java 8 . Es ermöglicht uns, veränderbare Falzoperationen (Umpacken von Elementen in einige Datenstrukturen und Anwenden einer zusätzlichen Logik, Verketten dieser Elemente usw.) auf Datenelemente durchzuführen, die in einer Stream- Instanz enthalten sind.

Die Strategie für diesen Vorgang wird über die Implementierung der Collector- Schnittstelle bereitgestellt .

3. Sammler

Alle vordefinierten Implementierungen finden Sie in der Collectors- Klasse. Es ist üblich, den 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 ToList- Kollektor kann zum Sammeln aller Stream- Elemente in einer List- Instanz verwendet werden. Wichtig ist, dass wir mit dieser Methode keine bestimmte List- Implementierung annehmen können . Wenn Sie mehr Kontrolle darüber haben möchten, verwenden Sie stattdessen toCollection .

Erstellen wir eine Stream- Instanz, die eine Folge von Elementen darstellt, und sammeln sie in einer List- Instanz:

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

3.1.1. Collectors.toUnmodizableList ()

Java 10 hat eine bequeme Möglichkeit eingeführt, die Stream- Elemente in einer nicht veränderbaren Liste zu akkumulieren :

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

Wenn wir jetzt versuchen , das zu ändern Ergebnis - Liste , werden wir eine bekommen UnsupportedOperationException :

assertThatThrownBy(() -> result.add("foo")) .isInstanceOf(UnsupportedOperationException.class);

3.2. Collectors.toSet ()

Der ToSet- Kollektor kann zum Sammeln aller Stream- Elemente in einer Set- Instanz verwendet werden. Das Wichtigste ist, dass wir mit dieser Methode keine bestimmte Set- Implementierung annehmen können . Wenn wir mehr Kontrolle darüber haben möchten, können wir stattdessen toCollection verwenden.

Erstellen wir eine Stream- Instanz, die eine Folge von Elementen darstellt, und sammeln sie in einer Set- Instanz:

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

Ein Set enthält keine doppelten Elemente. Wenn unsere Sammlung gleichwertige Elemente enthält, erscheinen sie nur einmal im resultierenden Set :

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

3.2.1. Collectors.toUnmodizableSet ()

Seit Java 10 können wir mit dem Collector toUnmodizableSet () problemlos ein nicht modifizierbares Set erstellen :

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

Jeder Versuch, die Ergebnismenge zu ändern , führt zu UnsupportedOperationException :

assertThatThrownBy(() -> result.add("foo")) .isInstanceOf(UnsupportedOperationException.class);

3.3. Collectors.toCollection ()

Wie Sie wahrscheinlich bereits bemerkt haben, können Sie bei der Verwendung von toSet- und toList- Kollektoren keine Annahmen über deren Implementierung treffen. Wenn Sie eine benutzerdefinierte Implementierung verwenden möchten, müssen Sie den toCollection- Kollektor mit einer bereitgestellten Sammlung Ihrer Wahl verwenden.

Erstellen wir eine Stream- Instanz, die eine Folge von Elementen darstellt, und sammeln sie in einer LinkedList- Instanz:

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

Beachten Sie, dass dies bei unveränderlichen Sammlungen nicht funktioniert. In einem solchen Fall müssen Sie entweder eine benutzerdefinierte schreiben Collector - Implementierung oder verwenden collectingAndThen .

3.4. Sammler . toMap ()

Der ToMap- Kollektor kann verwendet werden, um Stream- Elemente in einer Map- Instanz zu sammeln . Dazu müssen wir zwei Funktionen bereitstellen:

  • keyMapper
  • valueMapper

keyMapper wird zum Extrahieren eines Map- Schlüssels aus einem Stream- Element verwendet, und valueMapper wird zum Extrahieren eines Werts verwendet, der einem bestimmten Schlüssel zugeordnet ist.

Sammeln wir diese Elemente in einer Map , in der 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 zu Toset , toMap nicht still Filter Duplikate. Es ist verständlich - wie sollte es herausfinden, welchen Wert es für diesen Schlüssel wählen soll?

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

Beachten Sie, dass toMap nicht einmal auswertet, ob die Werte auch gleich sind. Wenn doppelte Schlüssel angezeigt werden , wird sofort eine IllegalStateException ausgelöst .

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

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

Das dritte Argument hier ist ein BinaryOperator , in dem 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 dieselben Längen haben.

3.4.1. Collectors.toUnmodizableMap ()

Ähnlich wie bei Listen und Sätzen hat Java 10 eine einfache Möglichkeit eingeführt, Stream- Elemente in einer nicht modifizierbaren Map zu sammeln :

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

Wie wir sehen können, wenn wir einen neuen Eintrag in eine setzen versuchen, Ergebnis Karte , werden wir bekommen UnsupportedOperationException :

assertThatThrownBy(() -> result.put("foo", 3)) .isInstanceOf(UnsupportedOperationException.class);

3.5. Sammler .c ollectingAndThen ()

CollectingAndThen ist ein spezieller Collector, mit dem Sie direkt nach dem Ende des Sammelns eine weitere Aktion für ein Ergebnis ausführen können.

Sammeln wir Stream- Elemente in einer List- Instanz und konvertieren das Ergebnis in eine ImmutableList- Instanz:

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

3.6. Sammler .j oining ()

Joining Collector kann zum Verbinden von Stream- Elementen verwendet werden.

Wir können sie zusammenbringen, indem wir:

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

was zu folgenden Ergebnissen führen wird:

"abbcccdd"

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

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

was zu folgenden Ergebnissen führen wird:

"a bb ccc dd"

oder du kannst schreiben:

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

was zu folgenden Ergebnissen führen wird:

"PRE-a bb ccc dd-POST"

3.7. Sammler .c ounting ()

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

Jetzt können wir schreiben:

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

3.8. Collectors.summarizingDouble/Long/Int()

SummarizingDouble/Long/Int is a collector that returns a special class containing statistical information about numerical data in a Stream of extracted elements.

We can obtain information about string lengths by doing:

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

In this case, the following will be true:

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 is a collector that simply returns an average of extracted elements.

We can get average string length by doing:

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

3.10. Collectors.summingDouble/Long/Int()

SummingDouble/Long/Int is a collector that simply returns a sum of extracted elements.

We can get a sum of all string lengths by doing:

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.

We can pick the biggest element by doing:

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

Notice that returned value is wrapped in an Optional instance. This forces users to rethink the empty collection corner case.

3.12. Collectors.groupingBy()

GroupingBy collector is used for grouping objects by some property and storing results in a Map instance.

We can group them by string length and store grouping results in Set instances:

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

This will result in the following being true:

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

Notice that the second argument of the groupingBy method is a Collector and you are free to use any Collector of your choice.

3.13. Collectors.partitioningBy()

PartitioningBy is a specialized case of groupingBy that accepts a Predicate instance and collects Stream elements into a Map instance that stores Boolean values as keys and collections as values. Under the “true” key, you can find a collection of elements matching the given Predicate, and under the “false” key, you can find a collection of elements not matching the given Predicate.

You can write:

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

Which results in a Map containing:

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

3.14. Collectors.teeing()

Let's find the maximum and minimum numbers from a given Stream using the collectors we've learned so far:

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

Here, we're using two different collectors and then combining the result of those two to create something meaningful. Before Java 12, in order to cover such use cases, we had to operate on the given Stream twice, store the intermediate results into temporary variables and then combine those results afterward.

Fortunately, Java 12 offers a built-in collector that takes care of these steps on our behalf: all we have to do is provide the two collectors and the combiner function.

Since this new collector tees the given stream towards two different directions, it's called teeing:

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

This example is available on GitHub in the core-java-12 project.

4. Custom Collectors

If you want to write your Collector implementation, you need to implement Collector interface and specify its three generic parameters:

public interface Collector {...}
  1. T – the type of objects that will be available for collection,
  2. A – the type of a mutable accumulator object,
  3. R – the type of a final result.

Let's write an example Collector for collecting elements into an ImmutableSet instance. We start by specifying the right types:

private class ImmutableSetCollector implements Collector
    
      {...}
    

Since we need a mutable collection for internal collection operation handling, we can't use ImmutableSet for this; we need to use some other mutable collection or any other class that could temporarily accumulate objects for us.

In this case, we will go on with an ImmutableSet.Builder and now we need to implement 5 methods:

  • Supplier supplier()
  • BiConsumer accumulator()
  • BinaryOperator combiner()
  • Function finisher()
  • Set characteristics()

The supplier()method returns a Supplier instance that generates an empty accumulator instance, so, in this case, we can simply write:

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

The accumulator() method returns a function that is used for adding a new element to an existing accumulator object, so let's just use the Builder‘s add method.

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

The combiner()method returns a function that is used for merging two accumulators together:

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

The finisher() method returns a function that is used for converting an accumulator to final result type, so in this case, we will just use Builder‘s build method:

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

Die Methode properties () 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 in einem Set, sodass wir Characteristics.UNORDERED verwenden . Weitere Informationen zu diesem Thema finden Sieim JavaDoc von Characteristics .

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

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

public class ImmutableSetCollector implements Collector
    
      { @Override public Supplier
     
       supplier() { return ImmutableSet::builder; } @Override public BiConsumer
      
        accumulator() { return ImmutableSet.Builder::add; } @Override public BinaryOperator
       
         combiner() { return (left, right) -> left.addAll(right.build()); } @Override public Function
        
          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. Schlussfolgerung

In diesem Artikel haben wir uns eingehend mit den Collectors von Java 8 befasst und gezeigt, wie man einen implementiert. Stellen Sie sicher, dass Sie eines meiner Projekte überprüfen, das die Funktionen der Parallelverarbeitung in Java verbessert.

Alle Codebeispiele sind auf dem GitHub verfügbar. Sie können weitere interessante Artikel auf meiner Website lesen.