Guida ai collezionisti di Java 8

1. Panoramica

In questo tutorial, esamineremo i collector di Java 8, che vengono utilizzati nella fase finale dell'elaborazione di uno stream .

Se vuoi saperne di più sull'API Stream stessa, controlla questo articolo.

Se vuoi vedere come sfruttare la potenza dei servizi di raccolta per l'elaborazione parallela, controlla questo progetto.

2. Il metodo Stream.collect ()

Stream.collect () è uno dei metodi del terminale dell'API Stream di Java 8 . Ci consente di eseguire operazioni di piegatura mutabile (riconfezionamento di elementi in alcune strutture di dati e applicazione di logica aggiuntiva, concatenazione, ecc.) Sugli elementi di dati contenuti in un'istanza di Stream .

La strategia per questa operazione viene fornita tramite l' implementazione dell'interfaccia del servizio di raccolta .

3. Collezionisti

Tutte le implementazioni predefinite possono essere trovate nella classe Collectors . È pratica comune utilizzare la seguente importazione statica con loro per sfruttare una maggiore leggibilità:

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

o solo singoli collezionisti di importazione a tua scelta:

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

Nei seguenti esempi riutilizzeremo il seguente elenco:

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

3.1. Collectors.toList ()

Il raccoglitore ToList può essere utilizzato per raccogliere tutti gli elementi Stream in un'istanza di List . La cosa importante da ricordare è il fatto che non possiamo assumere alcuna implementazione particolare di List con questo metodo. Se vuoi avere più controllo su questo, usa invece toCollection .

Creiamo un'istanza Stream che rappresenta una sequenza di elementi e li raccogliamo in un'istanza List :

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

3.1.1. Collectors.toUnmodifiableList ()

Java 10 ha introdotto un modo conveniente per accumulare gli elementi Stream in un elenco non modificabile :

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

Se ora proviamo a modificare l' elenco dei risultati , otterremo un'eccezione UnsupportedOperationException :

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

3.2. Collectors.toSet ()

Il raccoglitore ToSet può essere utilizzato per raccogliere tutti gli elementi Stream in un'istanza Set . La cosa importante da ricordare è il fatto che non possiamo assumere nessuna particolare implementazione di Set con questo metodo. Se vogliamo avere un maggiore controllo su questo, possiamo invece usare toCollection .

Creiamo un'istanza Stream che rappresenta una sequenza di elementi e li raccogliamo in un'istanza Set :

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

Un set non contiene elementi duplicati. Se la nostra raccolta contiene elementi uguali tra loro, vengono visualizzati nel Set risultante solo una volta:

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

3.2.1. Collectors.toUnmodifiableSet ()

A partire da Java 10 possiamo creare facilmente un Set non modificabile usando il collector toUnmodifiableSet () :

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

Qualsiasi tentativo di modificare il set di risultati finirà con UnsupportedOperationException :

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

3.3. Collectors.toCollection ()

Come probabilmente avrai già notato, quando usi i raccoglitori toSet e toList , non puoi fare ipotesi sulle loro implementazioni. Se desideri utilizzare un'implementazione personalizzata, dovrai utilizzare il raccoglitore toCollection con una raccolta a tua scelta.

Creiamo un'istanza Stream che rappresenta una sequenza di elementi e li raccogliamo in un'istanza LinkedList :

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

Si noti che questo non funzionerà con nessuna raccolta immutabile. In tal caso, si avrebbe bisogno di scrivere o un costume di raccolta implementazione o utilizzare collectingAndThen .

3.4. Collezionisti . mappare()

Il raccoglitore ToMap può essere utilizzato per raccogliere elementi Stream in un'istanza Map . Per fare ciò, dobbiamo fornire due funzioni:

  • keyMapper
  • valueMapper

keyMapper verrà utilizzato per estrarre una chiave Map da un elemento Stream e valueMapper verrà utilizzato per estrarre un valore associato a una determinata chiave.

Raccogliamo questi elementi in una mappa che memorizza le stringhe come chiavi e le loro lunghezze come valori:

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

Function.identity () è solo una scorciatoia per definire una funzione che accetta e restituisce lo stesso valore.

What happens if our collection contains duplicate elements? Contrary to toSet, toMap doesn't silently filter duplicates. It's understandable – how should it figure out which value to pick for this key?

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

Note that toMap doesn't even evaluate whether the values are also equal. If it sees duplicate keys, it immediately throws an IllegalStateException.

In such cases with key collision, we should use toMap with another signature:

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

The third argument here is a BinaryOperator, where we can specify how we want collisions to be handled. In this case, we'll just pick any of these two colliding values because we know that the same strings will always have the same lengths, too.

3.4.1. Collectors.toUnmodifiableMap()

Similarly as for Lists and Sets, Java 10 introduced an easy way to collect Stream elements into an unmodifiable Map:

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

As we can see, if we try to put a new entry into a result Map, we'll get UnsupportedOperationException:

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

3.5. Collectors.collectingAndThen()

CollectingAndThen is a special collector that allows performing another action on a result straight after collecting ends.

Let's collect Stream elements to a List instance and then convert the result into an ImmutableList instance:

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

3.6. Collectors.joining()

Joining collector can be used for joining Stream elements.

We can join them together by doing:

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

which will result in:

"abbcccdd"

You can also specify custom separators, prefixes, postfixes:

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

which will result in:

"a bb ccc dd"

or you can write:

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

which will result in:

"PRE-a bb ccc dd-POST"

3.7. Collectors.counting()

Counting is a simple collector that allows simply counting of all Stream elements.

Now we can write:

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

Il metodo features () viene utilizzato per fornire a Stream alcune informazioni aggiuntive che verranno utilizzate per le ottimizzazioni interne. In questo caso, non prestiamo attenzione all'ordine degli elementi in un Set, quindi useremo Characteristics.UNORDERED . Per ottenere maggiori informazioni su questo argomento, di controllo Caratteristiche JavaDoc.

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

Ecco l'implementazione completa insieme all'utilizzo:

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

e qui in azione:

List givenList = Arrays.asList("a", "bb", "ccc", "dddd"); ImmutableSet result = givenList.stream() .collect(toImmutableSet());

5. conclusione

In questo articolo, abbiamo esplorato in modo approfondito i Collector di Java 8 e mostrato come implementarne uno. Assicurati di controllare uno dei miei progetti che migliora le capacità dell'elaborazione parallela in Java.

Tutti gli esempi di codice sono disponibili su GitHub. Puoi leggere articoli più interessanti sul mio sito.