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

  1. Què va portar Java 8 i què ja has vist
  2. Què és un stream i què NO és
  3. La canonada: font, operacions intermèdies, operació terminal
  4. Avaluació mandrosa: l'ordre real d'execució
  5. Fonts de streams
  6. Operacions intermèdies
  7. flatMap: el cas que costa
  8. takeWhile i dropWhile
  9. Operacions terminals
  10. reduce en les seves tres formes
  11. Els Collectors bàsics
  12. groupingBy i partitioningBy
  13. Streams de primitius
  14. Streams paral·lels
  15. L'informe de BiblioTech: de trenta línies a tres
  16. Optional: per a què es va crear
  17. Crear i consumir un Optional
  18. orElse enfront d'orElseGet
  19. Els antipatrons d'Optional
  20. Què retornar quan no hi ha valor
  21. BiblioTech refactoritzat
  22. Errors Comuns i Consells
  23. Exercicis

  1. 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.

  1. 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 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.iterate genera elements sense fi, i limit els acota.
  • Paral·lelitzable canviant stream() per parallelStream().

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: EXPLOTA
Exception 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:

long quants = cataleg.stream().count();
List<Llibre> llista = cataleg.stream().toList();

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.

  1. 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);
(no hi ha sortida)

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.

  1. 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);
[PR-2026-0001, PR-2026-0002, PR-2026-0003, PR-2026-0004, PR-2026-0005]

Sense mandra, Stream.iterate penjaria el programa. Amb ella, només es generen els cinc elements que el limit demana.

  1. 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();
    }
}

  1. Operacions intermèdies

Totes retornen un Stream i cap no executa res per si sola.

filter: quedar-se amb els que compleixen

Stream<Llibre> filter(Predicate<? super T> criteri)
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

<R> Stream<R> map(Function<? super T, ? extends R> transformacio)

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

List<String> empleatsUnics = prestecs.stream()
        .map(Prestec::getEmpleat)
        .distinct()
        .toList();

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:

  1. Sobre un stream infinit, es penja. Stream.iterate(1, n -> n+1).sorted() no acaba mai.
  2. Posa-la tan tard com puguis. filter(...).sorted() ordena menys elements que sorted().filter(...), i el resultat és el mateix.

peek: mirar sense tocar

Stream<T> peek(Consumer<? super T> accio)

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.

  1. flatMap: el cas que costa

flatMap és l'operació que més costa d'entendre, i mereix el seu propi apartat.

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> transformacio)

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 prestecs

El 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 Stream

Ara 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.

  1. takeWhile i dropWhile

Afegides 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 tot

Nomé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.

  1. Operacions terminals

Disparen el processament i retornen un resultat (o res).

forEach i forEachOrdered

cataleg.stream()
        .filter(Llibre::estaPrestat)
        .forEach(l -> System.out.println(l.getTitol()));

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 origen

Consell: 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 Java 16+
.collect(Collectors.toList()) Normalment sí (no garantit) Java 8+
.collect(Collectors.toUnmodifiableList()) No No Java 10+
.collect(Collectors.toCollection(ArrayList::new)) Sí, garantit 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

long prestats = cataleg.stream().filter(Llibre::estaPrestat).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 la

Amb 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));
comprovant: Java Eficac
true

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));    // true

allMatch 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.

  1. reduce en les seves tres formes

reduce 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<T> reduce(BinaryOperator<T> acumulador)
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

T reduce(T identitat, BinaryOperator<T> acumulador)
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 particio

Forma 3: identitat + acumulador + combinador → canvia de tipus

<U> U reduce(U identitat, BiFunction<U, ? super T, U> acumulador, BinaryOperator<U> combinador)

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 parcials
graph 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.

  1. Els Collectors bàsics

collect é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());
Llibres: 4
Total:   1719
Mitjana: 429.75
Minim:   395
Maxim:   464

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.

  1. groupingBy i partitioningBy

