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
nomsde 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:
- Una font:
col.stream(),Arrays.stream(array),Stream.of("a", "b"),IntStream.range(0, 10),Files.lines(path). - Zero o més operacions intermèdies, que retornen un altre
Streami per tant es poden encadenar. - 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.