CodeGym /Corsi /JAVA 25 SELF /Metodi groupingBy e partitioningBy (Collectors)

Metodi groupingBy e partitioningBy (Collectors)

JAVA 25 SELF
Livello 31, Lezione 2
Disponibile

1. Introduzione

Nella vita reale quasi sempre bisogna raggruppare i dati. Per esempio, dividere gli studenti per corso, suddividere i prodotti per categoria, separare i maggiorenni dai minori e così via.

Senza Stream API questi compiti si risolvono a mano: iterazione della collezione, verifica del criterio, aggiunta all’elenco giusto nella Map. Ecco un tipico codice «alla vecchia maniera»:

Map<String, List<Employee>> byDepartment = new HashMap<>();
for (Employee employee : employees) {
    String dept = employee.getDepartment();
    byDepartment.computeIfAbsent(dept, k -> new ArrayList<>()).add(employee);
}

Funziona, ma sembra di smistare a mano la pasta nei barattoli. E se serve raggruppare per più criteri? O anche calcolare somme per gruppo? Il codice cresce e diventa illeggibile.

Stream API e i collector dedicati (groupingBy, partitioningBy) permettono di farlo in una o due righe — e di sembrare un vero mago di Java.

2. Collector groupingBy: raggruppamento per criterio

Idea principale

groupingBy — è un collector che trasforma uno stream di elementi in una Map, in cui la chiave è il risultato della funzione-criterio e il valore è la lista di elementi che soddisfano quel criterio.

Firma:

Collectors.groupingBy(Function<T, K>)
  • T — tipo dell’elemento nello stream,
  • K — tipo della chiave (gruppo) restituita dalla funzione.

Esempio semplice: raggruppare i dipendenti per reparto

Supponiamo di avere una classe:

public class Employee {
    private final String name;
    private final String department;
    // ... costruttore e getter
    public Employee(String name, String department) {
        this.name = name;
        this.department = department;
    }
    public String getName() { return name; }
    public String getDepartment() { return department; }
}

E una collezione di dipendenti:

List<Employee> employees = List.of(
    new Employee("Alisa", "IT"),
    new Employee("Bob", "HR"),
    new Employee("Klara", "IT"),
    new Employee("Denis", "Finance"),
    new Employee("Eva", "HR")
);

Raggruppiamo per reparto:

Map<String, List<Employee>> byDepartment = employees.stream()
    .collect(Collectors.groupingBy(Employee::getDepartment));

Cosa abbiamo ottenuto?

  • Chiave: nome del reparto (String).
  • Valore: elenco dei dipendenti di quel reparto (List<Employee>).

Stampiamo il risultato:

byDepartment.forEach((dept, emps) -> {
    System.out.println(dept + ": " +
        emps.stream().map(Employee::getName).toList());
});

Output:

IT: [Alisa, Klara]
HR: [Bob, Eva]
Finance: [Denis]

Come funziona sotto il cofano?

Prima, per ogni elemento dello stream si calcola la chiave (ad esempio, getDepartment()). Se tale chiave è già presente nella Map, l’elemento viene aggiunto alla lista corrispondente. Se la chiave non c’è — si crea una nuova lista.

Analogia

Immaginate di smistare le lettere per cartelle: per ogni lettera verificate se esiste già una cartella con il nome giusto; se non c’è — ne create una nuova e ci mettete dentro la lettera.

3. Collector partitioningBy: suddivisione in due gruppi

A volte non serve tanto «raggruppare per valore», quanto dividere semplicemente la collezione in due parti in base a un criterio logico (true/false). Per esempio, dipendenti con stipendio superiore o inferiore a una certa soglia, studenti — in «promosso/non promosso».

Per questo esiste un collector specifico — partitioningBy.

Firma:

Collectors.partitioningBy(Predicate<T>)

Predicate — funzione che restituisce un valore booleano.

Esempio: suddivisione dei dipendenti per livello di stipendio

Supponiamo di avere:

public class Employee {
    private final String name;
    private final int salary;
    // ... costruttore e getter
    public Employee(String name, int salary) {
        this.name = name;
        this.salary = salary;
    }
    public String getName() { return name; }
    public int getSalary() { return salary; }
}

E un elenco:

List<Employee> employees = List.of(
    new Employee("Alisa", 120_000),
    new Employee("Bob", 80_000),
    new Employee("Klara", 150_000),
    new Employee("Denis", 95_000)
);

Dividiamo in «benestanti» e «modesti»:

Map<Boolean, List<Employee>> partitioned = employees.stream()
    .collect(Collectors.partitioningBy(e -> e.getSalary() > 100_000));
  • true — dipendenti con stipendio superiore a 100_000.
  • false — gli altri.

Stampiamo:

System.out.println("Benestanti: " +
    partitioned.get(true).stream().map(Employee::getName).toList());
System.out.println("Modesti: " +
    partitioned.get(false).stream().map(Employee::getName).toList());

Risultato:

Benestanti: [Alisa, Klara]
Modesti: [Bob, Denis]

Quando usare partitioningBy e quando groupingBy?

  • Se i gruppi sono più di due — usa groupingBy.
  • Se ci sono solo due gruppi basati su un booleano — usa partitioningBy: è più veloce e più chiaro.

4. Raggruppamenti annidati: raggruppare per più criteri

A volte si vuole raggruppare non solo per un criterio, ma anche «annidare» un raggruppamento dentro un altro. Per esempio, raggruppare i dipendenti prima per reparto e, all’interno del reparto, per posizione.

Esempio:

Supponiamo che nella nostra classe Employee ci sia anche il campo position:

public class Employee {
    private final String name;
    private final String department;
    private final String position;
    // ... costruttore e getter
}

