Guida al raggruppamento Java 8 di Collector

1. Introduzione

In questo tutorial vedremo come funziona il collector groupingBy utilizzando vari esempi.

Per comprendere il materiale trattato in questo tutorial, avremo bisogno di una conoscenza di base delle funzionalità di Java 8. Possiamo dare un'occhiata all'introduzione a Java 8 Streams e alla guida ai raccoglitori di Java 8 per queste nozioni di base.

2. groupingBy Collectors

L' API Java 8 Stream ci consente di elaborare raccolte di dati in modo dichiarativo.

I metodi factory statici Collectors.groupingBy () e Collectors.groupingByConcurrent () ci forniscono funzionalità simili alla clausola " GROUP BY" nel linguaggio SQL. Li usiamo per raggruppare gli oggetti in base a una proprietà e memorizzare i risultati in un'istanza di Map .

I metodi sovraccaricati di groupingBy sono:

  • Innanzitutto, con una funzione di classificazione come parametro del metodo:

static  Collector
    
     > groupingBy(Function classifier)
    
  • In secondo luogo, con una funzione di classificazione e un secondo raccoglitore come parametri del metodo:

static  Collector
    
      groupingBy(Function classifier, Collector downstream)
    
  • Infine, con una funzione di classificazione, un metodo del fornitore (che fornisce l' implementazione della mappa che contiene il risultato finale) e un secondo raccoglitore come parametri del metodo:

static 
    
      Collector groupingBy(Function classifier, Supplier mapFactory, Collector downstream)
    

2.1. Esempio di configurazione del codice

Per dimostrare l'uso di groupingBy () , definiamo una classe BlogPost (useremo un flusso di oggetti BlogPost ):

class BlogPost { String title; String author; BlogPostType type; int likes; } 

Successivamente, BlogPostType :

enum BlogPostType { NEWS, REVIEW, GUIDE } 

Quindi l' elenco degli oggetti BlogPost :

List posts = Arrays.asList( ... );

Definiamo anche una classe Tuple che verrà utilizzata per raggruppare i post in base alla combinazione del loro tipo e attributi dell'autore :

class Tuple { BlogPostType type; String author; } 

2.2. Raggruppamento semplice da una singola colonna

Cominciamo con il metodo groupingBy più semplice , che prende solo una funzione di classificazione come parametro. A ogni elemento del flusso viene applicata una funzione di classificazione. Usiamo il valore restituito dalla funzione come chiave per la mappa che otteniamo dal raccoglitore groupingBy .

Per raggruppare i post del blog nell'elenco dei post del blog in base al tipo :

Map
    
      postsPerType = posts.stream() .collect(groupingBy(BlogPost::getType)); 
    

2.3. groupingBy con un tipo di chiave mappa complessa

La funzione di classificazione non si limita a restituire solo un valore scalare o String. La chiave della mappa risultante potrebbe essere qualsiasi oggetto purché ci assicuriamo di implementare i metodi equals e hashcode necessari .

Per raggruppare i post del blog nell'elenco in base al tipo e all'autore combinati in un'istanza di Tuple :

Map
    
      postsPerTypeAndAuthor = posts.stream() .collect(groupingBy(post -> new Tuple(post.getType(), post.getAuthor()))); 
    

2.4. Modifica del tipo di valore mappa restituito

Il secondo sovraccarico di groupingBy richiede un secondo collettore aggiuntivo (collettore a valle) che viene applicato ai risultati del primo collettore.

Quando specifichiamo una funzione di classificazione, ma non un raccoglitore a valle, il raccoglitore toList () viene utilizzato dietro le quinte.

Usiamo il raccoglitore toSet () come raccoglitore a valle e otteniamo un set di post del blog (invece di un elenco ):

Map
    
      postsPerType = posts.stream() .collect(groupingBy(BlogPost::getType, toSet())); 
    

2.5. Raggruppamento per più campi

Una diversa applicazione del raccoglitore a valle è fare un raggruppamento secondario By ai risultati del primo gruppo di.

Per raggruppare l' Elenco dei BlogPost prima per autore e poi per tipo :

Map
    
      map = posts.stream() .collect(groupingBy(BlogPost::getAuthor, groupingBy(BlogPost::getType)));
    

2.6. Ottenere la media dai risultati raggruppati

Utilizzando il raccoglitore a valle, possiamo applicare le funzioni di aggregazione nei risultati della funzione di classificazione.

Ad esempio, per trovare il numero medio di Mi piace per ogni tipo di post sul blog :

Map averageLikesPerType = posts.stream() .collect(groupingBy(BlogPost::getType, averagingInt(BlogPost::getLikes))); 

2.7. Ottenere la somma dai risultati raggruppati

Per calcolare la somma totale dei Mi piace per ogni tipo :

Map likesPerType = posts.stream() .collect(groupingBy(BlogPost::getType, summingInt(BlogPost::getLikes))); 

2.8. Ottenere il massimo o il minimo dai risultati raggruppati

Another aggregation that we can perform is to get the blog post with the maximum number of likes:

Map
    
      maxLikesPerPostType = posts.stream() .collect(groupingBy(BlogPost::getType, maxBy(comparingInt(BlogPost::getLikes)))); 
    

Similarly, we can apply the minBy downstream collector to get the blog post with the minimum number of likes.

Note that the maxBy and minBy collectors take into account the possibility that the collection to which they are applied could be empty. This is why the value type in the map is Optional.

2.9. Getting a Summary for an Attribute of Grouped Results

The Collectors API offers a summarizing collector that we can use in cases when we need to calculate the count, sum, minimum, maximum and average of a numerical attribute at the same time.

Let's calculate a summary for the likes attribute of the blog posts for each different type:

Map likeStatisticsPerType = posts.stream() .collect(groupingBy(BlogPost::getType, summarizingInt(BlogPost::getLikes))); 

The IntSummaryStatistics object for each type contains the count, sum, average, min and max values for the likes attribute. Additional summary objects exist for double and long values.

2.10. Mapping Grouped Results to a Different Type

We can achieve more complex aggregations by applying a mapping downstream collector to the results of the classification function.

Let's get a concatenation of the titles of the posts for each blog post type:

Map postsPerType = posts.stream() .collect(groupingBy(BlogPost::getType, mapping(BlogPost::getTitle, joining(", ", "Post titles: [", "]")))); 

What we have done here is to map each BlogPost instance to its title and then reduce the stream of post titles to a concatenated String. In this example, the type of the Map value is also different from the default List type.

2.11. Modifying the Return Map Type

When using the groupingBy collector, we cannot make assumptions about the type of the returned Map. If we want to be specific about which type of Map we want to get from the group by, then we can use the third variation of the groupingBy method that allows us to change the type of the Map by passing a Map supplier function.

Let's retrieve an EnumMap by passing an EnumMap supplier function to the groupingBy method:

EnumMap
    
      postsPerType = posts.stream() .collect(groupingBy(BlogPost::getType, () -> new EnumMap(BlogPostType.class), toList())); 
    

3. Concurrent groupingBy Collector

Similar to groupingBy is the groupingByConcurrent collector, which leverages multi-core architectures. This collector has three overloaded methods that take exactly the same arguments as the respective overloaded methods of the groupingBy collector. The return type of the groupingByConcurrent collector, however, must be an instance of the ConcurrentHashMap class or a subclass of it.

To do a grouping operation concurrently, the stream needs to be parallel:

ConcurrentMap
    
      postsPerType = posts.parallelStream() .collect(groupingByConcurrent(BlogPost::getType)); 
    

If we choose to pass a Map supplier function to the groupingByConcurrent collector, then we need to make sure that the function returns either a ConcurrentHashMap or a subclass of it.

4. Java 9 Additions

Java 9 ha introdotto due nuovi collector che funzionano bene con groupingBy ; maggiori informazioni su di loro possono essere trovate qui.

5. conclusione

In questo articolo, abbiamo esplorato l'utilizzo del collector groupingBy offerto dall'API di Java 8 Collectors .

Abbiamo appreso come groupingBy possa essere utilizzato per classificare un flusso di elementi in base a uno dei loro attributi e come i risultati di questa classificazione possano essere ulteriormente raccolti, modificati e ridotti a contenitori finali.

L'implementazione completa degli esempi in questo articolo può essere trovata nel progetto GitHub.