Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Stream API

Volem els noms que comencen per A, en majúscules. Amb un bucle:

List<String> noms = List.of("Aina", "Bernat", "Carla", "Aleix");

List<String> resultat = new ArrayList<>();
for (String nom : noms) {
    if (nom.startsWith("A")) {
        resultat.add(nom.toUpperCase());
    }
}

Amb un stream:

List<String> resultat = noms.stream()
        .filter(nom -> nom.startsWith("A"))
        .map(String::toUpperCase)
        .toList();

Fan exactament el mateix. La diferència és que el bucle explica com obtenir el resultat (crea una llista, recorre, comprova, afegeix) i l’stream diu què volem (filtra, transforma, recull). Amb una operació no compensa; amb quatre o cinc encadenades, la versió amb bucles s’omple de variables temporals i if niats.

Què és un stream

Un Stream no és una col·lecció. No guarda dades: és una canonada d’operacions sobre una font que sí que en té. Tres conseqüències pràctiques:

  • No modifica la font. La llista noms de l’exemple continua igual després del filtre.
  • És d’un sol ús. Un cop executada l’operació final, l’stream està consumit i reutilitzar-lo llança IllegalStateException. Se’n crea un de nou cada vegada.
  • És mandrós (lazy). Les operacions intermèdies no fan res quan les escrius:
noms.stream().map(nom -> {
    System.out.println("processant " + nom);
    return nom.toUpperCase();
});
// no imprimeix res: falta l'operació terminal

Res no s’executa fins que arriba l’operació final. Aleshores els elements passen per tota la canonada d’un en un, no operació per operació sobre tota la col·lecció.

La canonada

Una canonada té sempre tres parts:

  1. Una font: col.stream(), Arrays.stream(array), Stream.of("a", "b"), IntStream.range(0, 10), Files.lines(path).
  2. Zero o més operacions intermèdies, que retornen un altre Stream i per tant es poden encadenar.
  3. Una operació terminal, que produeix el resultat i tanca l’stream.

Operacions intermèdies

OperacióQuè fa
filter(Predicate)Deixa passar només els elements que compleixen la condició
map(Function)Transforma cada element, possiblement canviant de tipus
flatMap(Function)Transforma cada element en un stream i n’ajunta el contingut en un de sol
distinct()Elimina repeticions, segons equals()
sorted(), sorted(Comparator)Ordena per ordre natural o pel comparador
limit(n), skip(n)Es queda amb els primers n, o se’ls salta

flatMap() és la que costa més de veure. Serveix quan cada element amaga una col·lecció i vols treballar amb els elements de dins:

List<List<String>> grups = List.of(List.of("Aina", "Bernat"), List.of("Carla"));

grups.stream().flatMap(List::stream).toList();   // [Aina, Bernat, Carla]

Operacions terminals

OperacióRetorna
toList()Una llista immutable amb el resultat
collect(Collector)El que digui el col·lector: un Set, un Map, un String
forEach(Consumer)Res: executa una acció per element
count()Quants elements queden
reduce(BinaryOperator)Un únic valor, combinant els elements de dos en dos
anyMatch(), allMatch(), noneMatch()Un booleà, sense recórrer més del necessari
findFirst(), min(), max()Un Optional, perquè l’stream pot ser buit

toList() és la forma curta i la que hauries de fer servir per defecte. Trobaràs molt de codi amb collect(Collectors.toList()), que és l’equivalent anterior a Java 16 i retorna una llista modificable.

Les operacions que poden no trobar cap element retornen un Optional, un embolcall que obliga a decidir què passa si no hi ha resultat:

String primer = noms.stream()
        .filter(nom -> nom.startsWith("Z"))
        .findFirst()
        .orElse("cap");

Col·lectors

collect() accepta un Collector, i la classe Collectors en té de fets per als casos habituals. Amb un record com a exemple:

record Alumne(String nom, String grup, double nota) {}

List<Alumne> alumnes = List.of(
        new Alumne("Aina", "DAM1", 8.5),
        new Alumne("Bernat", "DAM1", 4.0),
        new Alumne("Carla", "DAM2", 7.0));

Recollir en un conjunt, o ajuntar en un text:

Set<String> grups = alumnes.stream().map(Alumne::grup).collect(Collectors.toSet());
// [DAM1, DAM2]

String llista = alumnes.stream().map(Alumne::nom).collect(Collectors.joining(", "));
// "Aina, Bernat, Carla"

Agrupar és el que fa que l’API valgui la pena. groupingBy() construeix un Map amb la clau que li diguis:

Map<String, List<Alumne>> perGrup =
        alumnes.stream().collect(Collectors.groupingBy(Alumne::grup));

I el segon paràmetre diu què fer amb cada grup en lloc de deixar-hi la llista sencera:

Map<String, Long> quantsPerGrup =
        alumnes.stream().collect(Collectors.groupingBy(Alumne::grup, Collectors.counting()));
// {DAM1=2, DAM2=1}

Map<String, Double> mitjanaPerGrup =
        alumnes.stream().collect(Collectors.groupingBy(Alumne::grup,
                                 Collectors.averagingDouble(Alumne::nota)));
// {DAM1=6.25, DAM2=7.0}

Escriure això amb bucles vol dir crear el mapa, comprovar si la clau hi és, crear la llista si no hi és, acumular i dividir. És la comparació que justifica l’API.

Streams de primitius

Stream<Integer> treballa amb objectes, i cada operació fa autoboxing. Per a números hi ha IntStream, LongStream i DoubleStream, que eviten l’embolcall i afegeixen operacions aritmètiques:

double mitjana = alumnes.stream()
        .mapToDouble(Alumne::nota)
        .average()
        .orElse(0);

int suma = IntStream.rangeClosed(1, 100).sum();   // 5050

mapToInt(), mapToLong() i mapToDouble() hi entren; boxed() en surt.

Quan no fer-los servir

Un stream no és sempre millor que un bucle. Si només has de recórrer una col·lecció i fer una cosa amb cada element, el for és més curt i més fàcil de depurar.

Sobretot, evita els efectes secundaris dins de les lambdes: modificar una variable de fora, escriure en una llista externa o llançar excepcions comprovades. Una canonada hauria de rebre dades i retornar dades. Si el que necessites és modificar coses, un bucle ho diu més clarament.

Referències

Last change: , commit: 87dfa51