Aquí 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())));
{Marta Ruiz={LLIBRE=3, DVD=1},
 Diego Alonso={LLIBRE=1, REVISTA=2},
 Nuria Vidal={LLIBRE=2}}

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.

  1. 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.

  1. 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());
Parallelisme: 7
Nuclis:       8

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 els ExecutorService propis de 08-05 i CompletableFuture de 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:

  1. Amb 1.000 elements, el paral·lel és més lent. El cost de coordinar supera la feina.
  2. 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.
  3. Amb feina cara, millora 5,3×. Com més costa cada element, millor s'amortitza el cost fix.
  4. 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()

  1. Hi ha desenes de milers d'elements o més?
  2. El cost per element és apreciable?
  3. La font és fàcil de partir (ArrayList, array, rang)?
  4. Les operacions són sense estat i sense bloqueig?
  5. Ho has mesurat i és realment més ràpid?
  6. L'aplicació no està ja saturant els nuclis?

Si alguna resposta és "no", fes servir stream().

  1. 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 Disseny

La 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.

  1. Optional: per a què es va crear

Canviem 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 existeix

I 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>.

  1. Crear i consumir un Optional

Creació

// 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.

  1. orElse enfront d'orElseGet

Semblen 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 calcula

orElse 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 xarxa

Mira 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).

  1. Els antipatrons d'Optional

Optional 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

// MALAMENT
public List<Llibre> cercar(String text, Optional<TipusMaterial> tipus) { ... }

Tres raons per les quals és un error:

  1. Qui crida ha d'embolcallar: cercar("java", Optional.of(LLIBRE)) és més sorollós que cercar("java", LLIBRE).
  2. Un Optional pot ser null. Res no impedeix cercar("java", null), i llavors tens un NullPointerException més un Optional. El pitjor dels dos mons.
  3. 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

// MALAMENT
public class Llibre {
    private Optional<String> editorial;      // NO
}
  • Optional no é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ó

// MALAMENT
public Optional<List<Prestec>> prestecsDe(String empleat) { ... }

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.

  1. 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

  1. 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:

  1. Total de materials i desglossament per tipus.
  2. Els 3 materials més valorats, amb la seva valoració.
  3. Préstecs actius agrupats per empleat, amb el nombre i la llista de títols ordenada.
  4. L'empleat amb més préstecs (Optional) i el que porta més dies de retard acumulats.
  5. Mitjana, mínim i màxim de pàgines dels llibres, en un sol recorregut.
  6. Materials mai prestats (els que no apareixen en cap préstec).
  7. 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:

2026-08-05|INFO|prestar|Marta Ruiz|12
2026-08-05|SEVERE|retornar|Diego Alonso|340

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.

  1. Reescriu CacheFitxes perquè cercar(String isbn) retorni Optional<Fitxa> i tingui un obtenirOCalcular(String, Function<String, Fitxa>).
  2. Afegeix List<Fitxa> cercarTotes(Collection<String> isbns) que retorni només les trobades fent servir flatMap(Optional::stream).
  3. Afegeix Map<Boolean, List<String>> classificar(Collection<String> isbns) amb partitioningBy per separar els que són a la memòria cau dels que no.
  4. Escriu un banc de proves que compari stream() enfront de parallelStream() 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.
  5. Demostra amb codi per què acumular en un ArrayList amb forEach en 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     0

Comentaris. 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, sempre

Comentaris. 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 fontsstream(), 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

Mòdul 2: Flux de control

Mòdul 3: Programació orientada a objectes

Mòdul 4: Programació orientada a objectes avançada

Mòdul 5: Estructures de dades i col·leccions

Mòdul 6: Gestió d'excepcions

Mòdul 7: Entrada/sortida de fitxers

Mòdul 8: Multifil i concurrència

Mòdul 9: Xarxes

Mòdul 10: Temes avançats

Mòdul 11: Frameworks i llibreries de Java

Mòdul 12: Construcció d'aplicacions del món real

© Copyright 2026. Tots els drets reservats