flowchart LR
F["Font<br/>List.stream()"] --> I1["filter()<br/>intermèdia"]
I1 --> I2["map()<br/>intermèdia"]
I2 --> I3["sorted()<br/>intermèdia"]
I3 --> T["collect()<br/>TERMINAL"]
T --> R["Resultat"]
16 Operacions agregades: lambda i streams
Objectius
- Escriure expressions lambda i entendre-les com “funcions curtes sense nom”.
- Fer servir referències a mètode (
Classe::metode) quan simplifiquen. - Convertir una col·lecció en un Stream i encadenar operacions.
- Aplicar les operacions clau: filter, map, reduce i collect.
- Decidir quan un stream és més clar que un bucle, i quan no.
Hem treballat amb llistes, conjunts i mapes. Ara aprendrem a processar-les d’una manera nova: en comptes de dir pas a pas com recórrer-les (estil imperatiu, amb bucles), diràs què vols obtenir (estil declaratiu). Aquesta és la programació funcional que Java va incorporar a la versió 8 i que avui és omnipresent.
16.1 Expressions lambda
Una expressió lambda és un tros de codi que representa una funció curta i anònima: rep uns paràmetres i produeix un resultat, però no li poses nom ni la declares com un mètode complet. La sintaxi és:
(paràmetres) -> cos
Serveix per passar comportament com si fos un valor. Fins ara passaves dades a un mètode (números, textos); amb les lambda pots passar accions.
Exemple:
// Una interfície funcional: un sol mètode abstracte
Runnable saluda = () -> System.out.println("Hola!");
saluda.run(); // Hola!
// Amb paràmetres (la interfície Comparator del capítol anterior)
Comparator<String> perLongitud = (a, b) -> a.length() - b.length();
// Amb un paràmetre, cos amb retorn implícit
Function<Integer, Integer> doble = x -> x * 2;
System.out.println(doble.apply(5)); // 10Una lambda només funciona on s’espera una interfície funcional: una interfície amb un sol mètode abstracte (com Comparator, Runnable, Predicate, Function). La lambda “omple” aquest mètode.
- Sense paràmetres:
() -> ... - Un paràmetre:
x -> ...(parèntesis opcionals) - Diversos:
(a, b) -> ... - Cos d’una expressió:
x -> x * 2(retorn implícit) - Cos de bloc:
x -> { int r = x * 2; return r; }(returnexplícit)
return i claus
Function<Integer, Integer> doble = x -> { x * 2; }; // ERRORSi obres claus { }, el cos és un bloc i necessites return i ;: x -> { return x * 2; }. Si no poses claus, és una expressió i el retorn és implícit: x -> x * 2. No barregis les dues coses.
Escriu una lambda de tipus Predicate<String> que digui si un text té més de 5 caràcters. Prova-la amb .test("programació").
16.2 Referències a mètode
Sovint una lambda només crida un mètode que ja existeix: x -> x.toUpperCase().
En aquests casos pots escriure una referència a mètode, més curta i llegible, amb ::.
Exemple:
// Lambda → Referència a mètode equivalent
x -> x.toUpperCase() String::toUpperCase
x -> System.out.println(x) System.out::println
(a, b) -> a.compareTo(b) String::compareTo
n -> new Llibre(n) Llibre::new // ConstructorList<String> noms = List.of("marta", "pau", "aina");
noms.forEach(System.out::println); // En comptes de n -> System.out.println(n)
noms.stream().map(String::toUpperCase); // En comptes de n -> n.toUpperCase()Fes servir la referència a mètode quan la lambda no fa res més que cridar aquell mètode. Si la lambda fa càlculs o passa arguments en un altre ordre, deixa-la com a lambda: serà més clara.
16.3 Què és un Stream?
Un Stream és una seqüència de dades que flueix per una cadena d’operacions. No és una col·lecció que guarda dades: és una canonada de processament. Agafes una font (una List, per exemple), hi apliques passos (filtrar, transformar…) i en surt un resultat.
Un stream té tres parts:
- Font: d’on surten les dades (
llista.stream()). - Operacions intermèdies: transformen el flux i en tornen un altre stream (
filter,map,sorted). Són mandroses: no fan res fins que… - Operació terminal — dispara tot el procés i produeix un resultat (
collect,count,forEach,reduce).
Un stream és una cadena de muntatge d’una fàbrica. Les peces (dades) entren per un extrem i passen per estacions: una les inspecciona i descarta les defectuoses (filter), una altra les pinta (map), una altra les ordena (sorted). Al final de la cinta, s’empaqueten en una caixa (collect).
Mentre no hi ha caixa esperant al final (operació terminal), la cadena no s’engega.
Exemple:
List<String> noms = List.of("Marta", "pau", "Aina", "joan");
List<String> resultat = noms.stream() // 1. Font
.filter(n -> n.length() > 3) // 2. Intermèdia: llargs
.map(String::toUpperCase) // 2. Intermèdia: majúscules
.sorted() // 2. Intermèdia: ordena
.collect(Collectors.toList()); // 3. Terminal: recull en llista
System.out.println(resultat); // [AINA, JOAN, MARTA]Sense operació terminal, les operacions intermèdies no s’executen mai (són mandroses). Cal tancar la cadena amb collect, forEach, count…
Un altre error: reutilitzar un stream ja consumit llança IllegalStateException. Un stream és d’un sol ús.
noms.stream().filter(n -> n.length() > 3).map(String::toUpperCase); // no fa RES!Amb List.of(4, 15, 8, 23, 42), escriu un stream que es quedi només amb els números més grans que 10 i els mostri per pantalla amb forEach.
16.4 Operacions principals
Les operacions principals són:
filter: es queda amb els compleixen una condició.map: transforma tots els elements implicats.reduce: ho combinar tot en un sol valor.collect: recull el resultat.
Operació filter
filter rep un Predicate (una condició) i deixa passar només els elements que la compleixen.
List<Integer> edats = List.of(15, 22, 17, 40, 19);
List<Integer> majors = edats.stream()
.filter(e -> e >= 18) // Condició
.collect(Collectors.toList());
System.out.println(majors); // [22, 40, 19]Operació map
map rep una Function i transforma cada element en un altre (potser d’un altre tipus). El nombre d’elements no canvia; el seu contingut o tipus, sí.
List<String> noms = List.of("marta", "pau", "aina");
List<Integer> longituds = noms.stream()
.map(String::length) // String → Integer
.collect(Collectors.toList());
System.out.println(longituds); // [5, 3, 4]Operació reduce
reduce agrega tots els elements en un de sol (una suma, un producte, el màxim…) combinant-los de dos en dos.
List<Integer> preus = List.of(10, 25, 5, 40);
int total = preus.stream()
.reduce(0, (a, b) -> a + b); // Valor inicial 0, suma acumulada
System.out.println(total); // 80
// Per a sumes hi ha una drecera encara més clara:
int total2 = preus.stream().mapToInt(Integer::intValue).sum();Operació collect
collect és l’operació terminal més versàtil: empaqueta el flux en una estructura. Amb la classe Collectors pots recollir en llista, conjunt, mapa, unir textos o agrupar.
List<String> paraules = List.of("gat", "gos", "girafa", "os", "elefant");
// A llista / conjunt
List<String> ambG = paraules.stream()
.filter(p -> p.startsWith("g"))
.collect(Collectors.toList());
// Unir en un text
String frase = paraules.stream()
.collect(Collectors.joining(", ", "[", "]")); // [gat, gos, girafa, os, elefant]
// Agrupar per longitud → Map<Integer, List<String>>
Map<Integer, List<String>> perLongitud = paraules.stream()
.collect(Collectors.groupingBy(String::length));
System.out.println(perLongitud); // {2=[os], 3=[gat, gos], 6=[girafa], 7=[elefant]}Collectors.toList() vs .toList()
Des de Java 16 pots tancar amb .toList() directament (més curt), que retorna una llista immutable.
Collectors.toList() retorna una llista mutable. Si després l’has de modificar, fes servir Collectors.toList() o Collectors.toCollection(ArrayList::new).
Exemple:
record Producte(String nom, String categoria, double preu) {}
List<Producte> cistella = List.of(
new Producte("Poma", "fruita", 0.50),
new Producte("Llet", "làctic", 1.20),
new Producte("Formatge", "làctic", 4.80),
new Producte("Plàtan", "fruita", 0.35)
);
// Preu total dels làctics
double totalLactics = cistella.stream()
.filter(p -> p.categoria().equals("làctic"))
.mapToDouble(Producte::preu)
.sum();
System.out.println("Làctics: " + totalLactics + " €"); // 6.0 €
// Noms de productes de menys d'1 €, en majúscules i ordenats
List<String> barats = cistella.stream()
.filter(p -> p.preu() < 1.0)
.map(p -> p.nom().toUpperCase())
.sorted()
.toList();
System.out.println(barats); // [PLÀTAN, POMA]Amb la llista cistella, agrupa els productes per categoria i calcula el preu mitjà de cada categoria.
Pista: Collectors.groupingBy(Producte::categoria, Collectors.averagingDouble(Producte::preu)).
16.5 Bucle o stream?
Els streams no substitueixen els bucles: conviuen. Tria segons la claredat.
| Aspecte | Bucle for / for-each |
Stream |
|---|---|---|
| Estil | Imperatiu (com fer-ho) | Declaratiu (què vull) |
| Llegibilitat en cadenes filtrar+transformar | Verbós | Molt clar |
| Modificar variables externes | Fàcil | Desaconsellat (efectes secundaris) |
Sortir a mig recorregut (break) |
Natural | Difícil (cal findFirst, anyMatch) |
| Depuració pas a pas | Còmoda | Menys directa |
| Lògica simple / un sol pas | Sovint més clar | Pot sobrar |
Fes servir streams quan encadenes diverses transformacions sobre una col·lecció (filtrar + transformar + agrupar) i vols codi net i sense variables temporals.
Fes servir bucles quan hi ha lògica complexa, sortides anticipades (break) o quan un stream et quedaria enrevessat.
Si un stream necessita moltes lambdes de bloc amb { }, probablement un bucle seria més clar.
Modificar una llista externa des d’un stream desvirtua tot el sentit funcional (i falla en paral·lel). El correcte és recollir el resultat: List<String> sortida = noms.stream().map(String::toUpperCase).toList();
List<String> sortida = new ArrayList<>();
noms.stream().forEach(n -> sortida.add(n.toUpperCase())); // Funciona, però és mal estilPer treballar amb primitius sense el cost de l’autoboxing, hi ha IntStream, LongStream i DoubleStream, amb mètodes com sum(), average(), max(), range():
int suma = IntStream.rangeClosed(1, 100).sum(); // 505016.6 Resum
- Una lambda
(params) -> cosés una funció curta i anònima que representa un comportament; només encaixa on s’espera una interfície funcional (un sol mètode abstracte). - Les referències a mètode (
Classe::metode) són la forma curta d’una lambda que només crida un mètode existent. - Un Stream és una canonada de processament: font → operacions intermèdies (peresoses) → operació terminal. Sense terminal, no s’executa res.
filter: selecciona.map: transforma.reduce: agrega en un sol valor.collect: recull el resultat (llista, mapa, text, agrupació…).
- Els streams brillen en cadenes de transformacions; els bucles guanyen amb lògica complexa o sortides anticipades. Controla els efectes secundaris dins dels streams.
16.7 Per practicar
- Filtratge bàsic. Donada una
List<Integer>, obtén amb un stream la llista dels parells, elevats al quadrat i ordenats de gran a petit. - Estadístiques de notes. Amb una
List<Double>de notes, calcula amb streams: la mitjana, la nota màxima, quantes són aprovades (≥ 5) i el percentatge d’aprovats. - De llista a mapa. Donada una
List<Soci>(nom, DNI), construeix unMap<String, Soci>indexat per DNI ambCollectors.toMap. - Agrupacions. Amb la llista
cistellade l’exemple, obtén: (a) productes agrupats per categoria, (b) recompte per categoria, (c) el producte més car de cada categoria. - Refactor. Agafa un bucle
ford’algun exercici anterior i reescriu-lo amb streams. Després decideix, amb arguments, quina versió és més clara.