Collectors.groupingBy în Java

Împarte elementele în grupuri după o cheie (oraș, grupă, prima literă) și dă un Map cheie → listă sau cheie → rezultat.

Sintaxă
static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(Function<T, K> clasificare)
static <T, K, A, D> Collector<T, ?, Map<K, D>> groupingBy(Function<T, K> clasificare,
        Collector<T, A, D> downstream)
static <T, K, D, A, M extends Map<K, D>> Collector<T, ?, M> groupingBy(Function<T, K> clasificare,
        Supplier<M> fabricaMap, Collector<T, A, D> downstream)

Ce face Collectors.groupingBy

groupingBy(Student::oras) pune fiecare student în grupul orașului lui și returnează un Map<String, List<Student>>. Funcția dată se numește clasificare: ea spune în ce grup intră elementul.

Al doilea argument (colectorul downstream) spune ce se face cu fiecare grup în loc de o listă: counting() numără, averagingDouble(...) face media, mapping(...) păstrează doar un câmp, maxBy(...) alege maximul.

Map-ul returnat este un HashMap: ordinea cheilor la afișare nu este garantată. Pentru un rezultat stabil, folosește varianta cu trei argumente și dă TreeMap::new (chei sortate) sau învelește rezultatul în new TreeMap<>(...).

Parametri

Parametru Descriere
clasificare Funcția care dă cheia grupului pentru fiecare element.
fabricaMap Opțional: ce map se creează, de exemplu TreeMap::new.
downstream Opțional: colectorul aplicat fiecărui grup (implicit toList()).

Ce returnează

Un Collector care produce un Map de la cheia grupului la lista elementelor sau la rezultatul colectorului downstream.

Exemple

Exemplul 1: Studenții grupați pe oraș

Cu TreeMap::new orașele apar în ordine alfabetică. Ultimul argument, Collectors.toList(), este cel implicit, dar trebuie scris în varianta cu trei argumente.

import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;

public class Main {
    record Student(String nume, String oras) {}

    public static void main(String[] args) {
        List<Student> studenti = List.of(
                new Student("Ana", "Iași"), new Student("Radu", "Cluj"),
                new Student("Ioana", "Iași"), new Student("Dan", "Brașov"), new Student("Elena", "Cluj"));

        Map<String, List<Student>> peOras = studenti.stream()
                .collect(Collectors.groupingBy(Student::oras, TreeMap::new, Collectors.toList()));

        peOras.forEach((oras, lista) -> System.out.println(oras + ": " + lista.size() + " " + lista));
    }
}

Rezultat

Brașov: 1 [Student[nume=Dan, oras=Brașov]]
Cluj: 2 [Student[nume=Radu, oras=Cluj], Student[nume=Elena, oras=Cluj]]
Iași: 2 [Student[nume=Ana, oras=Iași], Student[nume=Ioana, oras=Iași]]

Exemplul 2: Grupare cu downstream: numele pe prima literă

mapping(...) păstrează din fiecare grup doar textul, iar new TreeMap<>(...) sortează literele.

import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<String> nume = List.of("Ana", "Andrei", "Bogdan", "Maria", "Mihai", "Alina");

        Map<Character, String> dupaLitera = new TreeMap<>(nume.stream()
                .collect(Collectors.groupingBy(n -> n.charAt(0), Collectors.joining(", "))));
        System.out.println(dupaLitera);

        Map<Integer, Long> dupaLungime = nume.stream()
                .collect(Collectors.groupingBy(String::length, TreeMap::new, Collectors.counting()));
        System.out.println("Câte nume au fiecare lungime: " + dupaLungime);
    }
}

Rezultat

{A=Ana, Andrei, Alina, B=Bogdan, M=Maria, Mihai}
Câte nume au fiecare lungime: {3=1, 5=3, 6=2}

Exemplul 3: Grupări pe două niveluri: județ și apoi gen

import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;

public class Main {
    record Persoana(String nume, String judet, char gen) {}

    public static void main(String[] args) {
        List<Persoana> lista = List.of(
                new Persoana("Ana", "Iași", 'F'), new Persoana("Radu", "Iași", 'M'),
                new Persoana("Ioana", "Iași", 'F'), new Persoana("Dan", "Cluj", 'M'));

        Map<String, Map<Character, Long>> rezultat = lista.stream()
                .collect(Collectors.groupingBy(Persoana::judet, TreeMap::new,
                        Collectors.groupingBy(Persoana::gen, TreeMap::new, Collectors.counting())));
        System.out.println(rezultat);
    }
}

Rezultat

{Cluj={M=1}, Iași={F=2, M=1}}

Greșeli frecvente

  • Afișezi direct map-ul din groupingBy și te miri că ordinea se schimbă de la un set de date la altul. Cere un TreeMap.
  • Funcția de clasificare returnează null pentru un element: groupingBy aruncă NullPointerException („element cannot be mapped to a null key”).
  • Scrii groupingBy(f, TreeMap::new) cu doar două argumente: nu compilează. Cu fabrica de map trebuie dat și colectorul downstream.

Comenzi înrudite

  • Collectors.partitioningBy: Împarte elementele în două grupuri, după o condiție: cele care o îndeplinesc (true) și celelalte (false).
  • Collectors.counting: Numără elementele; folosit mai ales în groupingBy, pentru „câte elemente are fiecare grup”.
  • Collectors.mapping: Transformă fiecare element înainte să-l dea altui colector; folosit în groupingBy ca să păstrezi doar un câmp.
  • Collectors.toMap: Construiește un Map din stream: o funcție dă cheia, alta valoarea; o a treia spune ce faci când cheia se repetă.
  • Collectors: Clasa cu rețete gata făcute pentru collect: liste, mulțimi, map-uri, texte unite, grupări, numărări și medii.