Raggruppamento annidato:

Map<String, Map<String, List<Employee>>> byDeptAndPosition = employees.stream()
    .collect(Collectors.groupingBy(Employee::getDepartment,
        Collectors.groupingBy(Employee::getPosition)));
  • Chiave esterna — reparto.
  • Chiave interna — posizione.
  • Valore — elenco dei dipendenti.

Come accedere ai dipendenti del reparto IT con posizione "Developer"?

List<Employee> itDevs = byDeptAndPosition
    .getOrDefault("IT", Map.of())
    .getOrDefault("Developer", List.of());

Schema visivo

Map<Department, Map<Position, List<Employee>>>
└─ "IT"
    ├─ "Developer" -> [Alisa, Klara]
    └─ "QA"        -> [Boris]
└─ "HR"
    └─ "Recruiter" -> [Denis]

5. Esempi pratici di raggruppamento

Esempio 1: Raggruppare stringhe per lunghezza

List<String> words = List.of("cat", "dog", "elephant", "bee", "ant", "dolphin");

Map<Integer, List<String>> byLength = words.stream()
    .collect(Collectors.groupingBy(String::length));

byLength.forEach((len, ws) -> System.out.println(len + ": " + ws));

Output:

3: [cat, dog, bee, ant]
8: [elephant, dolphin]

Esempio 2: Raggruppare numeri per parità

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);

Map<String, List<Integer>> byParity = numbers.stream()
    .collect(Collectors.groupingBy(n -> n % 2 == 0 ? "even" : "odd"));

System.out.println(byParity);
// {odd=[1, 3, 5], even=[2, 4, 6]}

Esempio 3: Usare partitioningBy per le stringhe

Dividiamo le stringhe tra quelle che iniziano con "A" e tutte le altre:

List<String> names = List.of("Alice", "Bob", "Anna", "Charlie");

Map<Boolean, List<String>> byA = names.stream()
    .collect(Collectors.partitioningBy(s -> s.startsWith("A")));

System.out.println("A-names: " + byA.get(true));  // [Alice, Anna]
System.out.println("Other: " + byA.get(false));   // [Bob, Charlie]

5. Dettagli utili

Come usare il risultato del raggruppamento

Spesso dopo il raggruppamento non si vuole solo ottenere una Map, ma anche elaborarla:

  • Scorrere tutti i gruppi e stampare informazioni.
  • Trovare il gruppo con il numero massimo di elementi.
  • Per ogni gruppo calcolare, ad esempio, somma o media — ne parleremo nella prossima lezione.

Esempio: stampare il numero di dipendenti in ogni reparto

byDepartment.forEach((dept, emps) ->
    System.out.println(dept + ": " + emps.size() + " dipendenti"));

Confronto con un'implementazione manuale

Per fissare le idee: ecco come apparirebbe il raggruppamento per reparto «alla vecchia maniera»:

Map<String, List<Employee>> byDepartment = new HashMap<>();
for (Employee e : employees) {
    String dept = e.getDepartment();
    byDepartment.computeIfAbsent(dept, k -> new ArrayList<>()).add(e);
}

E come con Stream API:

Map<String, List<Employee>> byDepartment = employees.stream()
    .collect(Collectors.groupingBy(Employee::getDepartment));

Conclusione: meno codice, meno errori, maggiore leggibilità.

Trucchi

Se serve raggruppare non in una List ma, ad esempio, in un Set — usa come secondo parametro Collectors.toSet():

Collectors.groupingBy(Employee::getDepartment, Collectors.toSet())

Si può aggregare subito: ad esempio ottenere il numero di dipendenti in ogni reparto:

Collectors.groupingBy(Employee::getDepartment, Collectors.counting())

Ma di questo — nella prossima lezione!

Dopo partitioningBy ci saranno sempre due chiavi: true e false. Anche se uno dei gruppi è vuoto.

Dopo raggruppamenti annidati la struttura diventa un «albero»: Map dentro Map e così via.

6. Errori tipici nel raggruppamento e nel partitioning

Errore n. 1: tipo di risultato errato. I principianti spesso si aspettano che il risultato di groupingBy sia semplicemente una List<T>, e non una Map<K, List<T>>. Di conseguenza provano a chiamare metodi di lista e ottengono un errore di compilazione. Ricordate: il raggruppamento restituisce sempre una Map!

Errore n. 2: NullPointerException quando si accede a un gruppo inesistente. Se provate a ottenere la lista per una chiave inesistente — otterrete null. Usate getOrDefault(key, List.of()) oppure verificate la presenza della chiave con containsKey.

Errore n. 3: usare partitioningBy per problemi con più gruppi. partitioningBy — solo per due gruppi (true/false). Se i gruppi sono di più — usate groupingBy.

Errore n. 4: modificare le collezioni all’interno dello stream. Non cercate di modificare le collezioni di origine o la Map all’interno dello stream — porterà a errori imprevisti. Eseguite tutta l’elaborazione tramite Stream API e i collector.

Errore n. 5: struttura non ovvia dei raggruppamenti annidati. Dopo un groupingBy annidato il risultato è una Map dentro una Map. Non dimenticate di estrarre correttamente i dati (ad esempio con getOrDefault), altrimenti otterrete una ClassCastException.

1
Compito
JAVA 25 SELF, livello 31, lezione 2
Bloccato
Organizzazione del team nella società "Soluzioni Innovative" 🏢
Organizzazione del team nella società "Soluzioni Innovative" 🏢
1
Compito
JAVA 25 SELF, livello 31, lezione 2
Bloccato
Ordinamento della frutta esotica 🍎🍌
Ordinamento della frutta esotica 🍎🍌
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION