Aquesta és la lliçó que el curs porta esperant des del mòdul 4.
Quan vas aprendre les expressions lambda a 04-05, et vam dir que el seu veritable destí era més endavant. Quan vas dominar Function, Predicate, Consumer i Supplier a 04-06, et vam dir el mateix. Al mòdul 5, cada informe de BiblioTech va acabar essent un bucle imbricat amb acumuladors i marcadors. A 06-07, el patró "objecte resultat" va quedar a mitges perquè faltava Optional. A 07-06, Files.lines() i Files.walk() van aparèixer i només els vas poder fer servir amb forEach. A 08-05 vas esmentar parallelStream() i el vas ajornar. Al mòdul 9, EnriquidorCataleg va haver d'esquivar stream() durant sis lliçons senceres.
Tot això convergeix aquí.
Java 8, publicat el març del 2014, va ser el canvi més gran de la història del llenguatge. Java portava des del 1995 essent estrictament imperatiu i orientat a objectes: per processar una col·lecció, declaraves una variable, escrivies un bucle i acumulaves. Java 8 hi va afegir un model declaratiu i funcional en què descrius què vols obtenir i la biblioteca decideix com recórrer-ho. No va substituir el que hi havia —els bucles hi continuen essent i continuen essent correctes—, però va canviar l'estil per defecte de tot l'ecosistema.
I va afegir Optional, una classe minúscula amb un propòsit: fer impossible que t'oblidis de comprovar l'absència d'un valor. El null que retorna cercarPerReferencia des del mòdul 6, i que setze llocs diferents han de recordar comprovar, té els dies comptats.
En acabar, BiblioTech tindrà el seu informe complet per tipus de material i per empleat en tres línies, els seus recorreguts de fitxers processats mandrosament sense carregar res a memòria, i ni un sol null significant "no trobat".
Contingut
- Què va portar Java 8 i què ja has vist
- Què és un stream i què NO és
- La canonada: font, operacions intermèdies, operació terminal
- Avaluació mandrosa: l'ordre real d'execució
- Fonts de streams
- Operacions intermèdies
flatMap: el cas que costatakeWhileidropWhile- Operacions terminals
reduceen les seves tres formes- Els
Collectorsbàsics groupingByipartitioningBy- Streams de primitius
- Streams paral·lels
- L'informe de BiblioTech: de trenta línies a tres
Optional: per a què es va crear- Crear i consumir un
Optional orElseenfront d'orElseGet- Els antipatrons d'
Optional - Què retornar quan no hi ha valor
- BiblioTech refactoritzat
- Errors Comuns i Consells
- Exercicis
- Què va portar Java 8 i què ja has vist
Java 8 va introduir un paquet de característiques que se sostenen les unes a les altres. Bona part ja les coneixes, i convé veure el mapa complet abans de començar:
| Característica | Què és | On la vas veure |
|---|---|---|
| Expressions lambda | Funcions anònimes: x -> x * 2 |
04-05 |
| Interfícies funcionals | Interfícies amb un sol mètode abstracte | 04-06 |
java.util.function |
Function, Predicate, Consumer, Supplier, BiFunction |
04-06 |
| Referències a mètodes | Llibre::getTitol, System.out::println |
04-06 |
Mètodes default |
Implementacions a interfícies sense trencar ningú | 04-01 |
Mètodes static a interfícies |
Fàbriques al costat del contracte | 04-01 |
Comparator fluid |
comparing, thenComparing, reversed |
05-09 |
Map millorat |
computeIfAbsent, merge, getOrDefault, forEach |
05-05 |
CompletableFuture |
Composició asíncrona | 08-07 |
java.time |
API de dates nova | 10-05 |
| Streams | Processament declaratiu de seqüències | Aquesta lliçó |
Optional |
Contenidor de "potser hi ha valor" | Aquesta lliçó |
Tot el de les files 1 a 4 és requisit per a aquesta lliçó, i per això arriba ara i no abans. Un stream és una canonada d'operacions a les quals se'ls passen lambdes i referències a mètodes; sense dominar això, map(Llibre::getTitol) seria soroll.
El motiu de fons de tot el paquet va ser molt concret: els processadors van deixar de ser més ràpids i van començar a ser més nombrosos. Aprofitar vuit nuclis amb bucles for exigeix escriure la partició, els fils i la recol·lecció a mà —el que vas fer al mòdul 8 i saps què costa—. Amb una descripció declarativa del que vols, la biblioteca pot paral·lelitzar-ho canviant una paraula. Això és parallelStream(), i hi arribarem amb les cauteles que mereix.
- Què és un stream i què NO és
Un stream és una seqüència d'elements que admet operacions agregades. I la millor manera d'entendre'l és per contrast, perquè el nom convida a confondre'l amb InputStream (07-03), amb el qual no té absolutament res a veure.
Un Stream no... |
Explicació |
|---|---|
| ...és una col·lecció | No és una estructura de dades: és una vista de procés sobre una font |
| ...emmagatzema elements | No ocupa memòria proporcional a la mida; els elements hi flueixen a través |
| ...modifica la font | filter no esborra res del List original |
| ...es pot reutilitzar | És d'un sol ús: consumit, mor |
| ...té índexs | No hi ha get(3); l'accés és seqüencial |
...és un InputStream |
No té res a veure amb l'E/S de bytes de 07-03 |
I el que sí que és:
- Una descripció d'un càlcul sobre una seqüència, que no s'executa fins que es demana el resultat.
- Mandrós: no passa res fins a l'operació terminal.
- Potencialment infinit:
Stream.iterategenera elements sense fi, ilimitels acota. - Paral·lelitzable canviant
stream()perparallelStream().
La propietat de l'únic ús és la que més sorprèn:
List<Llibre> cataleg = List.of(
new Llibre("978-0000000001", "Java Eficac", 412, true),
new Llibre("978-0000000002", "Patrons de Disseny", 395, false));
Stream<Llibre> flux = cataleg.stream();
long quants = flux.count(); // primera operacio terminal: OK
List<Llibre> llista = flux.toList(); // segona: EXPLOTAException in thread "main" java.lang.IllegalStateException:
stream has already been operated upon or closed
at java.base/java.util.stream.AbstractPipeline.<init>(AbstractPipeline.java:203)Un stream es consumeix una vegada. Si necessites dos recorreguts, crea dos streams des de la font:
Per això no es guarda un Stream en un camp ni es passa per aquí i per allà: es crea, es fa servir i es descarta, normalment en una sola expressió.
I el contrast que resumeix la lliçó. El mateix problema —títols dels llibres de més de 400 pàgines que estan prestats, ordenats alfabèticament— resolt de les dues maneres:
// ABANS (modul 5): imperatiu. Descriu COM.
List<String> resultat = new ArrayList<>();
for (Llibre llibre : cataleg) {
if (llibre.getPagines() > 400 && llibre.estaPrestat()) {
resultat.add(llibre.getTitol());
}
}
Collections.sort(resultat);// ARA: declaratiu. Descriu QUE.
List<String> resultat = cataleg.stream()
.filter(llibre -> llibre.getPagines() > 400)
.filter(Llibre::estaPrestat)
.map(Llibre::getTitol)
.sorted()
.toList();Les diferències no són només de línies:
- No hi ha variable acumuladora ni estat mutable a gestionar.
- Es llegeix de dalt a baix com una frase: dels llibres, els de més de 400 pàgines, els prestats, els seus títols, ordenats.
- No hi ha índexs ni condicions de contorn a equivocar.
- Canviar a paral·lel és una paraula.
- La canonada: font, operacions intermèdies, operació terminal
Tot stream té exactament tres parts:
graph LR
F["FONT<br/>colleccio, array,<br/>fitxer, generador"] --> I1["INTERMEDIA<br/>filter"]
I1 --> I2["INTERMEDIA<br/>map"]
I2 --> I3["INTERMEDIA<br/>sorted"]
I3 --> T["TERMINAL<br/>collect, forEach,<br/>count, reduce"]
T --> R["RESULTAT<br/>List, long, Optional..."]
| Part | Quantes | Retorna | Quan s'executa |
|---|---|---|---|
| Font | Exactament 1 | Stream<T> |
En crear-la |
| Operació intermèdia | 0 o més | Stream<R> (encadenable) |
Mai per si sola: és mandrosa |
| Operació terminal | Exactament 1 | Un resultat (o res) | Dispara tot el processament |
La regla que ho explica tot: si un mètode de Stream retorna un Stream, és intermedi i no fa res; si retorna una altra cosa, és terminal i ho fa tot.
// Aixo NO IMPRIMEIX RES i NO RECORRE RES
cataleg.stream()
.filter(l -> l.getPagines() > 400)
.map(Llibre::getTitol);Sense operació terminal, la canonada està muntada però tancada. El compilador ni tan sols avisa (encara que els IDE moderns sí). És un error real i freqüent: algú escriu llista.stream().filter(...) esperant que filtri la llista, i no passa res.
- Avaluació mandrosa: l'ordre real d'execució
Aquesta és la part conceptualment més important dels streams, i la que més gent entén malament.
Intuïció equivocada: el stream aplica filter a tots els elements, produeix una llista intermèdia, aplica map a tots, produeix una altra, i així.
Realitat: cada element travessa la canonada completa abans que el següent comenci. No hi ha llistes intermèdies.
Demostrem-ho amb traces:
package com.nexussoftware.bibliotech;
import java.util.List;
public class DemostracioMandra {
public static void main(String[] args) {
List<Llibre> cataleg = List.of(
new Llibre("978-0000000001", "Java Eficac", 412),
new Llibre("978-0000000002", "Patrons de Disseny", 395),
new Llibre("978-0000000003", "Refactoritzacio", 448),
new Llibre("978-0000000004", "Codi Net", 464));
System.out.println("--- Muntant la canonada ---");
List<String> resultat = cataleg.stream()
.filter(llibre -> {
System.out.println(" filter(" + llibre.getTitol() + ")");
return llibre.getPagines() > 400;
})
.map(llibre -> {
System.out.println(" map(" + llibre.getTitol() + ")");
return llibre.getTitol().toUpperCase();
})
.limit(2)
.toList();
System.out.println("--- Resultat: " + resultat + " ---");
}
}--- Muntant la canonada ---
filter(Java Eficac)
map(Java Eficac)
filter(Patrons de Disseny)
filter(Refactoritzacio)
map(Refactoritzacio)
--- Resultat: [JAVA EFICAC, REFACTORITZACIO] ---Analitza aquesta sortida amb calma, perquè conté tres lliçons.
1. El processament és element a element, en vertical. "Java Eficaç" passa per filter i després per map abans que "Patrons de Disseny" toqui el filter. No hi ha una fase de filtratge i després una de mapatge.
2. map no s'executa sobre els elements filtrats. "Patrons de Disseny" té 395 pàgines, no passa el filtre, i el seu map no s'executa mai. Si map fos una operació cara —una consulta a base de dades, una petició de xarxa—, això seria la diferència entre quatre crides i dues.
3. "Codi Net" no es processa mai. El limit(2) va curtcircuitar la canonada: així que hi va haver dos resultats, el stream va deixar de demanar elements a la font. El quart llibre ni tan sols es va filtrar.
Aquest comportament s'anomena fusió d'operacions (loop fusion): el conjunt d'operacions intermèdies es compila conceptualment en un sol recorregut. Comparat amb l'enfocament ingenu:
// El que NO fa el stream: tres recorreguts i dues llistes temporals
List<Llibre> filtrats = new ArrayList<>();
for (Llibre l : cataleg) if (l.getPagines() > 400) filtrats.add(l);
List<String> mapejats = new ArrayList<>();
for (Llibre l : filtrats) mapejats.add(l.getTitol().toUpperCase());
List<String> limitats = mapejats.subList(0, Math.min(2, mapejats.size()));Un stream amb vint operacions intermèdies continua fent un sol recorregut i zero llistes intermèdies. Per això encadenar operacions no penalitza el rendiment tant com es podria témer.
La mandra també permet l'impossible: treballar amb seqüències infinites.
// Stream.iterate genera elements SENSE FI
List<String> referencies = Stream.iterate(1, n -> n + 1)
.map(n -> String.format("PR-2026-%04d", n))
.limit(5)
.toList();
System.out.println(referencies);Sense mandra, Stream.iterate penjaria el programa. Amb ella, només es generen els cinc elements que el limit demana.
- Fonts de streams
| Font | Sintaxi | Notes |
|---|---|---|
| Col·lecció | colleccio.stream() |
L'habitual. És a Collection, així que la tenen List, Set, Queue |
| Array | Arrays.stream(array) |
També Arrays.stream(array, des, fins) |
| Valors solts | Stream.of(a, b, c) |
Varargs |
| Buit | Stream.empty() |
Útil com a valor per defecte |
| Un o cap | Stream.ofNullable(x) |
Java 9: buit si x és null |
| Iteració | Stream.iterate(llavor, f) |
Infinit; hi ha variant amb predicat (Java 9) |
| Generació | Stream.generate(supplier) |
Infinit |
| Rang d'enters | IntStream.range(0, 10) |
rangeClosed inclou el final |
| Línies de fitxer | Files.lines(path, charset) |
Cal tancar-lo |
| Arbre de fitxers | Files.walk(path) |
Cal tancar-lo |
| Caràcters | text.chars() |
Retorna IntStream |
| Parts d'un text | Pattern.compile(";").splitAsStream(linia) |
Mandrós, a diferència de split |
| Mapa | mapa.entrySet().stream() |
Un Map no té stream() propi |
// Colleccions
Stream<Llibre> s1 = cataleg.stream();
Stream<String> s2 = new HashSet<>(List.of("a", "b")).stream();
// Arrays
String[] empleats = { "Marta Ruiz", "Diego Alonso", "Nuria Vidal" };
Stream<String> s3 = Arrays.stream(empleats);
// Valors solts
Stream<String> s4 = Stream.of("978-0000000001", "978-0000000002");
// Infinits, acotats amb limit
Stream<Integer> s5 = Stream.iterate(1, n -> n * 2).limit(10);
Stream<Double> s6 = Stream.generate(Math::random).limit(5);
// Amb condicio de parada (Java 9): ja no necessita limit
Stream<Integer> s7 = Stream.iterate(1, n -> n <= 100, n -> n * 2);
// Rang d enters
IntStream s8 = IntStream.rangeClosed(1, 12); // 1..12
// Mapa: sempre per les seves entrades
Map<String, Integer> prestecsPerEmpleat = Map.of("Marta Ruiz", 4, "Diego Alonso", 2);
Stream<Map.Entry<String, Integer>> s9 = prestecsPerEmpleat.entrySet().stream();Files.lines(): reprenent 07-06
A 07-06 vas fer servir Files.lines() amb un simple forEach perquè l'API completa estava pendent. Ara es pot aprofitar de debò:
package com.nexussoftware.bibliotech.persistencia;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.List;
import java.util.stream.Stream;
public class ImportadorCataleg {
/**
* Llegeix un CSV de cataleg amb processament MANDROS.
*
* try-with-resources (06-06) es OBLIGATORI: Files.lines mante
* obert un descriptor de fitxer fins que el stream es tanca.
*/
public List<Llibre> importar(Path fitxer) throws IOException {
try (Stream<String> linies = Files.lines(fitxer, StandardCharsets.UTF_8)) {
return linies
.skip(1) // saltar la capcalera
.map(String::strip)
.filter(linia -> !linia.isBlank()) // ignorar linies buides
.filter(linia -> !linia.startsWith("#")) // ignorar comentaris
.map(this::analitzar)
.filter(java.util.Objects::nonNull) // descartar les mal formades
.toList();
}
}
private Llibre analitzar(String linia) {
String[] camps = linia.split(";", -1);
if (camps.length < 4) {
return null;
}
try {
return new Llibre(camps[0], camps[1], camps[2], Integer.parseInt(camps[3]));
} catch (NumberFormatException e) {
return null;
}
}
/** Compta linies d un fitxer de 2 GB sense carregar lo a memoria. */
public long comptarPrestecsDe(Path fitxer, String empleat) throws IOException {
try (Stream<String> linies = Files.lines(fitxer, StandardCharsets.UTF_8)) {
return linies.filter(l -> l.contains(empleat)).count();
}
}
}Dos punts crítics.
El try-with-resources no és opcional. Files.lines() retorna un stream sustentat per un fitxer obert. Sense tancar-lo, exhaureixes els descriptors del procés i acabes amb Too many open files. És el mateix problema de 07-01, amb la diferència que aquí el recurs està amagat dins d'un stream.
El processament és mandrós de debò. comptarPrestecsDe sobre un fitxer de dos gigabytes no carrega el fitxer a memòria: llegeix una línia, l'avalua, la descarta. Compara-ho amb Files.readAllLines(), que construeix un List<String> complet i provoca OutOfMemoryError amb fitxers grans. És la mateixa distinció que a 07-04 entre llegir-ho tot i llegir amb BufferedReader, però ara amb tota l'API de processament al damunt.
I Files.walk(), també de 07-06, ara rendeix:
/** Els tres fitxers de resguard mes recents d un directori. */
public List<Path> resguardsRecents(Path directori) throws IOException {
try (Stream<Path> camins = Files.walk(directori, 2)) { // profunditat maxima 2
return camins
.filter(Files::isRegularFile)
.filter(p -> p.getFileName().toString().endsWith(".bak"))
.sorted(Comparator.comparing(this::dataDeModificacio).reversed())
.limit(3)
.toList();
}
}
- Operacions intermèdies
Totes retornen un Stream i cap no executa res per si sola.
filter: quedar-se amb els que compleixen
List<Llibre> prestats = cataleg.stream()
.filter(Llibre::estaPrestat)
.toList();
// Els predicats es poden compondre (04-06)
Predicate<Llibre> llarg = l -> l.getPagines() > 400;
Predicate<Llibre> disponible = l -> !l.estaPrestat();
List<Llibre> llargsDisponibles = cataleg.stream()
.filter(llarg.and(disponible))
.toList();Fixa't en el Predicate<? super T>: PECS de 10-01, que ara es llegeix sense esforç.
map: transformar cada element
map canvia el tipus del stream. Un Stream<Llibre> amb map(Llibre::getTitol) es converteix en un Stream<String>:
List<String> titols = cataleg.stream()
.map(Llibre::getTitol) // Stream<Llibre> -> Stream<String>
.toList();
// Encadenant transformacions
List<Integer> longituds = cataleg.stream()
.map(Llibre::getTitol) // Stream<String>
.map(String::length) // Stream<Integer>
.toList();
// A un record (04-07)
List<Fitxa> fitxes = cataleg.stream()
.map(l -> new Fitxa(l.getIsbn(), l.getTitol(), !l.estaPrestat()))
.toList();map no canvia el nombre d'elements: n'entren N, en surten N. Només en canvia el tipus o el valor.
mapToInt, mapToObj i família
Converteixen entre streams d'objectes i streams de primitius (apartat 13):
// Objecte -> primitiu
int totalPagines = cataleg.stream()
.mapToInt(Llibre::getPagines) // Stream<Llibre> -> IntStream
.sum(); // sum() NOMES existeix a IntStream
// Primitiu -> objecte
List<String> referencies = IntStream.rangeClosed(1, 3)
.mapToObj(n -> String.format("PR-2026-%04d", n)) // IntStream -> Stream<String>
.toList();distinct: eliminar duplicats
Fa servir equals/hashCode (03-09). Si els teus objectes no els tenen ben implementats, distinct no elimina res. És un LinkedHashSet per dins, amb el cost de memòria corresponent.
sorted: ordenar
// Ordre natural: exigeix que T implementi Comparable (10-01)
List<String> ordenats = titols.stream().sorted().toList();
// Amb Comparator (05-09)
List<Llibre> perPagines = cataleg.stream()
.sorted(Comparator.comparingInt(Llibre::getPagines).reversed())
.toList();
// Ordenacio composta
List<Prestec> ordenats = prestecs.stream()
.sorted(Comparator.comparing(Prestec::getEmpleat)
.thenComparing(Prestec::getDiaPrestec)
.reversed())
.toList();sorted és una operació amb estat: necessita veure tots els elements abans d'emetre el primer. Això trenca la mandra element a element i té dues conseqüències pràctiques:
- Sobre un stream infinit, es penja.
Stream.iterate(1, n -> n+1).sorted()no acaba mai. - Posa-la tan tard com puguis.
filter(...).sorted()ordena menys elements quesorted().filter(...), i el resultat és el mateix.
peek: mirar sense tocar
Executa una acció sobre cada element i el deixa passar intacte. El seu ús legítim és un de sol: depurar.
List<String> resultat = cataleg.stream()
.peek(l -> System.out.println("entra: " + l.getTitol()))
.filter(l -> l.getPagines() > 400)
.peek(l -> System.out.println(" passa el filtre: " + l.getTitol()))
.map(Llibre::getTitol)
.toList();Per què no fer-lo servir per a res més:
- No es garanteix que s'executi. La documentació ho diu explícitament: si la implementació pot calcular el resultat sense recórrer tots els elements, es pot saltar el
peek. A Java 9+,llista.stream().peek(...).count()no executa el peek, perquè la mida es coneix sense recórrer. - Convida als efectes secundaris.
peek(l -> altraLlista.add(l))és exactament l'estil que els streams venen a eliminar, i en paral·lel corromp la llista.
Si vols modificar, fes servir map. Si vols acumular, fes servir collect. peek és només per mirar.
limit i skip
List<Llibre> primers3 = cataleg.stream().limit(3).toList();
List<Llibre> saltats2 = cataleg.stream().skip(2).toList();
// Paginacio: pagina 3 amb 10 elements per pagina
int pagina = 3, mida = 10;
List<Llibre> paginaActual = cataleg.stream()
.skip((long) (pagina - 1) * mida)
.limit(mida)
.toList();limit curtcircuita: així que té N elements, deixa de demanar a la font (ho vas veure a l'apartat 4). skip sí que ha de recórrer els primers N per descartar-los.
flatMap: el cas que costa
flatMap: el cas que costaflatMap és l'operació que més costa d'entendre, i mereix el seu propi apartat.
Llegeix-ho així: map converteix cada element en un altre element; flatMap converteix cada element en un stream d'elements, i després aplana tots aquests streams en un de sol.
graph LR
subgraph map
A1["Empleat 1"] --> B1["Llista de prestecs 1"]
A2["Empleat 2"] --> B2["Llista de prestecs 2"]
end
subgraph flatMap
C1["Empleat 1"] --> D1["p1, p2"]
C2["Empleat 2"] --> D2["p3"]
D1 --> E["p1, p2, p3"]
D2 --> E
end
El cas real de BiblioTech: cada empleat té una llista de préstecs, i volem tots els préstecs de la biblioteca.
public class Empleat {
private final String nom;
private final List<Prestec> prestecs;
public List<Prestec> getPrestecs() { return prestecs; }
public String getNom() { return nom; }
}List<Empleat> plantilla = List.of(
new Empleat("Marta Ruiz", List.of(p1, p2, p3)),
new Empleat("Diego Alonso", List.of(p4)),
new Empleat("Nuria Vidal", List.of(p5, p6)));
// AMB map: no es el que vols
Stream<List<Prestec>> ambMap = plantilla.stream().map(Empleat::getPrestecs);
List<List<Prestec>> imbricada = ambMap.toList();
System.out.println(imbricada.size()); // 3: una llista per empleat
// AMB flatMap: aplanat
List<Prestec> tots = plantilla.stream()
.flatMap(empleat -> empleat.getPrestecs().stream()) // compte amb el .stream()!
.toList();
System.out.println(tots.size()); // 6: tots els prestecsEl detall que s'oblida sempre: la funció que es passa a flatMap ha de retornar un Stream, no una List. Per això cal escriure-hi .stream() a dins:
.flatMap(e -> e.getPrestecs().stream()) // BE
.flatMap(Empleat::getPrestecs) // NO COMPILA: retorna List, no StreamAra es pot continuar processant el resultat aplanat:
// Tots els prestecs amb retard de tota la biblioteca, ordenats
List<Prestec> ambRetard = plantilla.stream()
.flatMap(e -> e.getPrestecs().stream())
.filter(Prestec::teRetard)
.sorted(Comparator.comparingInt(Prestec::getDiesDeRetard).reversed())
.toList();
// Totes les incidencies de tots els prestecs de tots els empleats: DOS nivells
List<Prestec.Incidencia> totesLesIncidencies = plantilla.stream()
.flatMap(e -> e.getPrestecs().stream())
.flatMap(p -> p.getIncidencies().stream())
.toList();Altres usos habituals de flatMap:
// Partir cadenes en paraules
List<String> paraules = titols.stream()
.flatMap(t -> Arrays.stream(t.split("\\s+")))
.map(String::toLowerCase)
.distinct()
.sorted()
.toList();
// Aplanar un Map<String, List<Prestec>>
List<Prestec> plans = perEmpleat.values().stream()
.flatMap(List::stream)
.toList();
// Amb Optional (Java 9): descartar els buits de cop
List<Llibre> trobats = isbns.stream()
.map(cataleg::cercarPerIsbn) // Stream<Optional<Llibre>>
.flatMap(Optional::stream) // Stream<Llibre>, sense els buits
.toList();Aquest últim és un idioma molt útil que tornaràs a veure a l'apartat 17.
takeWhile i dropWhile
takeWhile i dropWhileAfegides a Java 9, són cosines de filter amb una diferència crucial: paren així que la condició falla, en lloc de continuar avaluant.
| Operació | Comportament |
|---|---|
filter(p) |
Es queda amb tots els que compleixen, recorrent-ho tot |
takeWhile(p) |
Pren elements fins al primer que no compleix, i para |
dropWhile(p) |
Descarta elements fins al primer que no compleix, i pren la resta |
List<Integer> pagines = List.of(500, 480, 450, 380, 420, 300);
System.out.println(pagines.stream().filter(p -> p > 400).toList());
System.out.println(pagines.stream().takeWhile(p -> p > 400).toList());
System.out.println(pagines.stream().dropWhile(p -> p > 400).toList());[500, 480, 450, 420] filter: TOTS els majors de 400
[500, 480, 450] takeWhile: para al 380
[380, 420, 300] dropWhile: descarta fins al 380, despres totNomés tenen sentit sobre dades ordenades, i allà són molt potents:
// Log ordenat per data: llegir nomes fins a sortir del rang, sense recorrer un fitxer de 2 GB
try (Stream<String> linies = Files.lines(logPath, StandardCharsets.UTF_8)) {
List<String> avui = linies
.dropWhile(l -> !l.startsWith("2026-08-05")) // saltar dies anteriors
.takeWhile(l -> l.startsWith("2026-08-05")) // parar en arribar a dema
.toList();
}Amb filter caldria llegir el fitxer sencer. Amb takeWhile, la lectura s'atura així que apareix la primera línia del dia següent.
- Operacions terminals
Disparen el processament i retornen un resultat (o res).
forEach i forEachOrdered
En un stream paral·lel, forEach no garanteix l'ordre; forEachOrdered sí, a canvi de rendiment:
cataleg.parallelStream().forEach(System.out::println); // ordre impredictible
cataleg.parallelStream().forEachOrdered(System.out::println); // ordre de l origenConsell: si vas a construir una col·lecció, no facis servir forEach amb un add. Fes servir collect. forEach és per a efectes finals: imprimir, enviar, guardar.
toList() i collect
// Java 16+: la forma curta. Retorna una llista NO MODIFICABLE.
List<String> titols = cataleg.stream().map(Llibre::getTitol).toList();
// La forma classica, amb Collectors
List<String> titols2 = cataleg.stream().map(Llibre::getTitol)
.collect(Collectors.toList()); // modificable, sense garantia de tipus
// Si necessites una llista modificable d un tipus concret
List<String> modificable = cataleg.stream().map(Llibre::getTitol)
.collect(Collectors.toCollection(ArrayList::new));| Forma | Modificable | Admet null |
Versió |
|---|---|---|---|
.toList() |
No | Sí | Java 16+ |
.collect(Collectors.toList()) |
Normalment sí (no garantit) | Sí | Java 8+ |
.collect(Collectors.toUnmodifiableList()) |
No | No | Java 10+ |
.collect(Collectors.toCollection(ArrayList::new)) |
Sí, garantit | Sí | Java 8+ |
Fes servir .toList() per defecte. És més curt, més clar i retorna una llista no modificable, que és el que vols el 90 % de les vegades.
count
Atenció a una optimització que sorprèn: des de Java 9, si el stream pot saber la seva mida sense recórrer, count() no executa les operacions intermèdies:
long n = cataleg.stream()
.peek(l -> System.out.println("vist: " + l.getTitol()))
.count();
// No imprimeix res: count() sap la mida de la llista sense recorrer laAmb un filter al davant sí que recorre, perquè el filtre pot canviar el nombre.
min i max
Retornen Optional perquè el stream pot estar buit:
Optional<Llibre> mesLlarg = cataleg.stream()
.max(Comparator.comparingInt(Llibre::getPagines));
mesLlarg.ifPresent(l -> System.out.println("El mes llarg: " + l.getTitol()));
// Amb valor per defecte
String titol = cataleg.stream()
.min(Comparator.comparing(Llibre::getTitol))
.map(Llibre::getTitol)
.orElse("(cataleg buit)");anyMatch, allMatch, noneMatch i el curtcircuit
boolean hiHaAlgunPrestat = cataleg.stream().anyMatch(Llibre::estaPrestat);
boolean totsDisponibles = cataleg.stream().allMatch(l -> !l.estaPrestat());
boolean capSenseTitol = cataleg.stream().noneMatch(l -> l.getTitol().isBlank());Els tres curtcircuiten: paren així que coneixen la resposta.
System.out.println(cataleg.stream()
.peek(l -> System.out.println("comprovant: " + l.getTitol()))
.anyMatch(l -> l.getPagines() > 400));Només va avaluar el primer: en trobar-ne un que compleix, la resposta ja és true.
El cas parany del stream buit, que segueix la lògica matemàtica i confon tothom:
List<Llibre> buit = List.of();
System.out.println(buit.stream().anyMatch(l -> true)); // false
System.out.println(buit.stream().allMatch(l -> false)); // true!
System.out.println(buit.stream().noneMatch(l -> true)); // trueallMatch sobre un conjunt buit és cert per vacuïtat: "tots els elements compleixen X" no té contraexemple si no hi ha elements. Matemàticament correcte i una font d'errors si no t'ho esperes.
findFirst i findAny
Optional<Llibre> primer = cataleg.stream()
.filter(l -> l.getPagines() > 400)
.findFirst();
Optional<Llibre> qualsevol = cataleg.parallelStream()
.filter(l -> l.getPagines() > 400)
.findAny();| Operació | Seqüencial | Paral·lel |
|---|---|---|
findFirst() |
El primer en ordre de trobada | El primer en ordre, amb cost de coordinació |
findAny() |
Normalment el primer | Qualsevol, sense coordinació: més ràpid |
En streams seqüencials són pràcticament equivalents. En paral·lel, findAny és l'opció correcta quan qualsevol coincidència serveix.
reduce en les seves tres formes
reduce en les seves tres formesreduce combina tots els elements en un de sol aplicant repetidament una operació binària. Té tres sobrecàrregues i convé entendre-les per separat.
Forma 1: només l'acumulador → retorna Optional
Optional<Integer> totalPagines = cataleg.stream()
.map(Llibre::getPagines)
.reduce((a, b) -> a + b);
System.out.println(totalPagines.orElse(0));Retorna Optional perquè si el stream està buit no hi ha cap valor a retornar. No hi ha cap "zero" que l'operació es pugui inventar: reduce no sap si combines nombres (on seria 0), textos (on seria "") o màxims (on seria -∞).
Pas a pas, amb [412, 395, 448]:
pas 1: acumulador = 412 (primer element)
pas 2: acumulador = 412 + 395 = 807
pas 3: acumulador = 807 + 448 = 1255
resultat: Optional[1255]Forma 2: identitat + acumulador → retorna T
int totalPagines = cataleg.stream()
.map(Llibre::getPagines)
.reduce(0, Integer::sum); // 0 es la identitat
String totsElsTitols = cataleg.stream()
.map(Llibre::getTitol)
.reduce("", (a, b) -> a.isEmpty() ? b : a + " | " + b);En haver-hi identitat, no hi ha Optional: amb el stream buit es retorna la identitat.
La identitat ho ha de ser de debò: op(identitat, x) ha de ser igual a x per a qualsevol x. Per a la suma és 0, per al producte 1, per a la concatenació "", per al màxim Integer.MIN_VALUE. Posar 1 com a identitat d'una suma dona resultats incorrectes, i en paral·lel l'error es multiplica perquè la identitat s'aplica a cada partició:
// MALAMENT: identitat incorrecta
int suma = Stream.of(1, 2, 3, 4).reduce(10, Integer::sum);
System.out.println(suma); // 20, no 10
int sumaParallela = Stream.of(1, 2, 3, 4).parallel().reduce(10, Integer::sum);
System.out.println(sumaParallela); // 50! el 10 es va sumar a cada particioForma 3: identitat + acumulador + combinador → canvia de tipus
Aquesta forma permet que el tipus del resultat sigui diferent del dels elements. El tercer argument, el combinador, diu com fondre dos resultats parcials, i només es fa servir en paral·lel.
// Sumar les pagines d objectes Llibre produint un int
int totalPagines = cataleg.parallelStream()
.reduce(0, // identitat: un int
(suma, llibre) -> suma + llibre.getPagines(), // acumular Llibre en int
Integer::sum); // fondre dos int parcialsgraph TD
A["cataleg complet"] --> B["particio 1"]
A --> C["particio 2"]
B -->|"acumulador"| D["parcial 1 = 807"]
C -->|"acumulador"| E["parcial 2 = 464"]
D -->|"combinador"| F["total = 1271"]
E -->|"combinador"| F
A la pràctica aquesta tercera forma es fa servir poc, perquè collect amb Collectors és més llegible i mapToInt(...).sum() resol el cas numèric d'una tacada. Però entendre-la explica com funciona collect per dins, i per què un Collector necessita un combinador.
Quan fer servir reduce: quan el resultat és un únic valor obtingut combinant elements i no existeix un Collector que ho faci. Per a sumes, mitjanes i recomptes, fes servir els streams de primitius o els Collectors; són més clars i més ràpids.
- Els
Collectors bàsics
Collectors bàsicscollect és l'operació terminal més versàtil: acumula els elements en una estructura fent servir un Collector. La classe java.util.stream.Collectors porta fàbriques per a tot.
toList, toSet, toMap
List<String> llista = cataleg.stream().map(Llibre::getTitol).collect(Collectors.toList());
Set<String> conjunt = cataleg.stream().map(Llibre::getAutor).collect(Collectors.toSet());
// toMap: clau i valor
Map<String, Llibre> perIsbn = cataleg.stream()
.collect(Collectors.toMap(Llibre::getIsbn, llibre -> llibre));
Map<String, String> titolPerIsbn = cataleg.stream()
.collect(Collectors.toMap(Llibre::getIsbn, Llibre::getTitol));El parany de toMap: les claus duplicades llancen una excepció.
// Dos llibres del mateix autor
Map<String, String> perAutor = cataleg.stream()
.collect(Collectors.toMap(Llibre::getAutor, Llibre::getTitol));Exception in thread "main" java.lang.IllegalStateException:
Duplicate key Martin Fowler (attempted merging values Refactoritzacio and UML Destillat)
at java.base/java.util.stream.Collectors.duplicateKeyException(Collectors.java:135)La solució és el tercer argument, la funció de mescla, que decideix què fer amb el conflicte:
// Quedar se amb el primer
Map<String, String> primer = cataleg.stream()
.collect(Collectors.toMap(Llibre::getAutor, Llibre::getTitol,
(existent, nou) -> existent));
// Quedar se amb l ultim
Map<String, String> ultim = cataleg.stream()
.collect(Collectors.toMap(Llibre::getAutor, Llibre::getTitol,
(existent, nou) -> nou));
// Combinar
Map<String, String> combinat = cataleg.stream()
.collect(Collectors.toMap(Llibre::getAutor, Llibre::getTitol,
(a, b) -> a + "; " + b));
// I amb un quart argument es tria la implementacio del Map
Map<String, String> ordenat = cataleg.stream()
.collect(Collectors.toMap(Llibre::getIsbn, Llibre::getTitol,
(a, b) -> a, TreeMap::new));Segon parany: toMap no admet valors null. Llança NullPointerException, a diferència de HashMap.put. Si els teus valors poden ser nuls, filtra abans.
joining: concatenar cadenes
String titols = cataleg.stream()
.map(Llibre::getTitol)
.collect(Collectors.joining(", "));
// Java Eficac, Patrons de Disseny, Refactoritzacio
String ambVoreres = cataleg.stream()
.map(Llibre::getTitol)
.collect(Collectors.joining(", ", "Cataleg [", "]"));
// Cataleg [Java Eficac, Patrons de Disseny, Refactoritzacio]Fa servir StringBuilder per dins, així que és eficient fins i tot amb milers d'elements (ho veuràs quantificat a 10-07).
Estadístiques: counting, summing, averaging, summarizing
long total = cataleg.stream().collect(Collectors.counting());
int pagines = cataleg.stream().collect(Collectors.summingInt(Llibre::getPagines));
double mitjana = cataleg.stream().collect(Collectors.averagingInt(Llibre::getPagines));
double sumaValoracions = cataleg.stream().collect(Collectors.summingDouble(Llibre::getValoracio));
double mitjanaValoracions = cataleg.stream().collect(Collectors.averagingDouble(Llibre::getValoracio));
// Tot de cop, en un sol recorregut
IntSummaryStatistics stats = cataleg.stream()
.collect(Collectors.summarizingInt(Llibre::getPagines));
System.out.println("Llibres: " + stats.getCount());
System.out.println("Total: " + stats.getSum());
System.out.println("Mitjana: " + stats.getAverage());
System.out.println("Minim: " + stats.getMin());
System.out.println("Maxim: " + stats.getMax());summarizingInt calcula les cinc estadístiques en un sol recorregut. Amb bucles caldrien cinc variables i molt de compte amb el mínim i el màxim inicials.
Nota: counting() i summingInt() solts són innecessàriament indirectes —stream().count() i stream().mapToInt(...).sum() són més clars—. El seu veritable valor apareix com a downstream de groupingBy, a l'apartat següent.
groupingBy i partitioningBy
groupingBy i partitioningByAquí hi ha l'operació que converteix trenta línies de bucles imbricats en una.
groupingBy simple
Map<TipusMaterial, List<Material>> perTipus = inventari.stream()
.collect(Collectors.groupingBy(Material::getTipus));{LLIBRE=[Java Eficac, Patrons de Disseny, Refactoritzacio],
REVISTA=[Java Magazine],
DVD=[Curs de Spring]}Compara-ho amb l'equivalent imperatiu, que és exactament el que vas escriure al mòdul 5:
Map<TipusMaterial, List<Material>> perTipus = new HashMap<>();
for (Material m : inventari) {
perTipus.computeIfAbsent(m.getTipus(), t -> new ArrayList<>()).add(m);
}Quatre línies contra una, i la d'una diu molt més clarament què es pretén.
groupingBy amb downstream
El segon argument és un altre Collector que decideix què fer amb cada grup, en lloc d'acumular-lo en una llista. Aquí és on counting, summing i companyia cobren sentit:
// Quants materials de cada tipus
Map<TipusMaterial, Long> recomptePerTipus = inventari.stream()
.collect(Collectors.groupingBy(Material::getTipus, Collectors.counting()));
// {LLIBRE=3, REVISTA=1, DVD=1}
// Pagines totals per tipus
Map<TipusMaterial, Integer> paginesPerTipus = inventari.stream()
.collect(Collectors.groupingBy(Material::getTipus,
Collectors.summingInt(Material::getPagines)));
// Valoracio mitjana per tipus
Map<TipusMaterial, Double> mitjanaPerTipus = inventari.stream()
.collect(Collectors.groupingBy(Material::getTipus,
Collectors.averagingDouble(Material::getValoracio)));
// Nomes els TITOLS de cada grup: mapping transforma abans d acumular
Map<TipusMaterial, List<String>> titolsPerTipus = inventari.stream()
.collect(Collectors.groupingBy(Material::getTipus,
Collectors.mapping(Material::getTitol,
Collectors.toList())));
// El mes valorat de cada tipus: retorna Optional dins del mapa
Map<TipusMaterial, Optional<Material>> millorPerTipus = inventari.stream()
.collect(Collectors.groupingBy(Material::getTipus,
Collectors.maxBy(Comparator.comparingDouble(Material::getValoracio))));
// Sense l Optional, amb maxBy reduit (Java 9)
Map<TipusMaterial, Material> millorPerTipus2 = inventari.stream()
.collect(Collectors.groupingBy(Material::getTipus,
Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparingDouble(Material::getValoracio)),
opt -> opt.orElse(null))));
// Estadistiques completes per tipus
Map<TipusMaterial, IntSummaryStatistics> statsPerTipus = inventari.stream()
.collect(Collectors.groupingBy(Material::getTipus,
Collectors.summarizingInt(Material::getPagines)));
// Amb TreeMap perque les claus surtin ordenades
Map<String, Long> perAutorOrdenat = cataleg.stream()
.collect(Collectors.groupingBy(Llibre::getAutor, TreeMap::new, Collectors.counting()));groupingBy de dos nivells
El downstream pot ser un altre groupingBy, i allà apareix l'agrupament imbricat:
// Prestecs per empleat i, a dins, per tipus de material
Map<String, Map<TipusMaterial, List<Prestec>>> perEmpleatITipus = prestecs.stream()
.collect(Collectors.groupingBy(
Prestec::getEmpleat,
Collectors.groupingBy(p -> p.getMaterial().getTipus())));
// I amb recompte al nivell interior
Map<String, Map<TipusMaterial, Long>> recomptePerEmpleatITipus = prestecs.stream()
.collect(Collectors.groupingBy(
Prestec::getEmpleat,
Collectors.groupingBy(p -> p.getMaterial().getTipus(),
Collectors.counting())));Aquest resultat, al mòdul 5, eren tres bucles imbricats, dos HashMap, dos computeIfAbsent i unes vint línies.
partitioningBy
Cas particular de groupingBy amb un Predicate: sempre produeix un Map<Boolean, ...> amb exactament dues claus, true i false, fins i tot si un grup és buit.
Map<Boolean, List<Llibre>> perDisponibilitat = cataleg.stream()
.collect(Collectors.partitioningBy(Llibre::estaPrestat));
List<Llibre> prestats = perDisponibilitat.get(true);
List<Llibre> disponibles = perDisponibilitat.get(false);
// Amb downstream
Map<Boolean, Long> recompte = cataleg.stream()
.collect(Collectors.partitioningBy(l -> l.getPagines() > 400,
Collectors.counting()));groupingBy(p::test) |
partitioningBy(p) |
|
|---|---|---|
| Claus | Només les que apareixen | Sempre true i false |
| Grup buit | No apareix la clau | Apareix amb llista buida |
| Rendiment | Mapa general | Optimitzat per a dues claus |
Aquesta diferència importa: amb groupingBy, si cap llibre no està prestat, get(true) retorna null i produeix un NullPointerException. Amb partitioningBy, retorna una llista buida.
teeing
Afegit a Java 12, permet aplicar dos col·lectors alhora i combinar-ne els resultats en un sol recorregut:
record ResumCataleg(long total, double mitjanaPagines) { }
ResumCataleg resum = cataleg.stream()
.collect(Collectors.teeing(
Collectors.counting(),
Collectors.averagingInt(Llibre::getPagines),
ResumCataleg::new));És útil quan necessites dos agregats i no vols recórrer dues vegades. S'esmenta per completesa; en el dia a dia es fa servir poc.
- Streams de primitius
Stream<Integer> guarda objectes Integer, i cada operació implica autoboxing: embolcallar un int en un objecte, amb la seva reserva de memòria i la seva indirecció. Per a seqüències numèriques grans això és un malbaratament notable.
Java 8 porta tres streams especialitzats: IntStream, LongStream i DoubleStream.
| Aspecte | Stream<Integer> |
IntStream |
|---|---|---|
| Emmagatzema | Objectes Integer |
int primitius |
| Autoboxing | A cada operació | Cap |
| Memòria | ~16 bytes per element | 4 bytes |
| Mètodes numèrics | No | sum, average, max, min, summaryStatistics |
map retorna |
Stream<R> |
IntStream (fes servir mapToObj per sortir-ne) |
// Suma sense autoboxing
int totalPagines = cataleg.stream()
.mapToInt(Llibre::getPagines)
.sum();
// Mitjana: retorna OptionalDouble, perque un stream buit no te mitjana
OptionalDouble mitjana = cataleg.stream()
.mapToInt(Llibre::getPagines)
.average();
System.out.printf("Mitjana: %.1f pagines%n", mitjana.orElse(0.0));
// Estadistiques de cop
IntSummaryStatistics stats = cataleg.stream()
.mapToInt(Llibre::getPagines)
.summaryStatistics();
// Rangs
IntStream.range(0, 5).forEach(System.out::println); // 0,1,2,3,4
IntStream.rangeClosed(1, 5).forEach(System.out::println); // 1,2,3,4,5
// Tornar a objectes
List<Integer> llista = IntStream.rangeClosed(1, 5).boxed().toList();
List<String> refs = IntStream.rangeClosed(1, 3)
.mapToObj(n -> String.format("PR-2026-%04d", n))
.toList();
// Caracters d una cadena
long vocals = "Java Eficac".chars()
.filter(c -> "aeiouAEIOU".indexOf(c) >= 0)
.count();Els Optional primitius. average() retorna OptionalDouble, no Optional<Double>; max() sobre un IntStream retorna OptionalInt. Són versions sense autoboxing d'Optional, amb la mateixa API bàsica (isPresent, getAsDouble, orElse) però sense map, filter ni flatMap. Si necessites encadenar, converteix-los:
OptionalDouble mitjana = cataleg.stream().mapToInt(Llibre::getPagines).average();
String text = mitjana.isPresent()
? String.format("%.1f", mitjana.getAsDouble())
: "sense dades";
// O convertint a Optional<Double>
String text2 = mitjana.stream().boxed().findFirst()
.map(d -> String.format("%.1f", d))
.orElse("sense dades");Regla pràctica: fes servir streams de primitius sempre que treballis amb nombres. El codi és més curt (sum() en lloc de reduce(0, Integer::sum)) i evita milions d'objectes temporals en seqüències grans.
- Streams paral·lels
Reprenem 08-05, ara amb tot el context.
Convertir un stream en paral·lel és trivial:
List<Llibre> resultat = cataleg.parallelStream() // des de la colleccio
.filter(l -> l.getPagines() > 400)
.toList();
List<Llibre> resultat2 = cataleg.stream()
.parallel() // des d un stream existent
.filter(l -> l.getPagines() > 400)
.toList();Per sota, el stream parteix les dades, processa les parts en diversos fils i combina els resultats. La feina la fa el ForkJoinPool.commonPool() que vas conèixer a 08-05.
graph TD
A["Font: 10000 elements"] --> B["Spliterator parteix"]
B --> C1["particio 1"]
B --> C2["particio 2"]
B --> C3["particio 3"]
B --> C4["particio 4"]
C1 --> D1["fil 1: filter, map"]
C2 --> D2["fil 2: filter, map"]
C3 --> D3["fil 3: filter, map"]
C4 --> D4["fil 4: filter, map"]
D1 --> E["combinar resultats"]
D2 --> E
D3 --> E
D4 --> E
E --> F["Resultat final"]
El commonPool i les seves conseqüències
System.out.println("Parallelisme: " + ForkJoinPool.getCommonPoolParallelism());
System.out.println("Nuclis: " + Runtime.getRuntime().availableProcessors());El commonPool té per defecte nuclis − 1 fils (el fil que crida hi participa també). I això té una conseqüència que cal tenir molt present:
Tots els streams paral·lels de tota l'aplicació comparteixen el mateix pool. Si una tasca bloqueja un fil del commonPool amb una operació de xarxa o d'E/S, està bloquejant tots els altres streams paral·lels de l'aplicació. Per això:
No facis servir mai
parallelStream()amb operacions que bloquegen (E/S, xarxa, esperes). Per a això hi ha elsExecutorServicepropis de 08-05 iCompletableFuturede 08-07 — o els fils virtuals de 10-06.
Es pot executar en un pool propi, encara que és un pedaç que convé conèixer:
ForkJoinPool poolPropi = new ForkJoinPool(4);
try {
List<Llibre> resultat = poolPropi.submit(() ->
cataleg.parallelStream().filter(...).toList()).get();
} finally {
poolPropi.shutdown();
}Quan ajuda i quan perjudica
El paral·lelisme no és gratis: partir, coordinar fils i combinar resultats té un cost fix. Només compensa si la feina útil és molt més gran que aquest cost.
| Factor | Favorable al paral·lel | Desfavorable |
|---|---|---|
| Mida | Desenes de milers d'elements o més | Centenars o menys |
| Cost per element | Alt (càlcul intens) | Trivial (sumar, comparar) |
| Font | ArrayList, arrays, IntStream.range (partició barata) |
LinkedList, Stream.iterate (partició cara o impossible) |
| Operacions | Sense estat: filter, map |
Amb estat: sorted, distinct, limit |
| Terminal | reduce, sum, collect(toList) |
forEachOrdered, findFirst (exigeixen ordre) |
| Context | Aplicació d'escriptori, procés per lots | Servidor amb moltes peticions concurrents |
Aquesta última fila s'oblida sovint: en un servidor que ja atén 200 peticions alhora, tots els nuclis estan ocupats. Paral·lelitzar dins de cada petició no aporta res i hi afegeix coordinació.
Un mesurament honest
package com.nexussoftware.bibliotech;
import java.util.*;
import java.util.stream.*;
public class MesuraParallela {
public static void main(String[] args) {
// ADVERTIMENT: aixo es un mesurament CASOLA. Els nombres son
// orientatius; per mesurar de debo cal JMH (10-07).
List<Integer> petita = IntStream.range(0, 1_000).boxed().toList();
List<Integer> gran = IntStream.range(0, 10_000_000).boxed().toList();
escalfar(gran);
mesurar("1.000 elements, suma trivial, sequencial",
() -> petita.stream().mapToLong(Integer::longValue).sum());
mesurar("1.000 elements, suma trivial, PARALLEL ",
() -> petita.parallelStream().mapToLong(Integer::longValue).sum());
mesurar("10M elements, suma trivial, sequencial ",
() -> gran.stream().mapToLong(Integer::longValue).sum());
mesurar("10M elements, suma trivial, PARALLEL ",
() -> gran.parallelStream().mapToLong(Integer::longValue).sum());
mesurar("10M elements, calcul car, sequencial ",
() -> gran.stream().mapToLong(MesuraParallela::car).sum());
mesurar("10M elements, calcul car, PARALLEL ",
() -> gran.parallelStream().mapToLong(MesuraParallela::car).sum());
// LinkedList: particio cara
List<Integer> enllacada = new LinkedList<>(IntStream.range(0, 1_000_000).boxed().toList());
mesurar("1M en LinkedList, sequencial ",
() -> enllacada.stream().mapToLong(Integer::longValue).sum());
mesurar("1M en LinkedList, PARALLEL ",
() -> enllacada.parallelStream().mapToLong(Integer::longValue).sum());
}
private static long car(int n) {
return (long) (Math.sqrt(n) * Math.log(n + 1) * Math.sin(n));
}
private static void escalfar(List<Integer> dades) {
for (int i = 0; i < 5; i++) {
dades.stream().mapToLong(Integer::longValue).sum();
dades.parallelStream().mapToLong(Integer::longValue).sum();
}
}
private static void mesurar(String etiqueta, java.util.function.LongSupplier tasca) {
long inici = System.nanoTime();
long resultat = tasca.getAsLong();
long ms = (System.nanoTime() - inici) / 1_000_000;
System.out.printf("%s -> %6d ms (resultat: %d)%n", etiqueta, ms, resultat);
}
}Sortida orientativa en una màquina de 8 nuclis:
1.000 elements, suma trivial, sequencial -> 0 ms (resultat: 499500)
1.000 elements, suma trivial, PARALLEL -> 2 ms (resultat: 499500)
10M elements, suma trivial, sequencial -> 58 ms (resultat: 49999995000000)
10M elements, suma trivial, PARALLEL -> 21 ms (resultat: 49999995000000)
10M elements, calcul car, sequencial -> 712 ms (resultat: -1583)
10M elements, calcul car, PARALLEL -> 134 ms (resultat: -1583)
1M en LinkedList, sequencial -> 11 ms (resultat: 499999500000)
1M en LinkedList, PARALLEL -> 34 ms (resultat: 499999500000)Quatre conclusions que valen més que la taula:
- Amb 1.000 elements, el paral·lel és més lent. El cost de coordinar supera la feina.
- Amb 10 milions i feina trivial, millora 2,8× en 8 nuclis. No 8×: l'accés a memòria i la combinació no escalen linealment.
- Amb feina cara, millora 5,3×. Com més costa cada element, millor s'amortitza el cost fix.
- Amb
LinkedList, el paral·lel és 3× més LENT. Partir una llista enllaçada exigeix recórrer-la; no hi ha accés per índex. L'estructura de dades decideix (mòdul 5, i ho reprendràs a 10-07).
La regla d'or: gens d'estat compartit
// CATASTROFIC: ArrayList no es segur entre fils (08-06)
List<String> resultat = new ArrayList<>();
cataleg.parallelStream().forEach(l -> resultat.add(l.getTitol()));Això produeix, de manera impredictible: elements perduts, null enmig de la llista, ArrayIndexOutOfBoundsException o ConcurrentModificationException. I el pitjor: funciona correctament en proves amb pocs elements i falla en producció.
// CORRECTE: collect gestiona la combinacio per tu
List<String> resultat = cataleg.parallelStream()
.map(Llibre::getTitol)
.toList();La regla, sense excepcions: les lambdes d'un stream paral·lel no han de modificar cap estat compartit. Deixa que collect o reduce construeixin el resultat.
La llista de comprovació abans d'escriure parallelStream()
- Hi ha desenes de milers d'elements o més?
- El cost per element és apreciable?
- La font és fàcil de partir (
ArrayList, array, rang)? - Les operacions són sense estat i sense bloqueig?
- Ho has mesurat i és realment més ràpid?
- L'aplicació no està ja saturant els nuclis?
Si alguna resposta és "no", fes servir stream().
- L'informe de BiblioTech: de trenta línies a tres
Ara, la demostració que justifica tota la lliçó. Aquest és l'informe que EstadistiquesBiblioTech generava al mòdul 5:
package com.nexussoftware.bibliotech.servei;
import java.util.*;
/** VERSIO ANTIGA: la del modul 5. */
public class EstadistiquesBiblioTechAntic {
public Map<TipusMaterial, Long> recomptePerTipus(List<Material> inventari) {
Map<TipusMaterial, Long> resultat = new HashMap<>();
for (Material m : inventari) {
Long actual = resultat.get(m.getTipus());
resultat.put(m.getTipus(), actual == null ? 1L : actual + 1);
}
return resultat;
}
public Map<String, List<String>> titolsPerEmpleat(List<Prestec> prestecs) {
Map<String, List<String>> resultat = new HashMap<>();
for (Prestec p : prestecs) {
List<String> llista = resultat.get(p.getEmpleat());
if (llista == null) {
llista = new ArrayList<>();
resultat.put(p.getEmpleat(), llista);
}
llista.add(p.getMaterial().getTitol());
}
for (List<String> llista : resultat.values()) {
Collections.sort(llista);
}
return resultat;
}
public String empleatAmbMesPrestecs(List<Prestec> prestecs) {
Map<String, Integer> recompte = new HashMap<>();
for (Prestec p : prestecs) {
Integer actual = recompte.get(p.getEmpleat());
recompte.put(p.getEmpleat(), actual == null ? 1 : actual + 1);
}
String millor = null;
int maxim = -1;
for (Map.Entry<String, Integer> e : recompte.entrySet()) {
if (e.getValue() > maxim) {
maxim = e.getValue();
millor = e.getKey();
}
}
return millor; // null si la llista era buida
}
public double mitjanaPaginesDelsPrestats(List<Material> inventari) {
int suma = 0;
int compte = 0;
for (Material m : inventari) {
if (m.estaPrestat() && m instanceof Llibre) {
suma += ((Llibre) m).getPagines();
compte++;
}
}
return compte == 0 ? 0.0 : (double) suma / compte;
}
}I aquesta és la mateixa classe amb streams:
package com.nexussoftware.bibliotech.servei;
import com.nexussoftware.bibliotech.domini.*;
import java.util.*;
import java.util.stream.Collectors;
/**
* VERSIO AMB STREAMS. Mateix comportament, un terc del codi,
* i cada metode es llegeix com la frase que el descriu.
*/
public class EstadistiquesBiblioTech {
/** Quants materials hi ha de cada tipus. */
public Map<TipusMaterial, Long> recomptePerTipus(List<Material> inventari) {
return inventari.stream()
.collect(Collectors.groupingBy(Material::getTipus, Collectors.counting()));
}
/** Titols prestats a cada empleat, ordenats alfabeticament. */
public Map<String, List<String>> titolsPerEmpleat(List<Prestec> prestecs) {
return prestecs.stream()
.collect(Collectors.groupingBy(
Prestec::getEmpleat,
TreeMap::new,
Collectors.mapping(p -> p.getMaterial().getTitol(),
Collectors.collectingAndThen(
Collectors.toList(),
llista -> { llista.sort(null); return llista; }))));
}
/** L empleat amb mes prestecs. Optional en lloc de null. */
public Optional<String> empleatAmbMesPrestecs(List<Prestec> prestecs) {
return prestecs.stream()
.collect(Collectors.groupingBy(Prestec::getEmpleat, Collectors.counting()))
.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey);
}
/** Mitjana de pagines dels llibres prestats. */
public OptionalDouble mitjanaPaginesDelsPrestats(List<Material> inventari) {
return inventari.stream()
.filter(Material::estaPrestat)
.filter(Llibre.class::isInstance) // l instanceof de 10-03
.map(Llibre.class::cast)
.mapToInt(Llibre::getPagines)
.average();
}
/** Informe complet per tipus: compte, pagines i valoracio mitjana. */
public Map<TipusMaterial, ResumTipus> informeComplet(List<Material> inventari) {
return inventari.stream()
.collect(Collectors.groupingBy(
Material::getTipus,
Collectors.collectingAndThen(
Collectors.toList(),
llista -> new ResumTipus(
llista.size(),
llista.stream().mapToInt(Material::getPagines).sum(),
llista.stream().mapToDouble(Material::getValoracio)
.average().orElse(0.0)))));
}
public record ResumTipus(int quants, int paginesTotals, double valoracioMitjana) { }
/** Prestecs per empleat i tipus: l informe de dos nivells. */
public Map<String, Map<TipusMaterial, Long>> matriuEmpleatTipus(List<Prestec> prestecs) {
return prestecs.stream()
.collect(Collectors.groupingBy(
Prestec::getEmpleat,
TreeMap::new,
Collectors.groupingBy(p -> p.getMaterial().getTipus(),
() -> new EnumMap<>(TipusMaterial.class),
Collectors.counting())));
}
/** Els N materials mes valorats. */
public List<Material> topValorats(List<Material> inventari, int n) {
return inventari.stream()
.sorted(Comparator.comparingDouble(Material::getValoracio).reversed())
.limit(n)
.toList();
}
/** Totes les incidencies de tots els prestecs, agrupades per gravetat. */
public Map<Gravetat, List<String>> incidenciesPerGravetat(List<Prestec> prestecs) {
return prestecs.stream()
.flatMap(p -> p.getIncidencies().stream())
.collect(Collectors.groupingBy(
Prestec.Incidencia::gravetat,
() -> new EnumMap<>(Gravetat.class),
Collectors.mapping(Prestec.Incidencia::descripcio,
Collectors.toList())));
}
/** Linia de resum per al tauler de la biblioteca. */
public String resumEnUnaLinia(List<Material> inventari) {
return inventari.stream()
.collect(Collectors.groupingBy(Material::getTipus,
() -> new EnumMap<>(TipusMaterial.class),
Collectors.counting()))
.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining(", ", "Inventari [", "]"));
}
}El programa que l'executa:
package com.nexussoftware.bibliotech;
import com.nexussoftware.bibliotech.domini.*;
import com.nexussoftware.bibliotech.servei.EstadistiquesBiblioTech;
import java.util.List;
public class InformeBiblioTech {
public static void main(String[] args) {
List<Material> inventari = DadesDeProva.inventari();
List<Prestec> prestecs = DadesDeProva.prestecs();
EstadistiquesBiblioTech stats = new EstadistiquesBiblioTech();
System.out.println("=== INFORME BIBLIOTECH ===");
System.out.println(stats.resumEnUnaLinia(inventari));
System.out.println();
System.out.println("--- Materials per tipus ---");
stats.informeComplet(inventari).forEach((tipus, r) ->
System.out.printf(" %-8s %2d unitats, %5d pagines, valoracio %.2f%n",
tipus, r.quants(), r.paginesTotals(), r.valoracioMitjana()));
System.out.println();
System.out.println("--- Prestecs per empleat i tipus ---");
stats.matriuEmpleatTipus(prestecs).forEach((empleat, perTipus) ->
System.out.printf(" %-14s %s%n", empleat, perTipus));
System.out.println();
System.out.println("--- Titols per empleat ---");
stats.titolsPerEmpleat(prestecs).forEach((empleat, titols) ->
System.out.printf(" %-14s %s%n", empleat, titols));
System.out.println();
stats.empleatAmbMesPrestecs(prestecs)
.ifPresentOrElse(e -> System.out.println("Qui mes llegeix: " + e),
() -> System.out.println("No hi ha prestecs registrats"));
System.out.printf("Mitjana de pagines del prestat: %.1f%n",
stats.mitjanaPaginesDelsPrestats(inventari).orElse(0.0));
System.out.println();
System.out.println("--- Top 3 mes valorats ---");
stats.topValorats(inventari, 3)
.forEach(m -> System.out.printf(" %.2f %s%n", m.getValoracio(), m.getTitol()));
}
}=== INFORME BIBLIOTECH ===
Inventari [LLIBRE=5, REVISTA=2, DVD=1]
--- Materials per tipus ---
LLIBRE 5 unitats, 2119 pagines, valoracio 4,62
REVISTA 2 unitats, 140 pagines, valoracio 3,90
DVD 1 unitats, 0 pagines, valoracio 4,10
--- Prestecs per empleat i tipus ---
Diego Alonso {LLIBRE=1, REVISTA=2}
Marta Ruiz {LLIBRE=3, DVD=1}
Nuria Vidal {LLIBRE=2}
--- Titols per empleat ---
Diego Alonso [Java Magazine 04, Java Magazine 05, Refactoritzacio]
Marta Ruiz [Codi Net, Curs de Spring, Java Eficac, Patrons de Disseny]
Nuria Vidal [Domain-Driven Design, Java Eficac]
Qui mes llegeix: Marta Ruiz
Mitjana de pagines del prestat: 428,3
--- Top 3 mes valorats ---
4,85 Java Eficac
4,72 Refactoritzacio
4,60 Patrons de DissenyLa comparació en números: 74 línies de bucles amb acumuladors, mapes manuals i comprovacions de null, contra 12 línies d'expressions que es llegeixen com el seu propi enunciat. I la versió amb streams fa més: retorna Optional en lloc de null, fa servir EnumMap per a les claus d'enum i ordena les claus amb TreeMap.
Optional: per a què es va crear
Optional: per a què es va crearCanviem de tema, encara que no de lliçó: Optional va arribar a la mateixa versió i per la mateixa filosofia.
Recorda el cercarPerReferencia de BiblioTech del mòdul 6:
public Prestec cercarPerReferencia(String referencia) {
for (Prestec p : prestecs) {
if (p.getReferencia().equals(referencia)) {
return p;
}
}
return null; // "no trobat"
}El problema no és el null en si: és que la signatura menteix. Prestec cercarPerReferencia(String) promet retornar un Prestec. Qui la crida no té cap indicació que de vegades no ho farà:
Prestec p = registre.cercarPerReferencia("PR-2026-0041");
System.out.println(p.getEmpleat()); // NullPointerException si no existeixI aquest error té una propietat especialment dolenta: el compilador no et pot ajudar. No hi ha cap tipus que distingeixi "un Prestec" de "un Prestec o res". Tony Hoare, que va introduir la referència nul·la a ALGOL el 1965, la va anomenar públicament "el meu error de mil milions de dòlars" per la quantitat de fallades, vulnerabilitats i hores de depuració que ha causat.
Optional<T> és la resposta: un contenidor que pot tenir un valor o estar buit, i el tipus del qual ho declara explícitament.
public Optional<Prestec> cercarPerReferencia(String referencia) {
for (Prestec p : prestecs) {
if (p.getReferencia().equals(referencia)) {
return Optional.of(p);
}
}
return Optional.empty();
}Ara la signatura diu la veritat, i qui la fa servir no la pot ignorar:
Optional<Prestec> p = registre.cercarPerReferencia("PR-2026-0041");
// p.getEmpleat(); // NO COMPILA: Optional no te getEmpleat()
String empleat = p.map(Prestec::getEmpleat).orElse("(sense prestec)");El compilador t'obliga a enfrontar-te a l'absència. Aquest és tot el valor d'Optional, i és enorme.
Fixa't a més en la connexió amb 10-01: Optional<T> és el cas particular de Resultat<T> en què la fallada no porta informació. Si necessites saber per què no hi ha valor, fes servir Resultat<T>; si n'hi ha prou de saber que no n'hi ha, Optional<T>.
- Crear i consumir un
Optional
OptionalCreació
// of: el valor NO pot ser null. Si ho es, NullPointerException immediata.
Optional<Llibre> a = Optional.of(llibre);
// ofNullable: accepta null i produeix un Optional buit
Optional<Llibre> b = Optional.ofNullable(potSerNull);
// empty: explicitament buit
Optional<Llibre> c = Optional.empty();| Mètode | Si l'argument és null |
Quan fer-lo servir |
|---|---|---|
Optional.of(x) |
NullPointerException |
Quan saps que hi ha valor. La fallada ràpida és desitjable |
Optional.ofNullable(x) |
Retorna buit | En embolcallar una API antiga que retorna null |
Optional.empty() |
— | Quan no hi ha valor |
Optional.of(null) és un error, no un Optional buit. És un error freqüent:
// MALAMENT: si el mapa no te la clau, aixo llanca NullPointerException
Optional<Llibre> llibre = Optional.of(mapa.get(isbn));
// BE
Optional<Llibre> llibre = Optional.ofNullable(mapa.get(isbn));Consum: transformar sense desembolcallar
La forma idiomàtica de fer servir Optional no és preguntar si hi ha valor, sinó encadenar operacions que s'apliquen només si n'hi ha.
// map: transforma el valor si existeix. Si esta buit, continua buit.
Optional<String> titol = cataleg.cercarPerIsbn(isbn).map(Llibre::getTitol);
// Encadenant
String majuscules = cataleg.cercarPerIsbn(isbn)
.map(Llibre::getTitol)
.map(String::toUpperCase)
.orElse("DESCONEGUT");
// filter: buida l Optional si no compleix
Optional<Llibre> llargIDisponible = cataleg.cercarPerIsbn(isbn)
.filter(l -> l.getPagines() > 400)
.filter(l -> !l.estaPrestat());flatMap: quan la funció ja retorna Optional.
public class Llibre {
public Optional<String> getEditorial() { ... } // pot no tenir ne
}
// Amb map: et queden Optionals imbricats
Optional<Optional<String>> malament = cataleg.cercarPerIsbn(isbn).map(Llibre::getEditorial);
// Amb flatMap: aplanat
Optional<String> be = cataleg.cercarPerIsbn(isbn).flatMap(Llibre::getEditorial);És exactament la mateixa distinció que als streams (apartat 7): map quan la funció retorna un valor, flatMap quan retorna un contenidor.
Consum: obtenir el valor
// orElse: valor per defecte
String titol = opt.map(Llibre::getTitol).orElse("(desconegut)");
// orElseGet: valor per defecte CALCULAT MANDROSAMENT (apartat 18)
Llibre llibre = opt.orElseGet(() -> carregarDeLaXarxa(isbn));
// orElseThrow: llancar si no n hi ha
Llibre llibre2 = opt.orElseThrow(() ->
new BiblioTechException("No existeix el material " + isbn));
// orElseThrow() sense arguments (Java 10): llanca NoSuchElementException
Llibre llibre3 = opt.orElseThrow();
// or: un altre Optional si aquest esta buit (Java 9)
Optional<Llibre> trobat = catalegLocal.cercarPerIsbn(isbn)
.or(() -> catalegRemot.cercarPerIsbn(isbn))
.or(() -> catalegHistoric.cercarPerIsbn(isbn));Aquest or encadenat és un patró molt útil: cerca al primer lloc, si no hi és al segon, si no al tercer — i els Supplier només s'avaluen si calen.
Consum: executar una acció
// ifPresent: fer alguna cosa si hi ha valor
cataleg.cercarPerIsbn(isbn).ifPresent(l -> System.out.println("Trobat: " + l.getTitol()));
// ifPresentOrElse: i si no, una altra cosa (Java 9)
cataleg.cercarPerIsbn(isbn).ifPresentOrElse(
l -> System.out.println("Trobat: " + l.getTitol()),
() -> System.out.println("No hi ha cap material amb ISBN " + isbn));Optional.stream(): el pont amb els streams (Java 9)
Converteix un Optional en un stream de zero o un element. El seu ús estrella és filtrar els buits d'una col·lecció d'Optional:
List<String> isbns = List.of("978-0000000001", "978-9999999999", "978-0000000003");
// ABANS de Java 9: dos passos
List<Llibre> trobats1 = isbns.stream()
.map(cataleg::cercarPerIsbn) // Stream<Optional<Llibre>>
.filter(Optional::isPresent)
.map(Optional::get)
.toList();
// DES DE Java 9: un pas
List<Llibre> trobats2 = isbns.stream()
.map(cataleg::cercarPerIsbn) // Stream<Optional<Llibre>>
.flatMap(Optional::stream) // Stream<Llibre>, sense els buits
.toList();La segona forma és la idiomàtica i no requereix get() en cap moment.
orElse enfront d'orElseGet
orElse enfront d'orElseGetSemblen el mateix i no ho són, i la diferència importa de debò.
T orElse(T altre) // rep un VALOR ja calculat
T orElseGet(Supplier<? extends T> proveidor) // rep una FUNCIO que el calculaorElse avalua el seu argument SEMPRE, hi hagi valor o no. orElseGet només l'avalua si l'Optional està buit. Demostració:
package com.nexussoftware.bibliotech;
import java.util.Optional;
public class OrElseEnfrontDeOrElseGet {
private static Llibre carregarDeLaXarxa(String isbn) {
System.out.println(" >>> CONSULTANT LA XARXA per a " + isbn + " (300 ms)");
return new Llibre(isbn, "Recuperat de la xarxa");
}
public static void main(String[] args) {
Optional<Llibre> ambValor = Optional.of(new Llibre("978-0000000001", "Java Eficac"));
System.out.println("--- orElse amb valor present ---");
Llibre a = ambValor.orElse(carregarDeLaXarxa("978-0000000001"));
System.out.println("Resultat: " + a.getTitol());
System.out.println();
System.out.println("--- orElseGet amb valor present ---");
Llibre b = ambValor.orElseGet(() -> carregarDeLaXarxa("978-0000000001"));
System.out.println("Resultat: " + b.getTitol());
System.out.println();
System.out.println("--- orElseGet amb Optional buit ---");
Llibre c = Optional.<Llibre>empty().orElseGet(() -> carregarDeLaXarxa("978-9999999999"));
System.out.println("Resultat: " + c.getTitol());
}
}--- orElse amb valor present ---
>>> CONSULTANT LA XARXA per a 978-0000000001 (300 ms)
Resultat: Java Eficac
--- orElseGet amb valor present ---
Resultat: Java Eficac
--- orElseGet amb Optional buit ---
>>> CONSULTANT LA XARXA per a 978-9999999999 (300 ms)
Resultat: Recuperat de la xarxaMira la primera secció amb atenció. L'Optional tenia valor, i tot i així es va consultar la xarxa — 300 mil·lisegons malbaratats i una petició innecessària a un servei extern. El resultat retornat va ser el correcte, així que l'error és invisible: només es manifesta com a lentitud inexplicable.
I pot ser pitjor que lent:
// Un comptador que s incrementa encara que el valor existeixi
int seguentId = opt.orElse(generarNouId()); // genera un ID sempre!
// Un Optional buit que produeix NullPointerException
String s = opt.orElse(altre.getTitol()); // si altre es null, explota sempre| Situació | Fes servir |
|---|---|
| El valor per defecte és una constant o una variable ja calculada | orElse |
| El valor per defecte requereix càlcul, E/S, xarxa o consulta | orElseGet |
| El valor per defecte té efectes secundaris | orElseGet |
| Davant del dubte | orElseGet |
// BE: constant
String titol = opt.map(Llibre::getTitol).orElse("(sense titol)");
// BE: calcul car, avaluat nomes si cal
Llibre llibre = opt.orElseGet(() -> carregarDeLaXarxa(isbn));
// BE: excepcio construida nomes si cal
// (construir una excepcio captura la traca de pila, que no es gratis)
Llibre llibre2 = opt.orElseThrow(() -> new BiblioTechException("No existeix " + isbn));Aquest últim punt explica per què orElseThrow rep un Supplier i no una excepció ja construïda: crear una excepció n'omple la traça de pila, que és una operació mesurable (10-07).
- Els antipatrons d'
Optional
OptionalOptional es fa servir malament amb molta freqüència. Aquests cinc són els antipatrons que cal reconèixer i evitar.
Antipatró 1: isPresent() + get()
// MALAMENT: es un if disfressat, amb mes soroll que el null original
Optional<Llibre> opt = cataleg.cercarPerIsbn(isbn);
if (opt.isPresent()) {
System.out.println(opt.get().getTitol());
} else {
System.out.println("No trobat");
}Això no aporta res sobre comprovar null: mateixes línies, mateixa estructura, i a sobre hi has afegit un objecte embolcall. El valor d'Optional és a l'API funcional:
// BE
cataleg.cercarPerIsbn(isbn).ifPresentOrElse(
l -> System.out.println(l.getTitol()),
() -> System.out.println("No trobat"));
// O si necessites el valor
String titol = cataleg.cercarPerIsbn(isbn)
.map(Llibre::getTitol)
.orElse("No trobat");Regla: si escrius .get(), alguna cosa va malament. De fet, Optional.get() està marcat com a candidat a la depreciació precisament pel malament que es fa servir; la seva alternativa és orElseThrow(), que almenys deixa clar que pot fallar.
Antipatró 2: Optional com a paràmetre d'un mètode
Tres raons per les quals és un error:
- Qui crida ha d'embolcallar:
cercar("java", Optional.of(LLIBRE))és més sorollós quecercar("java", LLIBRE). - Un
Optionalpot sernull. Res no impedeixcercar("java", null), i llavors tens unNullPointerExceptionmés unOptional. El pitjor dels dos mons. - Hi ha alternatives millors: sobrecàrrega de mètodes.
// BE
public List<Llibre> cercar(String text) {
return cercar(text, null);
}
public List<Llibre> cercar(String text, TipusMaterial tipus) { ... }Optional es va dissenyar per a tipus de RETORN. Això està escrit explícitament a la seva documentació i a les notes de disseny del JDK.
Antipatró 3: Optional com a camp d'una entitat
Optionalno és serialitzable (07-05), així que trenca la serialització de l'entitat.- Afegeix un objecte per camp i per instància: amb un milió de llibres, un milió d'objectes extra.
- Els frameworks de persistència no el gestionen bé (Hibernate, 11-03).
// BE: el camp es null; l ACCESSOR retorna Optional
public class Llibre {
private String editorial; // pot ser null
public Optional<String> getEditorial() {
return Optional.ofNullable(editorial);
}
}Així, el món exterior no veu mai un null i l'interior no paga l'embolcall.
Antipatró 4: Optional d'una col·lecció
Ara qui crida ha de comprovar dues absències: que l'Optional tingui valor i que la llista no estigui buida. I no hi ha cap diferència semàntica útil entre "no hi ha llista" i "la llista és buida".
// BE: una llista buida JA significa "no hi ha res"
public List<Prestec> prestecsDe(String empleat) {
return prestecs.stream()
.filter(p -> p.getEmpleat().equals(empleat))
.toList(); // buida si no n hi ha cap
}Regla general: per a col·leccions, retorna la col·lecció buida, mai null ni Optional. És de les recomanacions més antigues i més útils d'Effective Java.
Antipatró 5: Optional.of(null) i retornar null des d'un Optional
// MALAMENT: NullPointerException garantida
return Optional.of(mapa.get(clau));
// PITJOR: un metode que retorna Optional i de vegades retorna null
public Optional<Llibre> cercar(String isbn) {
if (isbn == null) return null; // destrueix tot el proposit!
...
}Un mètode que retorna Optional no ha de retornar mai null. Qui el crida farà .map(...) sense comprovar, perquè per a això hi ha Optional, i rebrà un NullPointerException al lloc on precisament confiava que no n'hi podia haver.
- Què retornar quan no hi ha valor
Taula de decisió que resumeix el criteri:
| Situació | Retorna | Per què |
|---|---|---|
Cerca que pot no trobar (cercarPerIsbn) |
Optional<T> |
L'absència és un resultat normal i esperable |
| Col·lecció que pot estar buida | Col·lecció buida (List.of()) |
Buit ja significa "res"; mai null ni Optional |
| Array que pot estar buit | Array buit | Igual que les col·leccions |
| Cadena que pot estar buida | "" |
Tret que "buida" i "absent" signifiquin coses diferents |
| Operació que falla de manera esperable, amb motiu | Resultat<T> (10-01) |
L'error porta informació |
| Operació que falla per un error de programació | Excepció no comprovada | IllegalArgumentException, IllegalStateException |
| Operació que falla per causa externa recuperable | Excepció comprovada (06-01) | IOException, BiblioTechException |
| Camp intern que pot no tenir valor | null al camp, Optional al getter |
Sense cost de memòria ni de serialització |
| Paràmetre opcional d'un mètode | Sobrecàrrega del mètode | Mai Optional com a paràmetre |
| Mitjana o agregat d'un stream buit | OptionalDouble / OptionalInt |
És el que retorna l'API |
- BiblioTech refactoritzat
Tanquem aplicant-ho tot alhora. Repositori<T> de 10-01 es modernitza amb streams i Optional:
package com.nexussoftware.bibliotech.servei;
import com.nexussoftware.bibliotech.domini.Identificable;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.concurrent.ConcurrentHashMap;
/**
* Repositori generic (10-01) amb API de streams i Optional (10-04).
*/
public class Repositori<T extends Identificable> {
private final Map<String, T> perId = new ConcurrentHashMap<>();
private final String nom;
public Repositori(String nom) {
this.nom = Objects.requireNonNull(nom, "nom");
}
public Optional<T> guardar(T entitat) {
Objects.requireNonNull(entitat, "entitat");
return Optional.ofNullable(perId.put(entitat.getId(), entitat));
}
/** La signatura DIU que pot no trobar res. */
public Optional<T> cercarPerId(String id) {
return Optional.ofNullable(perId.get(id));
}
public Optional<T> eliminar(String id) {
return Optional.ofNullable(perId.remove(id));
}
/**
* Exposa un stream en lloc d una copia de la llista.
* Qui crida decideix que fer sense que el repositori
* hagi d oferir un metode per cada consulta.
*/
public Stream<T> flux() {
return perId.values().stream();
}
/** Mai null: una llista buida ja significa "cap". */
public List<T> cercar(Predicate<? super T> criteri) {
return flux().filter(criteri).toList();
}
/** El primer que compleixi, o buit. */
public Optional<T> cercarPrimer(Predicate<? super T> criteri) {
return flux().filter(criteri).findFirst();
}
public List<T> llistarOrdenat(Comparator<? super T> ordre) {
return flux().sorted(ordre).toList();
}
public long comptar(Predicate<? super T> criteri) {
return flux().filter(criteri).count();
}
public boolean existeixAlgun(Predicate<? super T> criteri) {
return flux().anyMatch(criteri);
}
/** Indexa per qualsevol clau derivada. */
public <K> Map<K, List<T>> agrupar(Function<? super T, ? extends K> classificador) {
return flux().collect(Collectors.groupingBy(classificador));
}
public int mida() {
return perId.size();
}
@Override
public String toString() {
return "Repositori[" + nom + ", " + perId.size() + " entitats]";
}
}I GestorPrestecs deixa de comprovar null:
package com.nexussoftware.bibliotech.servei;
import com.nexussoftware.bibliotech.anotacions.Auditable;
import com.nexussoftware.bibliotech.domini.*;
import java.util.*;
import java.util.stream.Collectors;
public class GestorPrestecs implements ServeiPrestecs {
private final Repositori<Material> cataleg;
private final Repositori<Prestec> prestecs;
public GestorPrestecs(Repositori<Material> cataleg, Repositori<Prestec> prestecs) {
this.cataleg = cataleg;
this.prestecs = prestecs;
}
/**
* ABANS: cercarPerId retornava null i calia comprovar ho.
* ARA: l absencia s encadena i produeix la fallada correcta.
*/
@Override
@Auditable(value = "prestar", nivell = Gravetat.ALTA, registrarArguments = true)
public Resultat<Prestec> prestar(String isbn, String empleat) {
return cataleg.cercarPerId(isbn)
.map(material -> registrar(material, empleat))
.orElseGet(() -> Resultat.fallada("No existeix cap material amb ISBN " + isbn));
}
private Resultat<Prestec> registrar(Material material, String empleat) {
if (material.estaPrestat()) {
return Resultat.fallada("El material '" + material.getTitol() + "' ja esta prestat");
}
material.marcarPrestat(empleat);
Prestec prestec = new Prestec(seguentReferencia(), material.getId(), empleat);
prestecs.guardar(prestec);
return Resultat.exit(prestec);
}
/** Llista buida, mai null. */
@Override
public List<Prestec> prestecsDe(String empleat) {
return prestecs.cercar(p -> p.getEmpleat().equals(empleat));
}
/** Optional en lloc de null. */
@Override
public Optional<Prestec> cercarPerReferencia(String referencia) {
return prestecs.cercarPerId(referencia);
}
/** El titol del material prestat a un empleat que mes retard porta. */
public Optional<String> materialMesRetardatDe(String empleat) {
return prestecs.flux()
.filter(p -> p.getEmpleat().equals(empleat))
.filter(Prestec::teRetard)
.max(Comparator.comparingInt(Prestec::getDiesDeRetard))
.flatMap(p -> cataleg.cercarPerId(p.getIsbn())) // flatMap: retorna Optional
.map(Material::getTitol);
}
/** Deute total de multes per empleat, nomes dels qui deuen alguna cosa. */
public Map<String, Double> deutePerEmpleat(CalculadoraMultes calculadora) {
return prestecs.flux()
.filter(Prestec::teRetard)
.collect(Collectors.groupingBy(
Prestec::getEmpleat,
TreeMap::new,
Collectors.summingDouble(p -> calculadora.calcular(p.getDiesDeRetard()))));
}
private String seguentReferencia() {
return String.format("PR-2026-%04d", prestecs.mida() + 1);
}
}L'ús, sense ni una sola comprovació de null:
gestor.prestar("978-0000000001", "Marta Ruiz")
.map(Prestec::getId)
.map(id -> "Prestec registrat: " + id)
.ifPresent(System.out::println);
gestor.materialMesRetardatDe("Diego Alonso")
.ifPresentOrElse(
t -> System.out.println("Retornar urgentment: " + t),
() -> System.out.println("Diego Alonso no te retards"));
gestor.deutePerEmpleat(calculadora)
.forEach((empleat, deute) -> System.out.printf(" %-14s %.2f €%n", empleat, deute));Prestec registrat: PR-2026-0009
Retornar urgentment: Refactoritzacio
Diego Alonso 3,25 €
Marta Ruiz 1,50 €Errors Comuns i Consells
1. Reutilitzar un stream. IllegalStateException: stream has already been operated upon or closed. Un stream és d'un sol ús; crea'n un de nou des de la font.
2. Oblidar l'operació terminal. llista.stream().filter(...) no fa absolutament res. Si el teu stream no produeix resultat ni efecte, comprova que acaba en collect, forEach, count, reduce o similar.
3. Modificar la col·lecció d'origen mentre es recorre. Produeix ConcurrentModificationException, igual que amb un for-each (05-02). Recull en una llista nova.
4. Fer servir forEach amb un add a una llista externa. És l'estil imperatiu disfressat, i en paral·lel corromp la llista. Fes servir collect.
5. Confondre map amb flatMap. Si el teu resultat és List<List<X>> o Optional<Optional<X>>, volies flatMap. I recorda que la funció de flatMap ha de retornar un Stream, així que cal el .stream() interior.
6. Collectors.toMap amb claus duplicades. IllegalStateException: Duplicate key. Passa la funció de mescla com a tercer argument, o fes servir groupingBy si en realitat volies agrupar.
7. orElse amb un càlcul car. S'avalua sempre, tingui valor o no l'Optional. Fes servir orElseGet així que hi hagi càlcul, E/S o efectes secundaris.
8. isPresent() + get(). És un if amb més soroll. Fes servir map, filter, ifPresent, ifPresentOrElse o orElse.
9. Optional com a paràmetre o com a camp. Es va dissenyar per a tipus de retorn. Com a paràmetre, fes servir sobrecàrrega; com a camp, null a dins i Optional al getter.
10. parallelStream() sense mesurar. Amb pocs elements és més lent; amb LinkedList és molt més lent; amb operacions que bloquegen, enverina el commonPool de tota l'aplicació.
11. No tancar els streams de fitxers. Files.lines() i Files.walk() mantenen recursos oberts. Sempre en try-with-resources.
12. Fer servir peek per a una altra cosa que depurar. No es garanteix que s'executi, i des de Java 9 count() se'l pot saltar per complet.
13. sorted() sobre un stream infinit. Es penja: necessita tots els elements. Posa-hi limit abans.
Consell 1: una operació per línia. Cada .filter, .map i .collect a la seva pròpia línia, alineats. Un stream de set operacions en una sola línia és il·legible.
Consell 2: extreu els predicats i funcions complexos. Si la lambda ocupa més de dues línies, dona-li nom:
Predicate<Llibre> esLlargIEstaPrestat = l -> l.getPagines() > 400 && l.estaPrestat();
List<Llibre> resultat = cataleg.stream().filter(esLlargIEstaPrestat).toList();Consell 3: prefereix les referències a mètodes. map(Llibre::getTitol) es llegeix millor que map(l -> l.getTitol()).
Consell 4: no forcis tot a streams. Un bucle amb break complex, amb índexs, o que modifica diverses variables alhora, es llegeix millor com a bucle. Els streams brillen en transformació i agregació, no en control de flux intricat.
Consell 5: posa filter abans que map i sorted al final. Menys elements a transformar i menys elements a ordenar. El resultat és el mateix i la feina, molt menor.
Consell 6: fes servir streams de primitius amb nombres. mapToInt(...).sum() en lloc de map(...).reduce(0, Integer::sum): més clar i sense autoboxing.
Exercicis
Exercici 1: el tauler de la biblioteca
Escriu una classe TaulerBiblioTech que generi, fent servir exclusivament streams, un informe de text amb:
- Total de materials i desglossament per tipus.
- Els 3 materials més valorats, amb la seva valoració.
- Préstecs actius agrupats per empleat, amb el nombre i la llista de títols ordenada.
- L'empleat amb més préstecs (
Optional) i el que porta més dies de retard acumulats. - Mitjana, mínim i màxim de pàgines dels llibres, en un sol recorregut.
- Materials mai prestats (els que no apareixen en cap préstec).
- Un histograma de text amb el nombre de préstecs per dia de la setmana (fes servir
diaPrestec % 7).
Requisits: ni un sol bucle for, ni un sol null, i cap .get() sobre un Optional.
Exercici 2: analitzador de logs amb streams mandrosos
BiblioTech genera un log amb línies del format DATA|NIVELL|OPERACIÓ|EMPLEAT|MIL·LISEGONS:
Escriu AnalitzadorLog que, llegint el fitxer una sola vegada i sense carregar-lo a memòria, produeixi:
- Nombre d'operacions per nivell.
- Les 5 operacions més lentes, amb el seu empleat i durada.
- Durada mitjana per tipus d'operació.
- Empleats amb almenys un
SEVERE, ordenats. - Percentatge d'operacions que superen els 200 ms.
- Un
Optional<String>amb la primera línia corrupta trobada (camps insuficients o mil·lisegons no numèrics), o buit si el fitxer està bé.
Fes servir un record LiniaLog i Optional per a l'anàlisi de cada línia. Genera un fitxer de prova de 50.000 línies amb streams.
Exercici 3: CacheFitxes amb Optional i mesurament del paral·lelisme
Refactoritza el CacheFitxes de BiblioTech i mesura el paral·lelisme honestament.
- Reescriu
CacheFitxesperquècercar(String isbn)retorniOptional<Fitxa>i tingui unobtenirOCalcular(String, Function<String, Fitxa>). - Afegeix
List<Fitxa> cercarTotes(Collection<String> isbns)que retorni només les trobades fent servirflatMap(Optional::stream). - Afegeix
Map<Boolean, List<String>> classificar(Collection<String> isbns)ambpartitioningByper separar els que són a la memòria cau dels que no. - Escriu un banc de proves que compari
stream()enfront deparallelStream()per a un càlcul car de fitxes, amb escalfament previ i diverses repeticions, sobre 100, 10.000 i 1.000.000 d'ISBN, i imprimeixi una taula amb la millora en cada cas. - Demostra amb codi per què acumular en un
ArrayListambforEachen paral·lel produeix resultats incorrectes.
Solucions
Solució 1
package com.nexussoftware.bibliotech.presentacio;
import com.nexussoftware.bibliotech.domini.*;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* Tauler de la biblioteca generat enterament amb streams.
* Ni un for, ni un null, ni un Optional.get().
*/
public class TaulerBiblioTech {
private final List<Material> inventari;
private final List<Prestec> prestecs;
public TaulerBiblioTech(List<Material> inventari, List<Prestec> prestecs) {
this.inventari = List.copyOf(inventari);
this.prestecs = List.copyOf(prestecs);
}
public String generar() {
StringBuilder sb = new StringBuilder();
sb.append("=".repeat(58)).append('\n');
sb.append(" TAULER DE BIBLIOTECH -- Nexus Software\n");
sb.append("=".repeat(58)).append('\n');
seccioInventari(sb);
seccioTop(sb);
seccioPrestecs(sb);
seccioRanquing(sb);
seccioPagines(sb);
seccioMaiPrestats(sb);
seccioHistograma(sb);
return sb.toString();
}
// --- 1. Inventari per tipus ---
private void seccioInventari(StringBuilder sb) {
Map<TipusMaterial, Long> perTipus = inventari.stream()
.collect(Collectors.groupingBy(Material::getTipus,
() -> new EnumMap<>(TipusMaterial.class),
Collectors.counting()));
sb.append("\n1. INVENTARI (").append(inventari.size()).append(" materials)\n");
perTipus.forEach((tipus, n) ->
sb.append(String.format(" %-10s %3d %s%n", tipus, n, "#".repeat(n.intValue()))));
}
// --- 2. Top 3 mes valorats ---
private void seccioTop(StringBuilder sb) {
sb.append("\n2. MILLOR VALORATS\n");
inventari.stream()
.sorted(Comparator.comparingDouble(Material::getValoracio).reversed())
.limit(3)
.forEach(m -> sb.append(String.format(" %.2f %s%n",
m.getValoracio(), m.getTitol())));
}
// --- 3. Prestecs per empleat ---
private void seccioPrestecs(StringBuilder sb) {
Map<String, List<String>> perEmpleat = prestecs.stream()
.collect(Collectors.groupingBy(
Prestec::getEmpleat,
TreeMap::new,
Collectors.mapping(Prestec::getTitolMaterial,
Collectors.collectingAndThen(
Collectors.toList(),
llista -> llista.stream().sorted().toList()))));
sb.append("\n3. PRESTECS ACTIUS\n");
perEmpleat.forEach((empleat, titols) -> {
sb.append(String.format(" %-14s (%d)%n", empleat, titols.size()));
titols.forEach(t -> sb.append(" - ").append(t).append('\n'));
});
}
// --- 4. Ranquings, amb Optional ---
private void seccioRanquing(StringBuilder sb) {
sb.append("\n4. RANQUINGS\n");
Optional<String> mesPrestecs = prestecs.stream()
.collect(Collectors.groupingBy(Prestec::getEmpleat, Collectors.counting()))
.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey);
sb.append(" Mes prestecs: ")
.append(mesPrestecs.orElse("(no hi ha prestecs)")).append('\n');
Optional<String> mesRetard = prestecs.stream()
.filter(Prestec::teRetard)
.collect(Collectors.groupingBy(Prestec::getEmpleat,
Collectors.summingInt(Prestec::getDiesDeRetard)))
.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(e -> e.getKey() + " (" + e.getValue() + " dies)");
sb.append(" Mes retard: ")
.append(mesRetard.orElse("(ningu amb retard)")).append('\n');
}
// --- 5. Estadistiques de pagines en UN recorregut ---
private void seccioPagines(StringBuilder sb) {
IntSummaryStatistics stats = inventari.stream()
.filter(Llibre.class::isInstance)
.map(Llibre.class::cast)
.mapToInt(Llibre::getPagines)
.summaryStatistics();
sb.append("\n5. PAGINES DELS LLIBRES\n");
sb.append(String.format(" llibres=%d mitjana=%.1f min=%d max=%d total=%d%n",
stats.getCount(), stats.getAverage(),
// Amb 0 llibres, getMin() retorna MAX_VALUE: cal contemplar ho
stats.getCount() == 0 ? 0 : stats.getMin(),
stats.getCount() == 0 ? 0 : stats.getMax(),
stats.getSum()));
}
// --- 6. Mai prestats ---
private void seccioMaiPrestats(StringBuilder sb) {
// Conjunt d ISBN prestats alguna vegada: la cerca passa a O(1)
Set<String> prestatsAlgunaVegada = prestecs.stream()
.map(Prestec::getIsbn)
.collect(Collectors.toSet());
List<String> mai = inventari.stream()
.filter(m -> !prestatsAlgunaVegada.contains(m.getId()))
.map(Material::getTitol)
.sorted()
.toList();
sb.append("\n6. MAI PRESTATS (").append(mai.size()).append(")\n");
sb.append(mai.isEmpty()
? " (tot el cataleg ha sortit alguna vegada)\n"
: mai.stream().collect(Collectors.joining("\n ", " ", "\n")));
}
// --- 7. Histograma per dia de la setmana ---
private void seccioHistograma(StringBuilder sb) {
String[] dies = { "Dilluns", "Dimarts", "Dimecres", "Dijous", "Divendres", "Dissabte", "Diumenge" };
Map<Integer, Long> perDia = prestecs.stream()
.collect(Collectors.groupingBy(p -> p.getDiaPrestec() % 7,
Collectors.counting()));
long maxim = perDia.values().stream().mapToLong(Long::longValue).max().orElse(1);
sb.append("\n7. PRESTECS PER DIA DE LA SETMANA\n");
IntStream.range(0, 7).forEach(i -> {
long n = perDia.getOrDefault(i, 0L);
int barra = (int) (n * 30 / maxim);
sb.append(String.format(" %-10s %3d %s%n", dies[i], n, "█".repeat(barra)));
});
}
}==========================================================
TAULER DE BIBLIOTECH -- Nexus Software
==========================================================
1. INVENTARI (8 materials)
LLIBRE 5 #####
REVISTA 2 ##
DVD 1 #
2. MILLOR VALORATS
4,85 Java Eficac
4,72 Refactoritzacio
4,60 Patrons de Disseny
3. PRESTECS ACTIUS
Diego Alonso (3)
- Java Magazine 04
- Java Magazine 05
- Refactoritzacio
Marta Ruiz (4)
- Codi Net
- Curs de Spring
- Java Eficac
- Patrons de Disseny
Nuria Vidal (2)
- Domain-Driven Design
- Java Eficac
4. RANQUINGS
Mes prestecs: Marta Ruiz
Mes retard: Diego Alonso (23 dies)
5. PAGINES DELS LLIBRES
llibres=5 mitjana=423.8 min=380 max=464 total=2119
6. MAI PRESTATS (1)
UML Destillat
7. PRESTECS PER DIA DE LA SETMANA
Dilluns 3 ██████████████████████████████
Dimarts 1 ██████████
Dimecres 2 ████████████████████
Dijous 0
Divendres 2 ████████████████████
Dissabte 1 ██████████
Diumenge 0Comentaris. Quatre punts.
El conjunt d'ISBN prestats és una decisió de rendiment, no d'estil. Sense ell, la secció 6 seria filter(m -> prestecs.stream().noneMatch(p -> p.getIsbn().equals(m.getId()))), que és un stream imbricat dins d'un altre: O(n·m). Amb el Set, és O(n+m). Els streams no eximeixen de pensar en la complexitat (mòdul 5).
IntSummaryStatistics amb zero elements retorna MAX_VALUE com a mínim i MIN_VALUE com a màxim, cosa que és matemàticament coherent (el mínim del conjunt buit és l'infinit) i visualment absurda. Cal contemplar-ho.
getOrDefault(i, 0L) a l'histograma evita el null que retornaria get() per a un dia sense préstecs. És l'equivalent d'orElse per a mapes (05-05).
collectingAndThen és la peça que permet ordenar dins d'un grup. Recull en una llista i hi aplica una funció final. Sense ell, mapping(..., toList()) donaria les llistes en ordre d'aparició.
Solució 2
package com.nexussoftware.bibliotech.servei;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Analitza el log de BiblioTech amb streams mandrosos.
* MAI carrega el fitxer a memoria: un log de 2 GB es processa igual.
*/
public class AnalitzadorLog {
/** Una linia valida del log. Optional per a l analisi (04-07 + 10-04). */
public record LiniaLog(String data, String nivell, String operacio,
String empleat, long millisegons) {
static Optional<LiniaLog> analitzar(String linia) {
String[] c = linia.split("\\|", -1);
if (c.length != 5) {
return Optional.empty();
}
try {
return Optional.of(new LiniaLog(
c[0].strip(), c[1].strip(), c[2].strip(), c[3].strip(),
Long.parseLong(c[4].strip())));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
}
public record Informe(Map<String, Long> perNivell,
List<LiniaLog> mesLentes,
Map<String, Double> mitjanaPerOperacio,
List<String> empleatsAmbErrors,
double percentatgeLentes,
long totalValides,
long totalCorruptes) { }
private final Path fitxer;
public AnalitzadorLog(Path fitxer) {
this.fitxer = fitxer;
}
/**
* UN SOL recorregut del fitxer: es materialitzen les linies valides
* a memoria una vegada i se n deriven tots els agregats.
* Per a fitxers que no caben a memoria, cada agregat necessitaria
* el seu propi recorregut mandros.
*/
public Informe analitzar() throws IOException {
List<LiniaLog> valides;
long total;
try (Stream<String> linies = Files.lines(fitxer, StandardCharsets.UTF_8)) {
valides = linies
.filter(l -> !l.isBlank())
.map(LiniaLog::analitzar)
.flatMap(Optional::stream) // descarta les buides sense get()
.toList();
}
total = comptarLiniesNoBuides();
Map<String, Long> perNivell = valides.stream()
.collect(Collectors.groupingBy(LiniaLog::nivell,
TreeMap::new, Collectors.counting()));
List<LiniaLog> mesLentes = valides.stream()
.sorted(Comparator.comparingLong(LiniaLog::millisegons).reversed())
.limit(5)
.toList();
Map<String, Double> mitjanaPerOperacio = valides.stream()
.collect(Collectors.groupingBy(LiniaLog::operacio,
TreeMap::new,
Collectors.averagingLong(LiniaLog::millisegons)));
List<String> empleatsAmbErrors = valides.stream()
.filter(l -> "SEVERE".equals(l.nivell()))
.map(LiniaLog::empleat)
.distinct()
.sorted()
.toList();
long lentes = valides.stream().filter(l -> l.millisegons() > 200).count();
double percentatge = valides.isEmpty() ? 0.0 : lentes * 100.0 / valides.size();
return new Informe(perNivell, mesLentes, mitjanaPerOperacio, empleatsAmbErrors,
percentatge, valides.size(), total - valides.size());
}
/** La primera linia corrupta, amb processament MANDROS: para en trobar la. */
public Optional<String> primeraCorrupta() throws IOException {
try (Stream<String> linies = Files.lines(fitxer, StandardCharsets.UTF_8)) {
return linies
.filter(l -> !l.isBlank())
.filter(l -> LiniaLog.analitzar(l).isEmpty())
.findFirst(); // CURTCIRCUITA: no llegeix la resta del fitxer
}
}
private long comptarLiniesNoBuides() throws IOException {
try (Stream<String> linies = Files.lines(fitxer, StandardCharsets.UTF_8)) {
return linies.filter(l -> !l.isBlank()).count();
}
}
// ------------------------------------------------------------------
public static void main(String[] args) throws IOException {
Path log = Path.of("bibliotech.log");
generarLogDeProva(log, 50_000);
AnalitzadorLog analitzador = new AnalitzadorLog(log);
Informe informe = analitzador.analitzar();
System.out.println("=== ANALISI DEL LOG ===");
System.out.printf("Linies valides: %d, corruptes: %d%n%n",
informe.totalValides(), informe.totalCorruptes());
System.out.println("Operacions per nivell:");
informe.perNivell().forEach((n, c) -> System.out.printf(" %-8s %6d%n", n, c));
System.out.println("\nLes 5 mes lentes:");
informe.mesLentes().forEach(l -> System.out.printf(" %6d ms %-10s %s%n",
l.millisegons(), l.operacio(), l.empleat()));
System.out.println("\nDuracio mitjana per operacio:");
informe.mitjanaPerOperacio().forEach((op, ms) ->
System.out.printf(" %-12s %7.1f ms%n", op, ms));
System.out.println("\nEmpleats amb errors greus: " + informe.empleatsAmbErrors());
System.out.printf("Operacions per sobre de 200 ms: %.2f %%%n",
informe.percentatgeLentes());
analitzador.primeraCorrupta().ifPresentOrElse(
l -> System.out.println("\nPrimera linia corrupta: \"" + l + "\""),
() -> System.out.println("\nNo hi ha linies corruptes"));
}
/** Genera el fitxer de prova, tambe amb streams. */
private static void generarLogDeProva(Path desti, int linies) throws IOException {
String[] nivells = { "INFO", "INFO", "INFO", "WARNING", "SEVERE" };
String[] operacions = { "prestar", "retornar", "cercar", "exportar", "renovar" };
String[] empleats = { "Marta Ruiz", "Diego Alonso", "Nuria Vidal" };
Random atzar = new Random(42); // llavor fixa: sortida reproduible
List<String> contingut = java.util.stream.IntStream.rangeClosed(1, linies)
.mapToObj(i -> {
if (i % 9_999 == 0) {
return "2026-08-05|LINIA CORRUPTA SENSE CAMPS";
}
return String.format("2026-08-%02d|%s|%s|%s|%d",
1 + atzar.nextInt(28),
nivells[atzar.nextInt(nivells.length)],
operacions[atzar.nextInt(operacions.length)],
empleats[atzar.nextInt(empleats.length)],
1 + atzar.nextInt(500));
})
.toList();
Files.write(desti, contingut, StandardCharsets.UTF_8);
}
}=== ANALISI DEL LOG ===
Linies valides: 49995, corruptes: 5
Operacions per nivell:
INFO 30012
SEVERE 9981
WARNING 10002
Les 5 mes lentes:
500 ms cercar Nuria Vidal
500 ms prestar Marta Ruiz
500 ms renovar Diego Alonso
500 ms retornar Marta Ruiz
500 ms exportar Nuria Vidal
Duracio mitjana per operacio:
cercar 250.9 ms
exportar 250.4 ms
prestar 250.7 ms
renovar 250.3 ms
retornar 250.1 ms
Empleats amb errors greus: [Diego Alonso, Marta Ruiz, Nuria Vidal]
Operacions per sobre de 200 ms: 59.94 %
Primera linia corrupta: "2026-08-05|LINIA CORRUPTA SENSE CAMPS"Comentaris.
map(LiniaLog::analitzar).flatMap(Optional::stream) és l'idioma clau. L'anàlisi retorna Optional<LiniaLog>, i flatMap(Optional::stream) descarta els buits i desembolcalla els plens en un sol pas, sense isPresent(), sense get() i sense null.
primeraCorrupta() curtcircuita de debò. findFirst() sobre un Files.lines() para així que troba la primera coincidència: en un fitxer de dos gigabytes amb la corrupció a la línia 12, es llegeixen dotze línies. Amb readAllLines() se'n llegirien dos gigabytes.
El try-with-resources apareix tres vegades, una per cada obertura del fitxer. I aquest és el compromís conscient d'aquest disseny: es recorre el fitxer tres vegades (comptar, analitzar, cercar la corrupta) per no carregar-lo sencer. Si el fitxer cabés folgadament a memòria, un sol toList() inicial seria més ràpid; si no hi cabés, la lectura mandrosa és l'única opció. Triar entre les dues és una decisió d'enginyeria, no d'estil.
Solució 3
package com.nexussoftware.bibliotech.servei;
import com.nexussoftware.bibliotech.domini.Fitxa;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Cache de fitxes amb API basada en Optional i streams.
*/
public class CacheFitxes {
private final Map<String, Fitxa> cache = new ConcurrentHashMap<>();
private long encerts = 0;
private long fallades = 0;
/** Optional en lloc de null: la signatura diu que pot no hi ser. */
public Optional<Fitxa> cercar(String isbn) {
Optional<Fitxa> resultat = Optional.ofNullable(cache.get(isbn));
if (resultat.isPresent()) { encerts++; } else { fallades++; }
return resultat;
}
/** Retorna la que hi ha a la memoria cau o la calcula i la guarda. */
public Fitxa obtenirOCalcular(String isbn, Function<String, Fitxa> calcul) {
return cache.computeIfAbsent(isbn, clau -> {
fallades++;
return calcul.apply(clau);
});
}
public void guardar(Fitxa fitxa) {
cache.put(fitxa.isbn(), fitxa);
}
/** Nomes les trobades, sense isPresent() ni get(). */
public List<Fitxa> cercarTotes(Collection<String> isbns) {
return isbns.stream()
.map(cache::get)
.filter(Objects::nonNull)
.toList();
}
/** Versio equivalent passant per Optional, per veure l idioma. */
public List<Fitxa> cercarTotesAmbOptional(Collection<String> isbns) {
return isbns.stream()
.map(isbn -> Optional.ofNullable(cache.get(isbn)))
.flatMap(Optional::stream)
.toList();
}
/** true = a la memoria cau, false = cal calcular les. SEMPRE les dues claus. */
public Map<Boolean, List<String>> classificar(Collection<String> isbns) {
return isbns.stream()
.collect(Collectors.partitioningBy(cache::containsKey));
}
public int mida() { return cache.size(); }
public double taxaEncerts() {
long total = encerts + fallades;
return total == 0 ? 0.0 : (double) encerts / total;
}
}El banc de proves:
package com.nexussoftware.bibliotech;
import com.nexussoftware.bibliotech.domini.Fitxa;
import java.util.*;
import java.util.concurrent.ForkJoinPool;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class BancDeProvesParallel {
/** Calcul car que simula generar una fitxa. */
private static Fitxa calcularFitxa(String isbn) {
double acumulat = 0;
for (int i = 1; i <= 2_000; i++) {
acumulat += Math.sqrt(i) * Math.log(i + isbn.hashCode() % 7 + 8);
}
return new Fitxa(isbn, "Fitxa " + isbn + " (" + (long) acumulat + ")", true);
}
public static void main(String[] args) {
System.out.println("Nuclis disponibles: " + Runtime.getRuntime().availableProcessors());
System.out.println("Parallelisme del commonPool: "
+ ForkJoinPool.getCommonPoolParallelism());
System.out.println();
System.out.printf("%-12s %14s %14s %10s%n",
"MIDA", "SEQUENCIAL", "PARALLEL", "MILLORA");
System.out.println("-".repeat(54));
for (int mida : new int[] { 100, 10_000, 1_000_000 }) {
List<String> isbns = generarIsbns(mida);
// ESCALFAMENT: sense aixo es mesura l interpret, no el codi compilat (10-07)
escalfar(isbns.subList(0, Math.min(500, mida)));
long sequencial = mesurarMediana(() ->
isbns.stream().map(BancDeProvesParallel::calcularFitxa).count());
long parallel = mesurarMediana(() ->
isbns.parallelStream().map(BancDeProvesParallel::calcularFitxa).count());
System.out.printf("%-12d %11d ms %11d ms %9.2fx%n",
mida, sequencial, parallel,
parallel == 0 ? 0.0 : (double) sequencial / parallel);
}
System.out.println();
demostrarCorrupcioParallela();
}
private static List<String> generarIsbns(int quants) {
return IntStream.rangeClosed(1, quants)
.mapToObj(n -> String.format("978-%010d", n))
.toList();
}
private static void escalfar(List<String> mostra) {
for (int i = 0; i < 3; i++) {
mostra.stream().map(BancDeProvesParallel::calcularFitxa).count();
mostra.parallelStream().map(BancDeProvesParallel::calcularFitxa).count();
}
}
/** Mediana de 5 repeticions: menys sensible a un pic puntual que la mitjana. */
private static long mesurarMediana(Runnable tasca) {
long[] temps = new long[5];
for (int i = 0; i < temps.length; i++) {
long inici = System.nanoTime();
tasca.run();
temps[i] = (System.nanoTime() - inici) / 1_000_000;
}
Arrays.sort(temps);
return temps[temps.length / 2];
}
/** Per que MAI s acumula en una colleccio no segura des d un stream parallel. */
private static void demostrarCorrupcioParallela() {
List<String> isbns = generarIsbns(100_000);
System.out.println("--- Acumular en ArrayList des de parallelStream ---");
for (int intent = 1; intent <= 3; intent++) {
List<String> desti = new ArrayList<>(); // NO es segur entre fils
try {
isbns.parallelStream().forEach(desti::add);
System.out.printf(" intent %d: esperats 100000, obtinguts %d%s%n",
intent, desti.size(),
desti.size() == 100_000 ? "" : " <-- ELEMENTS PERDUTS");
} catch (Exception e) {
System.out.printf(" intent %d: %s%n", intent, e.getClass().getSimpleName());
}
}
System.out.println("--- La forma correcta: collect ---");
List<String> correcte = isbns.parallelStream().collect(Collectors.toList());
System.out.println(" collect: " + correcte.size() + " elements, sempre");
}
}Sortida orientativa (8 nuclis):
Nuclis disponibles: 8
Parallelisme del commonPool: 7
MIDA SEQUENCIAL PARALLEL MILLORA
------------------------------------------------------
100 3 ms 2 ms 1,50x
10000 287 ms 49 ms 5,86x
1000000 28640 ms 4712 ms 6,08x
--- Acumular en ArrayList des de parallelStream ---
intent 1: esperats 100000, obtinguts 87423 <-- ELEMENTS PERDUTS
intent 2: ArrayIndexOutOfBoundsException
intent 3: esperats 100000, obtinguts 92108 <-- ELEMENTS PERDUTS
--- La forma correcta: collect ---
collect: 100000 elements, sempreComentaris. Quatre coses que aquest exercici demostra millor que qualsevol explicació.
L'escalfament canvia els números. Sense ell, el primer mesurament inclou la interpretació del bytecode abans que el JIT compili el mètode calent, i pot ser cinc o deu vegades més lent. Per això el banc escalfa abans de mesurar i pren la mediana de cinc repeticions. Tot i així continua essent un mesurament casolà: a 10-07 veuràs per què només JMH dona resultats en què confiar de debò.
Amb 100 elements el paral·lelisme amb prou feines aporta res, i amb un càlcul més barat seria contraproduent. Amb 10.000 i 1.000.000 la millora s'estabilitza al voltant de 6× sobre 8 nuclis: no 8×, perquè el repartiment, la combinació i la memòria compartida no escalen linealment. La llei d'Amdahl aplicada.
La corrupció de l'ArrayList no és determinista, i això és el perillós. Tres intents, tres resultats diferents: dos amb elements perduts i un amb excepció. Si això fos en producció, fallaria de manera intermitent i irreproduïble — el pitjor tipus d'error. I funcionaria perfectament a les proves amb deu elements.
collect funciona perquè coneix el seu contracte. Cada fil acumula en el seu propi contenidor i després es combinen; no hi ha estat compartit entre fils en cap moment. És exactament el paper del combinador del reduce de tres arguments de l'apartat 10.
Conclusió
Ha arribat i ha valgut la pena.
Saps què és un stream i —igual d'important— què no és: no és una col·lecció, no emmagatzema elements, no modifica l'origen, no té índexs, no té res a veure amb InputStream, i és d'un sol ús (IllegalStateException: stream has already been operated upon or closed). És una descripció d'un càlcul sobre una seqüència.
Domines la canonada: una font, zero o més operacions intermèdies que retornen Stream i no fan res, i exactament una operació terminal que ho dispara tot. I has vist amb traces l'avaluació mandrosa: cada element travessa la canonada completa abans que comenci el següent, map no s'executa sobre el que el filter va descartar, i limit(2) curtcircuita el recorregut de manera que el quart element no es processa mai. Un stream amb vint operacions continua essent un sol recorregut i zero llistes intermèdies.
Coneixes les fonts —stream(), Arrays.stream, Stream.of, iterate/generate amb limit, IntStream.range, i Files.lines() i Files.walk() de 07-06, ara amb tota l'API al damunt i sempre en try-with-resources—. Les operacions intermèdies: filter, map, mapToInt/mapToObj, distinct (que depèn d'equals/hashCode), sorted (amb estat: mai sobre un stream infinit i sempre tan tard com es pugui), peek (només per depurar, perquè no es garanteix que s'executi), limit, skip, i takeWhile/dropWhile que paren en lloc de continuar filtrant. I flatMap, el cas que costa: converteix cada element en un stream i els aplana tots — amb el .stream() interior que tothom oblida.
Les operacions terminals: forEach/forEachOrdered, toList() de Java 16 que retorna una llista no modificable, collect, count (que des de Java 9 es pot saltar les intermèdies), min/max que retornen Optional, anyMatch/allMatch/noneMatch que curtcircuiten —amb allMatch sobre buit retornant true per vacuïtat—, i findFirst/findAny amb la seva diferència real en paral·lel. I reduce en les seves tres formes: sense identitat retorna Optional perquè no hi ha un zero universal; amb identitat retorna T i la identitat ho ha de ser de debò, o en paral·lel s'aplica a cada partició i el resultat és absurd; amb combinador canvia de tipus i explica per què un Collector necessita combinar.
Collectors a fons: toList/toSet/toMap —amb el seu IllegalStateException: Duplicate key i la funció de mescla que el resol—, joining amb prefix i sufix, counting, summingInt, averagingDouble, summarizingInt que dona cinc estadístiques en un recorregut, mapping i collectingAndThen com a downstream, teeing per a dos col·lectors alhora, groupingBy simple, amb downstream i de dos nivells, i partitioningBy que sempre porta les dues claus. Amb això, l'informe de BiblioTech que al mòdul 5 eren 74 línies de bucles imbricats, mapes manuals i comprovacions de null, són 12 línies que es llegeixen com el seu propi enunciat — i fan més: Optional en lloc de null, EnumMap per a claus d'enum, TreeMap per a claus ordenades.
Saps fer servir streams de primitius (IntStream, LongStream, DoubleStream) per evitar l'autoboxing, amb boxed per tornar, mapToObj per sortir-ne, i average() retornant OptionalDouble perquè un stream buit no té mitjana.
I sobre els streams paral·lels tens el criteri, no només la sintaxi. Saps que corren sobre el ForkJoinPool.commonPool compartit per tota l'aplicació, i que per això mai es fan servir amb operacions que bloquegen. Saps que compensen amb desenes de milers d'elements, cost per element apreciable, font fàcil de partir (ArrayList, arrays, rangs — mai LinkedList, que va ser 3× més lenta en paral·lel) i operacions sense estat. I saps la regla que no admet excepcions: gens d'estat compartit a les lambdes, perquè acumular en un ArrayList des d'un parallelStream perd elements de manera no determinista, funciona a les proves i falla en producció.
I tens Optional. Saps per a què es va crear: perquè Prestec cercarPerReferencia(String) menteix, i el compilador no et pot ajudar a recordar el null — l'"error de mil milions de dòlars" de Tony Hoare. Amb Optional<Prestec>, la signatura diu la veritat i el compilador t'obliga a enfrontar-te a l'absència. El crees amb of (que rebutja null), ofNullable (que l'accepta) o empty. El consumeixes sense desembolcallar-lo: map, flatMap, filter, ifPresent, ifPresentOrElse, or encadenat per cercar en diversos llocs, stream() per descartar els buits de cop. I coneixes la diferència entre orElse i orElseGet, demostrada amb traces: orElse avalua el seu argument sempre, i una consulta a la xarxa innecessària de 300 ms és un error invisible que només es manifesta com a lentitud inexplicable.
Reconeixes els cinc antipatrons: isPresent() + get() (un if amb més soroll), Optional com a paràmetre (fes servir sobrecàrrega; i un Optional també pot ser null), Optional com a camp (no és serialitzable i multiplica els objectes: null a dins, Optional al getter), Optional d'una col·lecció (la col·lecció buida ja significa "res") i Optional.of(null). I tens la taula de què retornar quan no hi ha valor: Optional per a cerques, col·lecció buida per a col·leccions, Resultat<T> quan la fallada porta informació, excepció quan és un error de debò.
BiblioTech ha canviat per dins. EstadistiquesBiblioTech genera informes de dos nivells en tres línies. Repositori<T> exposa un flux() i retorna Optional a les cerques i llistes buides a les consultes. GestorPrestecs.prestar encadena cercarPerId().map().orElseGet() sense ni un sol if (x == null). ImportadorCataleg processa un CSV de dos gigabytes sense carregar-lo a memòria. AnalitzadorLog troba la primera línia corrupta llegint dotze línies de dos gigabytes. I el null que significava "no trobat" ha desaparegut del codi.
Queda un deute, i és dels més vells del curs. Prestec continua tenint int diaPrestec i int diaVenciment. L'histograma de l'exercici 1 calcula el dia de la setmana amb diaPrestec % 7, que és un pedaç que només funciona per casualitat. CalculadoraMultes resta enters i en diu "dies de retard". Ningú no pot respondre a "aquest préstec venç d'aquí a un mes?" sense decidir arbitràriament si un mes són 30 o 31 dies. El CSV exporta números que no signifiquen res fora de BiblioTech. I les marques de temps dels fitxers, que a 07-06 vas mostrar com a FileTime sense poder fer-hi res, continuen esperant.
A 10-05, Dates i hores amb java.time, s'acaba. Veuràs per què Date i Calendar van ser un desastre —mutables, amb els mesos començant a zero, insegurs entre fils, amb el SimpleDateFormat compartit que és l'error clàssic de les aplicacions Java— i per què java.time és el contrari: immutable, segur entre fils, fluid i explícit sobre si hi ha zona horària o no. Aprendràs quan fer servir LocalDate, LocalDateTime, ZonedDateTime i Instant —i la regla d'or de què guardar a la base de dades i què mostrar a l'usuari—, la diferència entre Duration i Period, els ajustadors temporals per calcular venciments, les zones horàries amb els dos casos perillosos de l'horari d'estiu, el format amb DateTimeFormatter i Locale, i Clock com a font de temps injectable, sense la qual és impossible provar codi que depèn de la data.
I en acabar, Prestec tindrà LocalDate dataPrestec i LocalDate dataVenciment, les multes es calcularan amb ChronoUnit.DAYS.between, els avisos faran servir TemporalAdjusters per caure en dia hàbil, i el CSV portarà dates ISO-8601 que qualsevol sistema del món entén.
Curs de Programació en Java
Mòdul 1: Introducció a Java
- Introducció a Java
- Configuració de l'entorn de desenvolupament
- Sintaxi i estructura bàsica
- Variables i tipus de dades
- Operadors
- Entrada i sortida per consola
- El teu primer programa complet: BiblioTech
Mòdul 2: Flux de control
- Sentències condicionals
- Bucles
- Sentències switch
- Break i continue
- Depuració i traces d'execució
- Projecte: menú interactiu de BiblioTech
Mòdul 3: Programació orientada a objectes
- Introducció a la POO
- Classes i objectes
- Mètodes
- Constructors
- Herència
- Polimorfisme
- Encapsulament
- Abstracció
- La classe Object: equals, hashCode i toString
Mòdul 4: Programació orientada a objectes avançada
- Interfícies
- Classes abstractes
- Classes internes
- Classes anònimes
- Expressions lambda
- Interfícies funcionals i referències a mètodes
- Enumeracions i registres
Mòdul 5: Estructures de dades i col·leccions
- Arrays
- El framework de col·leccions
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cua i Deque
- Pila
- Ordenació i cerca en col·leccions
Mòdul 6: Gestió d'excepcions
- Introducció a les excepcions
- Bloc try-catch
- Throw i throws
- Excepcions personalitzades
- Bloc finally
- Try-with-resources i AutoCloseable
- Estratègies de gestió d'errors i logging
Mòdul 7: Entrada/sortida de fitxers
- Lectura de fitxers
- Escriptura de fitxers
- Fluxos de fitxers
- BufferedReader i BufferedWriter
- Serialització
- L'API NIO.2: Path i Files
- Formats d'intercanvi: CSV i Properties
Mòdul 8: Multifil i concurrència
- Introducció al multifil
- Creació de fils
- Cicle de vida d'un fil
- Sincronització
- Utilitats de concurrència
- Col·leccions concurrents i variables atòmiques
- Tasques asíncrones amb CompletableFuture
Mòdul 9: Xarxes
- Introducció a les xarxes
- Sockets
- ServerSocket
- DatagramSocket i DatagramPacket
- URL i HttpURLConnection
- El client HTTP modern
Mòdul 10: Temes avançats
- Genèrics
- Anotacions
- Reflexió
- Característiques de Java 8: Streams i Optional
- Dates i hores amb java.time
- Java 9 i més enllà
- Memòria, recol·lecció de brossa i rendiment
Mòdul 11: Frameworks i llibreries de Java
- Introducció als frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Proves avançades amb Mockito
- Llibreries essencials de l'ecosistema
