La lliçó anterior va acabar assenyalant una cosa que arrossegues des del principi del curs: has estat fent servir característiques modernes de Java sense saber d'on van sortir.

switch amb fletxes. record. var. List.of. instanceof amb patró. String.repeat. Files.readString. HttpClient. Stream.toList. Totes van aparèixer en algun moment, es van fer servir amb naturalitat i ningú no va dir en quina versió van arribar ni quin problema resolien. I n'hi ha més que encara no has vist: blocs de text, classes segellades, pattern matching per a switch, i els fils virtuals, que portes dos mòduls esperant des que a 09-03 vam dir que canviarien per complet el càlcul d'"un fil per connexió".

Aquesta lliçó ordena tot això.

I no és només un catàleg. Java va canviar el seu model de desenvolupament el 2018: va passar de publicar una versió gran cada tres anys a una versió cada sis mesos, amb versions de suport estès cada dos o tres anys. Això va canviar per complet el ritme del llenguatge i també la pregunta pràctica que tot equip es fa: a quina versió migrem i quan?

En acabar, sabràs què va portar cada versió des de Java 9, què val la pena fer servir avui i què no, i BiblioTech tindrà una jerarquia segellada amb pattern matching exhaustiu i un ServidorCataleg que atén deu mil connexions simultànies sense un pool de fils.

Contingut

  1. El calendari de versions i les LTS
  2. Taula de novetats per versió
  3. El sistema de mòduls (JPMS)
  4. module-info.java: requires, exports, opens
  5. Encapsulament fort i la reflexió bloquejada
  6. Classpath enfront de modulepath, i jlink
  7. L'adopció real de JPMS: una valoració honesta
  8. Novetats d'API
  9. var: inferència de tipus locals
  10. switch d'expressió i yield
  11. Blocs de text
  12. record revisitat
  13. instanceof amb patró
  14. Classes segellades: sealed, permits, non-sealed
  15. Pattern matching per a switch i patrons de registre
  16. BiblioTech: refactorització de la jerarquia
  17. Fils virtuals: què són
  18. Fils de plataforma enfront de fils virtuals
  19. BiblioTech: el ServidorCataleg amb fils virtuals
  20. Pinning, synchronized i el que no resolen
  21. El que ve: concurrència estructurada i ScopedValue
  22. Eines: jshell, jpackage i companyia
  23. Com mantenir-se al dia i com decidir la migració
  24. Errors Comuns i Consells
  25. Exercicis

  1. El calendari de versions i les LTS

Fins a Java 8, les versions sortien quan estaven llestes: Java 5 el 2004, Java 6 el 2006, Java 7 el 2011, Java 8 el 2014. Java 9 es va endarrerir tres anys i mig pel sistema de mòduls, i aquella experiència va motivar un canvi radical.

Des de Java 10 (març del 2018), surt una versió cada sis mesos, al març i al setembre, amb data fixa. El que està llest hi entra; el que no, espera el tren següent.

graph LR
    A["Java 8<br/>2014<br/>LTS"] --> B["Java 9, 10<br/>2017-2018"]
    B --> C["Java 11<br/>2018<br/>LTS"]
    C --> D["Java 12 a 16<br/>2019-2021"]
    D --> E["Java 17<br/>2021<br/>LTS"]
    E --> F["Java 18 a 20<br/>2022-2023"]
    F --> G["Java 21<br/>2023<br/>LTS"]
    G --> H["Java 22, 23, 24<br/>2024-2025"]
    H --> I["Java 25<br/>2025<br/>LTS"]

Dos tipus de versió:

Tipus Freqüència Suport Ús recomanat
Normal (feature release) Cada 6 mesos 6 mesos, fins a la següent Provar novetats, projectes personals
LTS (Long-Term Support) Cada 2-3 anys Anys (varia segons el proveïdor) Producció

Les LTS fins avui són 8, 11, 17, 21 i 25. I el consell pràctic és clar: en producció, fes servir una LTS. Una versió normal deixa de rebre pedaços de seguretat als sis mesos, cosa que obliga a migrar dues vegades l'any.

Les característiques de vista prèvia. Des de Java 12, les novetats importants de llenguatge passen per una o diverses rondes de preview abans de ser definitives. S'activen amb una opció del compilador:

javac --release 21 --enable-preview LaMevaClasse.java
java --enable-preview LaMevaClasse

Això permet a l'equip de Java recollir opinions i canviar el disseny abans de comprometre's per sempre. Els record van estar dues versions en vista prèvia; els fils virtuals, tres. No facis servir característiques de vista prèvia en producció: poden canviar o desaparèixer, i el bytecode generat només funciona en aquella versió exacta.

  1. Taula de novetats per versió

Un mapa complet per a consulta. Les que ja coneixes porten la lliçó on les vas veure.

Versió Any Novetats rellevants
9 2017 Sistema de mòduls (JPMS), List.of/Set.of/Map.of (05-02), mètodes privats a interfícies, Stream.takeWhile/dropWhile/ofNullable (10-04), Optional.stream/or/ifPresentOrElse (10-04), jshell, jlink, diamant a classes anònimes (10-01)
10 2018 var, List.copyOf, Collectors.toUnmodifiableList, Optional.orElseThrow() sense arguments
11 2018 LTS HttpClient definitiu (09-06), String.repeat/strip/isBlank/lines, Files.readString/writeString (07-06), var a paràmetres de lambda, llançar un .java sense compilar
12 2019 Collectors.teeing (10-04), String.indent, switch d'expressió (vista prèvia)
13 2019 Blocs de text (vista prèvia), switch amb yield (vista prèvia)
14 2020 switch d'expressió definitiu, record (vista prèvia), instanceof amb patró (vista prèvia), missatges útils de NullPointerException
15 2020 Blocs de text definitius, classes segellades (vista prèvia), CharSequence.isEmpty
16 2021 record definitiu, instanceof amb patró definitiu, Stream.toList (10-04), Stream.mapMulti, encapsulament fort per defecte
17 2021 LTS Classes segellades definitives, RandomGenerator, eliminació del Security Manager, switch amb patrons (vista prèvia)
18 2022 UTF-8 per defecte (07-01), servidor web simple jwebserver, @snippet a Javadoc
19 2022 Fils virtuals (vista prèvia), concurrència estructurada (incubació), API de funcions foranes (vista prèvia)
20 2023 Refinaments de vista prèvia, ScopedValue (incubació)
21 2023 LTS Fils virtuals definitius, pattern matching per a switch definitiu, patrons de registre definitius, col·leccions seqüencials, String.repeat millorat
22 2024 API de funcions foranes definitiva, sentències abans de super() (vista prèvia)
23 2024 Javadoc en Markdown, refinaments
24 2025 Criptografia postquàntica, millores del GC generacional ZGC
25 2025 LTS Consolidació de vistes prèvies, fitxers font compactes i mètodes d'instància principals

Les tres fites que canvien com s'escriu Java són: Java 8 (lambdes i streams), Java 17 (records, segellades, pattern matching incipient, switch d'expressió) i Java 21 (fils virtuals i pattern matching complet).

  1. El sistema de mòduls (JPMS)

Java 9 va introduir el canvi estructural més gran des del naixement del llenguatge: el Java Platform Module System, conegut com a JPMS o pel seu nom en clau, Projecte Jigsaw.

El problema que resolia, en tres parts:

1. El JDK era monolític. El rt.jar de Java 8 pesava uns 60 MB i contenia tot: col·leccions, xarxa, XML, CORBA, Swing, applets. Una aplicació de servidor que només fa servir col·leccions i xarxa arrossegava Swing i CORBA. En un contenidor Docker, això són centenars de megabytes d'imatge per no res.

2. No hi havia encapsulament entre paquets (JAR hell). El modificador public significa "visible per a tothom". Si la teva biblioteca té un paquet com.lamevaempresa.intern amb classes de suport, no hi havia manera d'impedir que un usuari les importés. I un cop algú depèn de la teva classe interna, ja no la pots canviar. El mateix JDK patia això amb sun.misc.Unsafe, feta servir per mig ecosistema.

3. No hi havia comprovació de dependències en arrencar. El classpath és una llista plana de JAR. Si en falta un, el programa arrenca i falla amb NoClassDefFoundError en el moment en què l'arriba a fer servir — potser hores després, potser només en un cas poc freqüent.

La solució: un mòdul és una unitat amb nom que declara què necessita i què exposa.

graph TD
    A["module com.nexussoftware.bibliotech"] -->|"requires"| B["java.base<br/>implicit"]
    A -->|"requires"| C["java.logging"]
    A -->|"requires"| D["java.net.http"]
    A -->|"exports"| E["com...domini<br/>PUBLIC"]
    A -->|"exports"| F["com...servei<br/>PUBLIC"]
    A -.->|"NO exporta"| G["com...intern<br/>INACCESSIBLE des de fora"]
    A -->|"opens"| H["com...domini<br/>reflexio profunda permesa"]

El JDK mateix es va dividir en uns 70 mòduls. Es poden llistar:

java --list-modules

java.base és especial: conté java.lang, java.util, java.io, java.nio, java.time i java.util.concurrent, i tots els mòduls el requereixen implícitament. Mai no cal declarar-lo.

  1. module-info.java: requires, exports, opens

Un mòdul es declara en un fitxer anomenat exactament module-info.java, a l'arrel de l'arbre de fonts:

src/
  module-info.java
  com/nexussoftware/bibliotech/
      BiblioTechApp.java
      domini/
      servei/
      persistencia/
      xarxa/
      intern/
/**
 * Modul principal de BiblioTech.
 */
module com.nexussoftware.bibliotech {

    // --- QUE NECESSITO ---
    requires java.logging;              // java.util.logging (06-07)
    requires java.net.http;             // HttpClient (09-06)
    requires java.sql;                  // JDBC, per al modul 11

    // transitive: qui em requereixi obte tambe aixo
    requires transitive com.nexussoftware.comu;

    // static: necessari per compilar, opcional en execucio
    requires static com.nexussoftware.eines;

    // --- QUE OFEREIXO ---
    exports com.nexussoftware.bibliotech.domini;
    exports com.nexussoftware.bibliotech.servei;

    // Exportacio qualificada: NOMES a aquests moduls
    exports com.nexussoftware.bibliotech.persistencia
            to com.nexussoftware.bibliotech.proves;

    // com.nexussoftware.bibliotech.intern NO s exporta:
    // es inaccessible des de fora encara que les seves classes siguin public

    // --- REFLEXIO ---
    // opens permet reflexio PROFUNDA (setAccessible sobre privats)
    opens com.nexussoftware.bibliotech.domini;

    // Obertura qualificada
    opens com.nexussoftware.bibliotech.persistencia to com.fasterxml.jackson.databind;

    // --- SERVEIS (ServiceLoader) ---
    uses com.nexussoftware.bibliotech.servei.PoliticaMultes;
    provides com.nexussoftware.bibliotech.servei.PoliticaMultes
            with com.nexussoftware.bibliotech.intern.PoliticaEstandard;
}

Les directives, una a una:

Directiva Significat
requires X Necessito el mòdul X per compilar i executar
requires transitive X A més, qui em requereixi obté X automàticament
requires static X Necessari en compilació, opcional en execució
exports P El paquet P és accessible per a tothom
exports P to M1, M2 El paquet P és accessible només per a M1 i M2
opens P Reflexió profunda permesa sobre P
opens P to M Reflexió profunda permesa només a M
open module Tot el mòdul obert a reflexió
uses S Consumeixo el servei S via ServiceLoader
provides S with I Ofereixo I com a implementació de S

La distinció clau, que causa la meitat dels problemes pràctics:

exports opens
Ús normal en compilació (import, new) No
Reflexió sobre membres públics
Reflexió sobre membres privats (setAccessible) No
Es comprova En compilació i execució Només en execució

Un framework de persistència com Hibernate necessita opens, perquè llegeix camps privats de les teves entitats. Exportar el paquet no n'hi ha prou.

  1. Encapsulament fort i la reflexió bloquejada

Reprenem 10-03. Aquest codi va funcionar durant vint anys:

Field valor = String.class.getDeclaredField("value");
valor.setAccessible(true);
byte[] bytes = (byte[]) valor.get("Java Eficac");

I des de Java 16 (on l'encapsulament fort va passar a ser el valor per defecte):

Exception in thread "main" java.lang.reflect.InaccessibleObjectException:
    Unable to make field private final byte[] java.lang.String.value accessible:
    module java.base does not "opens java.lang" to unnamed module @0x1b6d3586

Què va canviar: els paquets interns del JDK ja no estan oberts. java.base exporta java.lang (pots fer servir String) però no l'obre (no pots furgar en els seus camps privats).

Això va trencar moltes biblioteques antigues, i per això existeix la sortida d'emergència:

# Obrir un paquet concret a tot el codi sense modul
java --add-opens java.base/java.lang=ALL-UNNAMED -cp app.jar com.exemple.App

# Exportar (us normal, no reflexio profunda)
java --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED ...

# Diverses alhora
java --add-opens java.base/java.lang=ALL-UNNAMED \
     --add-opens java.base/java.util=ALL-UNNAMED \
     -jar app.jar

--add-opens és una tireta, no una solució. Cada ús indica que alguna cosa depèn de detalls interns que poden canviar en qualsevol versió. Si el necessites per a una biblioteca de tercers, actualitza-la; si és del teu codi, replanteja-ho.

Per al teu propi codi modularitzat, la solució correcta és declarar opens a module-info.java:

module com.nexussoftware.bibliotech {
    requires java.logging;

    exports com.nexussoftware.bibliotech.domini;

    // El nostre ExportadorAnotat de 10-03 fa setAccessible sobre
    // els camps privats de les entitats: necessita opens.
    opens com.nexussoftware.bibliotech.domini;
}

Sense aquest opens, l'ExportadorAnotat que vas escriure a 10-03 llançaria InaccessibleObjectException així que BiblioTech es modularitzés. És exactament l'escenari que s'hi va anticipar.

  1. Classpath enfront de modulepath, i jlink

Conviuen dues formes d'executar:

# CLASSPATH: mode classic. Tot va al "modul sense nom".
javac -cp lib/*.jar -d classes $(find src -name "*.java")
java -cp classes:lib/*.jar com.nexussoftware.bibliotech.BiblioTechApp

# MODULEPATH: mode modular
javac -d classes --module-source-path src $(find src -name "*.java")
java --module-path classes -m com.nexussoftware.bibliotech/com.nexussoftware.bibliotech.BiblioTechApp
Aspecte Classpath Modulepath
Nom Mòdul sense nom (unnamed) Mòduls amb nom
Encapsulament Cap: tot el que és public és accessible Fort: només el que s'exporta
Dependències Sense comprovar Comprovades en arrencar
Fitxers duplicats El primer guanya, silenciosament Error: split packages
Compatibilitat Total Requereix que les biblioteques cooperin

Els JAR automàtics són el pont: un JAR normal al modulepath es converteix en un mòdul el nom del qual es deriva del nom del fitxer, que exporta tots els seus paquets i requereix tots els mòduls. Permet migrar a poc a poc, encara que el nom derivat del fitxer és fràgil.

jlink: un runtime a mida

L'avantatge més tangible de JPMS. jlink construeix una imatge de Java que conté només els mòduls que la teva aplicació fa servir:

# Descobrir quins moduls necessita l aplicacio
jdeps --print-module-deps --ignore-missing-deps classes/bibliotech.jar
java.base,java.logging,java.net.http
# Construir el runtime minim
jlink --add-modules java.base,java.logging,java.net.http \
      --strip-debug \
      --compress=zip-6 \
      --no-header-files \
      --no-man-pages \
      --output runtime-bibliotech

# I executar hi
./runtime-bibliotech/bin/java -m com.nexussoftware.bibliotech/...BiblioTechApp
du -sh $JAVA_HOME runtime-bibliotech
315M    /usr/lib/jvm/jdk-21
44M     runtime-bibliotech

De 315 MB a 44 MB. En una imatge de contenidor multiplicada per cent desplegaments al dia, això és temps de descàrrega, cost d'emmagatzematge i superfície d'atac. A més, el runtime resultant no necessita Java instal·lat a la màquina destí.

I jpackage (Java 14) va un pas més enllà: empaqueta aquest runtime juntament amb la teva aplicació en un instal·lador natiu (.deb, .rpm, .msi, .dmg).

  1. L'adopció real de JPMS: una valoració honesta

Toca ser sincer, perquè el tema genera confusió.

JPMS no s'ha adoptat àmpliament en aplicacions. Gairebé deu anys després de Java 9, la immensa majoria de les aplicacions empresarials Java continuen executant-se al classpath, sense module-info.java. Spring Boot, la plataforma dominant, no exigeix modularització. Moltes biblioteques populars no publiquen mòduls amb nom.

Per què:

  • Cost de migració alt. Modularitzar una aplicació gran amb cinquanta dependències exigeix que les cinquanta cooperin.
  • Els split packages trenquen molt. Dos JAR amb classes al mateix paquet són legals al classpath i il·legals al modulepath. És freqüent en biblioteques antigues.
  • La reflexió pateix. Frameworks que depenen de setAccessible necessiten opens per tot arreu, cosa que erosiona el benefici.
  • El benefici no és evident per a una aplicació. Un JAR executable no guanya gaire encapsulant paquets que només ell fa servir.

On sí que ha triomfat:

  • Al mateix JDK. És el que va permetre eliminar CORBA i els applets, dividir la plataforma i activar l'encapsulament fort que et protegeix de dependre d'interns.
  • En biblioteques serioses, que publiquen module-info.java per declarar la seva API pública de manera verificable.
  • Amb jlink i jpackage, per a aplicacions d'escriptori i contenidors mínims.

Què fer a la pràctica:

Cas Recomanació
Aplicació empresarial amb Spring Boot No modularitzis. Classpath, i --add-opens si alguna biblioteca ho demana
Biblioteca d'ús públic : publica module-info.java. És documentació verificada de la teva API
Aplicació d'escriptori per distribuir : jlink + jpackage justifiquen l'esforç
Contenidor on la mida importa Considera jlink encara que no modularitzis el teu codi
Codi nou des de zero, un sol artefacte Opcional. Aporta disciplina, costa fricció

El que sí que has de saber sí o sí és la part que t'afecta encara que no modularitzis: l'encapsulament fort és actiu, InaccessibleObjectException existeix, i --add-opens és la via d'escapament.

  1. Novetats d'API

Repàs de les incorporacions a la biblioteca estàndard, moltes ja fetes servir.

Col·leccions immutables (Java 9) — reprèn 05-02

List<String> empleats = List.of("Marta Ruiz", "Diego Alonso", "Nuria Vidal");
Set<String> isbns = Set.of("978-0000000001", "978-0000000002");
Map<String, Integer> prestecs = Map.of("Marta Ruiz", 4, "Diego Alonso", 2);

Map<String, Integer> molts = Map.ofEntries(
        Map.entry("Marta Ruiz", 4),
        Map.entry("Diego Alonso", 2),
        Map.entry("Nuria Vidal", 3));

// Copia immutable d una colleccio existent (Java 10)
List<String> copia = List.copyOf(llistaModificable);

Quatre regles que sorprenen:

  1. Són realment immutables. add, remove i set llancen UnsupportedOperationException.
  2. No admeten null, ni com a element ni com a clau ni com a valor. Llancen NullPointerException.
  3. Set.of i Map.of rebutgen duplicats amb IllegalArgumentException, en lloc d'ignorar-los silenciosament.
  4. L'ordre d'iteració de Set.of i Map.of no està especificat i canvia entre execucions expressament, perquè ningú no en depengui.
Set<String> conjunt = Set.of("a", "b", "c", "d");
System.out.println(conjunt);   // ordre diferent a cada execucio del programa

Arrays.asList continua essent diferent: mida fixa però permet set, i accepta null.

Mètodes privats a interfícies (Java 9) — reprèn 04-01

public interface ServeiPrestecs {

    Resultat<Prestec> prestar(String isbn, String empleat);

    default Resultat<Prestec> prestarValidant(String isbn, String empleat) {
        if (!esIsbnValid(isbn)) {
            return Resultat.fallada("ISBN mal format: " + isbn);
        }
        if (!esEmpleatValid(empleat)) {
            return Resultat.fallada("Empleat no valid");
        }
        return prestar(isbn, empleat);
    }

    // PRIVAT: logica compartida entre els default, invisible des de fora
    private boolean esIsbnValid(String isbn) {
        return isbn != null && isbn.matches("978-\\d{10}");
    }

    private static boolean esEmpleatValid(String empleat) {
        return empleat != null && !empleat.isBlank();
    }
}

Abans de Java 9, la lògica comuna entre mètodes default havia de ser un mètode default públic més, contaminant l'API.

Mètodes de String (Java 11-12)

String titol = "  Java Eficac  ";

System.out.println(titol.strip());            // "Java Eficac"  (Unicode-aware)
System.out.println(titol.stripLeading());     // "Java Eficac  "
System.out.println(titol.stripTrailing());    // "  Java Eficac"
System.out.println("   ".isBlank());          // true
System.out.println("".isEmpty());             // true
System.out.println("=".repeat(40));           // linia separadora
System.out.println("un\ndos".lines().count()); // 2  (retorna Stream<String>)
System.out.println("text".indent(4));         // sagnat (Java 12)

strip() enfront de trim(): trim() elimina caràcters amb codi menor o igual a 32 (heretat del 1996); strip() fa servir la definició Unicode d'espai en blanc i gestiona correctament espais no separables i caràcters d'altres alfabets. Fes servir strip() en codi nou.

I lines() retorna un Stream<String>, cosa que connecta amb 10-04:

long liniesNoBuides = textComplet.lines()
        .map(String::strip)
        .filter(l -> !l.isBlank())
        .count();

Files.readString i writeString (Java 11) — reprèn 07-06

// ABANS
List<String> linies = Files.readAllLines(cami, StandardCharsets.UTF_8);
String contingut = String.join("\n", linies);

// ARA
String contingut = Files.readString(cami, StandardCharsets.UTF_8);
Files.writeString(cami, contingut, StandardCharsets.UTF_8);

Per a fitxers petits de configuració és la forma més directa. Per a fitxers grans, continua essent Files.lines() amb processament mandrós (10-04).

Col·leccions seqüencials (Java 21)

Una incorporació petita que arregla una incoherència vella de l'API de col·leccions (mòdul 5):

List<Llibre> cataleg = new ArrayList<>(...);

// ABANS: cada colleccio tenia la seva forma
Llibre primer = cataleg.get(0);
Llibre ultim = cataleg.get(cataleg.size() - 1);
Llibre primerDeque = deque.getFirst();
Llibre primerSet = conjuntOrdenat.iterator().next();        // aixi!

// ARA: SequencedCollection unifica
Llibre primer2 = cataleg.getFirst();
Llibre ultim2 = cataleg.getLast();
cataleg.addFirst(nou);
cataleg.addLast(altre);
List<Llibre> alReves = cataleg.reversed();                  // vista, no copia

// I funciona igual a LinkedHashSet, TreeSet, Deque, LinkedHashMap
LinkedHashMap<String, Llibre> perIsbn = new LinkedHashMap<>();
var primeraEntrada = perIsbn.firstEntry();

Missatges útils de NullPointerException (Java 14)

Un canvi sense sintaxi nova que estalvia hores reals de depuració:

String editorial = cataleg.cercarPerIsbn(isbn).getEditorial().getNom().strip();

A Java 8:

Exception in thread "main" java.lang.NullPointerException
    at com.nexussoftware.bibliotech.BiblioTechApp.main(BiblioTechApp.java:42)

Quin dels tres era null? A depurar.

A Java 14+:

Exception in thread "main" java.lang.NullPointerException:
    Cannot invoke "String.strip()" because the return value of
    "com.nexussoftware.bibliotech.domini.Editorial.getNom()" is null
    at com.nexussoftware.bibliotech.BiblioTechApp.main(BiblioTechApp.java:42)

Et diu exactament què va retornar null i què s'hi va intentar cridar. És actiu per defecte des de Java 15, i és una de les millors raons per migrar des de Java 8.

UTF-8 per defecte (Java 18) — reprèn 07-01

Des de Java 18, el joc de caràcters per defecte de la plataforma és UTF-8 a tots els sistemes operatius. Abans depenia de la configuració regional, i era la causa de l'error clàssic: un CSV escrit a Windows amb windows-1252 que es llegia malament en un servidor Linux amb UTF-8.

Tot i així, continua especificant el charset explícitament. És el que vam insistir al mòdul 7, i continua essent la pràctica correcta: fa el codi independent de la versió de Java i de la configuració de l'entorn.

  1. var: inferència de tipus locals

Java 10 va introduir var per a variables locals. El compilador infereix el tipus de l'expressió d'inicialització.

var cataleg = new ArrayList<Llibre>();                  // ArrayList<Llibre>
var titol = "Java Eficac";                              // String
var pagines = 412;                                      // int
var data = LocalDate.of(2026, 8, 5);                    // LocalDate

// On mes brilla: tipus generics llargs
var perEmpleatITipus = new HashMap<String, Map<TipusMaterial, List<Prestec>>>();

// En bucles
for (var entrada : perEmpleatITipus.entrySet()) { }
for (var i = 0; i < 10; i++) { }

// En try-with-resources
try (var linies = Files.lines(cami, StandardCharsets.UTF_8)) { }

var NO és tipatge dinàmic. El tipus es fixa en compilació i no canvia mai:

var titol = "Java Eficac";
// titol = 42;          // NO COMPILA: String no admet int

On NO es pot fer servir:

// var camp = "x";                  // camps de classe: NO
// public var metode() { }          // retorn de metode: NO
// void m(var parametre) { }        // parametres: NO (tret de les lambdes, Java 11)
// var senseValor;                  // sense inicialitzador: NO
// var nul = null;                  // no hi ha res a inferir: NO
// var array = { 1, 2, 3 };         // inicialitzador d array: NO

Quan fer-lo servir i quan no:

// BE: el tipus es evident pel costat dret
var cataleg = new ArrayList<Llibre>();
var reserva = new Reserva("R-001", "Madrid-A", "Marta Ruiz", inici, durada);
var comptador = new AtomicInteger(0);

// BE: el tipus explicit seria illegible
var index = new HashMap<String, Map<TipusMaterial, List<Prestec>>>();

// MALAMENT: el tipus no es dedueix del nom del metode
var resultat = servei.processar(dades);        // que es 'resultat'?
var x = calcular();                             // int? double? BigDecimal?

// MALAMENT: es perd l abstraccio a la interficie
var llista = new ArrayList<Llibre>();           // el tipus es ArrayList
List<Llibre> llista2 = new ArrayList<>();       // el tipus es List: millor API

// PERILLOS: el diamant buit infereix Object
var buida = new ArrayList<>();                  // ArrayList<Object>

La regla: fes servir var quan el tipus sigui evident llegint la línia. Si cal anar a mirar la signatura d'un mètode per saber què és la variable, escriu el tipus.

I des de Java 11, var funciona en paràmetres de lambda, amb l'únic propòsit de poder anotar-los:

llista.forEach((@NoNul var llibre) -> System.out.println(llibre.getTitol()));

  1. switch d'expressió i yield — reprèn 02-03

A 02-03 vas fer servir el switch clàssic amb els seus dos problemes: la caiguda entre casos (fall-through) per oblidar un break, i que és una sentència, no una expressió, així que no pot retornar un valor sense una variable auxiliar.

// CLASSIC: verbos i amb paranys
String descripcio;
switch (gravetat) {
    case ALTA:
        descripcio = "Requereix intervencio immediata";
        break;
    case MITJANA:
        descripcio = "Revisar aquesta setmana";
        break;
    case BAIXA:
        descripcio = "Anotar i continuar";
        break;
    default:
        throw new IllegalStateException("Gravetat desconeguda: " + gravetat);
}

Java 14 el va convertir en expressió, amb fletxes i sense caiguda:

// EXPRESSIO: retorna un valor, sense break, sense caiguda
String descripcio = switch (gravetat) {
    case ALTA    -> "Requereix intervencio immediata";
    case MITJANA -> "Revisar aquesta setmana";
    case BAIXA   -> "Anotar i continuar";
};

Quatre avantatges concrets:

  1. Sense break, perquè no hi ha caiguda entre casos.
  2. És una expressió: s'assigna, es retorna, es passa com a argument.
  3. Diverses etiquetes per cas: case SATURDAY, SUNDAY ->.
  4. Exhaustivitat comprovada: sobre un enum, si falten casos i no hi ha default, no compila.

Aquesta última és la més valuosa. Si demà algú afegeix Gravetat.CRITICA, el compilador assenyalarà tots els switch que cal actualitzar. Amb el switch clàssic i un default, el cas nou cauria silenciosament al default.

yield per a blocs amb lògica:

double multa = switch (gravetat) {
    case ALTA -> {
        double base = dies * 0.50;
        double recarrec = dies > 30 ? 10.0 : 0.0;
        yield Math.min(base + recarrec, 50.0);     // yield retorna el valor del bloc
    }
    case MITJANA -> dies * 0.25;
    case BAIXA -> 0.0;
};

yield és a un bloc de switch el que return a un mètode. No es pot fer servir return dins d'un switch d'expressió: sortiria del mètode sencer, que no és el que es vol.

I el switch d'expressió també funciona sobre String i int, amb la diferència que allà sí que cal default, perquè els valors possibles són infinits:

int diesDeTermini = switch (tipus) {
    case "LLIBRE"  -> 21;
    case "REVISTA" -> 7;
    case "DVD"     -> 3;
    default -> throw new IllegalArgumentException("Tipus desconegut: " + tipus);
};

  1. Blocs de text

Java 15 va incorporar les cadenes multilínia, i la diferència en llegibilitat és enorme.

// ABANS: illegible, amb escapaments i concatenacio
String json = "{\n"
        + "  \"isbn\": \"978-0000000001\",\n"
        + "  \"titol\": \"Java Eficac\",\n"
        + "  \"disponible\": true\n"
        + "}";

// ARA
String json = """
        {
          "isbn": "978-0000000001",
          "titol": "Java Eficac",
          "disponible": true
        }""";

Regles de sintaxi:

  1. S'obre amb """ seguit d'un salt de línia obligatori. """text no compila.
  2. Es tanca amb """, a la mateixa línia que l'últim contingut o en una línia pròpia.
  3. Les cometes dobles no necessiten escapament.

La regla de sagnat (la que importa). El compilador calcula el marge comú mínim de totes les línies no buides inclosa la del tancament, i l'elimina:

public class Exemple {
    public String consulta() {
        String sql = """
                SELECT p.referencia, p.isbn, m.titol
                  FROM prestecs p
                  JOIN materials m ON m.isbn = p.isbn
                 WHERE p.data_devolucio IS NULL
                 ORDER BY p.data_venciment
                """;
        return sql;
    }
}

La cadena resultant no porta els 16 espais de sagnat del codi Java, només el sagnat relatiu de les línies del SQL. I aquí hi ha el detall que confon: la posició del """ de tancament participa en el càlcul.

// El tancament a l esquerra del contingut: es conserva mes sagnat
String a = """
        linia
  """;                    // el marge comu es 2: "      linia\n"

// El tancament alineat amb el contingut: s elimina tot el sagnat
String b = """
        linia
        """;              // el marge comu es 8: "linia\n"

// El tancament a la mateixa linia: sense salt final
String c = """
        linia""";         // "linia"  (sense \n al final)

Seqüències d'escapament pròpies dels blocs de text:

// \ al final de linia: UNEIX les linies (sense salt)
String unaSolaLinia = """
        Aquest text esta escrit en diverses linies \
        del codi font pero es una sola \
        linia de resultat.""";

// \s: espai que NO s elimina com a sagnat final
String ambEspais = """
        columna1   \s
        columna2   \s""";

L'ús estrella són SQL i JSON, que veuràs contínuament als mòduls 11 i 12:

public class ConsultesBiblioTech {

    static final String PRESTECS_VENCUTS = """
            SELECT p.referencia,
                   p.isbn,
                   m.titol,
                   p.empleat,
                   p.data_venciment,
                   CURRENT_DATE - p.data_venciment AS dies_retard
              FROM prestecs p
              JOIN materials m ON m.isbn = p.isbn
             WHERE p.data_devolucio IS NULL
               AND p.data_venciment < CURRENT_DATE
             ORDER BY dies_retard DESC
            """;

    static String peticioDeMetadades(String isbn) {
        return """
                {
                  "jsonrpc": "2.0",
                  "method": "cataleg.cercar",
                  "params": { "isbn": "%s" },
                  "id": 1
                }""".formatted(isbn);          // formatted: com String.format
    }
}

formatted() (Java 15) és String.format com a mètode d'instància, i encaixa perfectament amb els blocs de text.

  1. record revisitat — reprèn 04-07

Els vas conèixer a 04-07. Aquí convé fixar què són exactament i què se'ls pot fer, perquè a l'apartat 15 seran la peça clau del pattern matching.

Un record és un portador transparent de dades immutables:

public record Fitxa(String isbn, String titol, boolean disponible) { }

El compilador genera automàticament:

  • Camps private final per a cada component.
  • Constructor canònic amb tots els components.
  • Mètodes d'accés amb el nom del component (isbn(), no getIsbn()).
  • equals i hashCode basats en tots els components.
  • toString amb tots els components.

Compacte però complet:

public record Prestec(String referencia, String isbn, String empleat,
                      LocalDate dataPrestec, LocalDate dataVenciment) {

    /** Constructor COMPACTE: valida i normalitza abans d assignar. */
    public Prestec {
        Objects.requireNonNull(referencia, "referencia");
        Objects.requireNonNull(isbn, "isbn");
        if (dataVenciment.isBefore(dataPrestec)) {
            throw new IllegalArgumentException("Venciment anterior al prestec");
        }
        empleat = empleat.strip();         // es pot NORMALITZAR el parametre
    }

    /** Constructor addicional. */
    public Prestec(String referencia, String isbn, String empleat, LocalDate des) {
        this(referencia, isbn, empleat, des, des.plusDays(21));
    }

    /** Metodes derivats. */
    public long diesDeRetard(LocalDate avui) {
        return Math.max(0, ChronoUnit.DAYS.between(dataVenciment, avui));
    }

    /** "Modificar" un record = crear ne un altre. */
    public Prestec ambVenciment(LocalDate nou) {
        return new Prestec(referencia, isbn, empleat, dataPrestec, nou);
    }

    /** Metodes i camps estatics SI que es permeten. */
    public static final Period TERMINI = Period.ofDays(21);

    public static Prestec nou(String ref, String isbn, String empleat, LocalDate des) {
        return new Prestec(ref, isbn, empleat, des, des.plus(TERMINI));
    }
}

Limitacions (que són decisions de disseny, no mancances):

  • No poden estendre cap classe (estenen java.lang.Record implícitament). Sí que poden implementar interfícies.
  • Són implícitament final: ningú no els pot estendre.
  • No poden tenir camps d'instància a part dels components.
  • Els components són final: no hi ha setters.

Quan fer servir record i quan una classe:

Fes servir record Fes servir una classe
Portador de dades immutable Hi ha estat mutable
La identitat és el valor dels camps La identitat és la referència
DTO, valor de retorn, clau de mapa Necessites herència
Resultat d'un càlcul Encapsules la representació interna

A BiblioTech: Fitxa i ResumSessio són records; Material és una classe abstracta perquè hi ha jerarquia; Prestec pot ser qualsevol de les dues segons si el seu estat canvia.

  1. instanceof amb patró — reprèn 03-06

Definitiu a Java 16. Elimina la conversió redundant després d'un instanceof:

// ABANS: la conversio repeteix el que l instanceof ja va comprovar
if (material instanceof Llibre) {
    Llibre llibre = (Llibre) material;
    System.out.println(llibre.getPagines());
}

// ARA: la variable de patro es declara i s assigna alhora
if (material instanceof Llibre llibre) {
    System.out.println(llibre.getPagines());
}

L'àmbit de la variable de patró es calcula amb lògica de flux, cosa que produeix comportaments molt útils:

// Amb && : la variable esta disponible a la dreta
if (material instanceof Llibre llibre && llibre.getPagines() > 400) {
    System.out.println("Llibre llarg: " + llibre.getTitol());
}

// Amb negacio: disponible DESPRES de l if (sortida primerenca)
public String descriure(Object objecte) {
    if (!(objecte instanceof Material material)) {
        return "No es un material";
    }
    // 'material' esta en ambit aqui, perque si no ho estigues ja hauriem tornat
    return material.getTitol() + " (" + material.getTipus() + ")";
}

// Amb || : NO esta disponible, i el compilador ho impedeix
// if (o instanceof Llibre l || l.getPagines() > 0) { }   // NO COMPILA

I equals queda especialment net (03-09):

@Override
public boolean equals(Object altre) {
    return altre instanceof Material material
            && isbn.equals(material.getIsbn());
}

De sis línies a dues, sense perdre res.

  1. Classes segellades: sealed, permits, non-sealed

Definitives a Java 17. Resolen un problema real que fins ara no tenia solució.

El problema. A BiblioTech, Material és abstracta amb tres subclasses: Llibre, Revista i Dvd. Però qualsevol pot crear-ne una quarta:

// En un altre projecte, sense que ningu ho aprovi
public class Videojoc extends Material { }

I tots els switch i cadenes d'instanceof que vas escriure assumint tres tipus queden incomplets, sense que el compilador digui res.

Les alternatives abans de Java 17 eren dolentes: fer el constructor package-private (fràgil i no impedeix subclasses al mateix paquet) o simplement confiar i documentar.

La solució:

package com.nexussoftware.bibliotech.domini;

/**
 * Jerarquia TANCADA: nomes existeixen aquests tres materials.
 * Cap altra classe no pot estendre Material, ni en aquest
 * projecte ni en cap.
 */
public sealed abstract class Material
        permits Llibre, Revista, Dvd {

    private final String isbn;
    private final String titol;

    protected Material(String isbn, String titol) {
        this.isbn = isbn;
        this.titol = titol;
    }

    public String getIsbn()  { return isbn; }
    public String getTitol() { return titol; }

    public abstract TipusMaterial getTipus();
}

Cada subclasse ha de declarar la seva pròpia obertura, i aquesta és la part que s'oblida:

/** final: ningu no esten Llibre. Es el mes habitual. */
public final class Llibre extends Material {
    private final int pagines;
    private final String autor;
    // ...
}

/** sealed: Revista te al seu torn una jerarquia tancada. */
public sealed class Revista extends Material permits RevistaMensual, RevistaTrimestral {
    // ...
}

/** non-sealed: Dvd torna a estar obert a qualsevol. */
public non-sealed class Dvd extends Material {
    // ...
}
Modificador Significat
final Ningú no la pot estendre. El cas més comú
sealed ... permits Només les classes llistades la poden estendre
non-sealed Qualsevol la pot estendre: reobre la jerarquia

Les regles que imposa el compilador:

  1. Tota subclasse d'una classe sealed ha de ser final, sealed o non-sealed. No hi ha quarta opció.
  2. Les classes permeses han de ser al mateix mòdul (o al mateix paquet si no hi ha mòduls).
  3. Han d'estendre-la realment: llistar una classe que no l'estengui és error de compilació.
  4. permits es pot ometre si les subclasses són al mateix fitxer.
// Tot en un fitxer: permits implicit
public sealed interface Esdeveniment {
    record Prestat(String isbn, String empleat, LocalDate quan) implements Esdeveniment { }
    record Retornat(String isbn, LocalDate quan, boolean ambRetard) implements Esdeveniment { }
    record Perdut(String isbn, String empleat, double quantitat) implements Esdeveniment { }
}

El que guanyes: exhaustivitat. El compilador sap que només hi ha tres tipus, i per tant pot verificar que els has cobert tots:

// SENSE default: el compilador comprova que hi son els tres
String etiqueta = switch (material) {
    case Llibre l  -> "Llibre de " + l.getPagines() + " pagines";
    case Revista r -> "Revista num. " + r.getNumero();
    case Dvd d     -> "DVD de " + d.getMinuts() + " minuts";
};

Si demà algú afegeix Mapa a la llista de permits i no actualitza aquest switch:

error: the switch expression does not cover all possible input values
    String etiqueta = switch (material) {
                      ^

El compilador et porta de la mà per tots els llocs que cal tocar. Això és el que fa que les jerarquies segellades s'anomenin "tipus algebraics" o "tipus suma": el conjunt de possibilitats és tancat i conegut.

I combinades amb record, modelen estats de manera molt expressiva:

package com.nexussoftware.bibliotech.domini;

import java.time.LocalDate;

/**
 * Estat d un prestec modelat com a tipus suma.
 * Cada estat porta EXACTAMENT les dades que te sentit
 * que porti, en lloc d una classe amb camps que de vegades son null.
 */
public sealed interface EstatPrestec {

    record Actiu(LocalDate venciment) implements EstatPrestec { }

    record Vencut(LocalDate venciment, long diesDeRetard, double multa)
            implements EstatPrestec { }

    record Retornat(LocalDate quan, boolean puntual) implements EstatPrestec { }

    record Perdut(LocalDate declaratEl, double quantitatReposicio)
            implements EstatPrestec { }
}

Compara amb l'alternativa clàssica: una classe Prestec amb boolean retornat, LocalDate dataDevolucio (null si no s'ha retornat), double multa (0 si no s'aplica) i boolean perdut. Combinacions impossibles representables: retornat i perdut alhora, multa sense venciment. Amb el tipus segellat, aquests estats no es poden construir.

  1. Pattern matching per a switch i patrons de registre

Definitius a Java 21, tanquen el cercle.

Patrons de tipus a switch

// ABANS: cadena d instanceof, amb conversio a cada branca
public String descriure(Object objecte) {
    if (objecte instanceof Llibre) {
        Llibre l = (Llibre) objecte;
        return "Llibre: " + l.getTitol() + " (" + l.getPagines() + " pags.)";
    } else if (objecte instanceof Revista) {
        Revista r = (Revista) objecte;
        return "Revista: " + r.getTitol() + " num. " + r.getNumero();
    } else if (objecte instanceof Dvd) {
        Dvd d = (Dvd) objecte;
        return "DVD: " + d.getTitol() + " (" + d.getMinuts() + " min)";
    } else if (objecte == null) {
        return "(res)";
    }
    return "Desconegut";
}

// ARA
public String descriure(Object objecte) {
    return switch (objecte) {
        case null      -> "(res)";
        case Llibre l  -> "Llibre: " + l.getTitol() + " (" + l.getPagines() + " pags.)";
        case Revista r -> "Revista: " + r.getTitol() + " num. " + r.getNumero();
        case Dvd d     -> "DVD: " + d.getTitol() + " (" + d.getMinuts() + " min)";
        default        -> "Desconegut: " + objecte.getClass().getSimpleName();
    };
}

case null és una novetat important. El switch clàssic llançava NullPointerException amb un valor nul; ara es pot tractar com un cas més. Si no es declara case null, continua llançant NullPointerException per compatibilitat.

Guardes amb when

public String classificar(Material material) {
    return switch (material) {
        case Llibre l when l.getPagines() > 500 -> "Llibre extens";
        case Llibre l when l.getPagines() > 200 -> "Llibre estandard";
        case Llibre l                           -> "Llibre breu";
        case Revista r when r.esEspecial()      -> "Numero especial";
        case Revista r                          -> "Revista ordinaria";
        case Dvd d                              -> "Material audiovisual";
    };
}

L'ordre importa: els casos s'avaluen de dalt a baix, i el més específic va primer. El compilador impedeix posar un cas inabastable:

case Llibre l                           -> "...";
case Llibre l when l.getPagines() > 500 -> "...";   // ERROR: inabastable

Patrons de registre (record patterns)

La peça més potent: descompondre un record directament al patró.

public sealed interface Esdeveniment {
    record Prestat(String isbn, String empleat, LocalDate quan) implements Esdeveniment { }
    record Retornat(String isbn, LocalDate quan, boolean ambRetard) implements Esdeveniment { }
    record Perdut(String isbn, String empleat, double quantitat) implements Esdeveniment { }
}
public String registrar(Esdeveniment esdeveniment) {
    return switch (esdeveniment) {

        // Es DESCOMPON el record: isbn, empleat i quan surten directament
        case Esdeveniment.Prestat(String isbn, String empleat, LocalDate quan) ->
                String.format("%s va prestar %s el %s", empleat, isbn, quan);

        // Amb guarda sobre un component descompost
        case Esdeveniment.Retornat(String isbn, LocalDate quan, boolean retard) when retard ->
                String.format("%s retornat el %s AMB RETARD", isbn, quan);

        case Esdeveniment.Retornat(String isbn, LocalDate quan, boolean retard) ->
                String.format("%s retornat puntualment el %s", isbn, quan);

        case Esdeveniment.Perdut(String isbn, String empleat, double quantitat) ->
                String.format("%s va declarar perdut %s (reposicio: %.2f €)",
                        empleat, isbn, quantitat);
    };
    // SENSE default: la interficie es sealed i hi son els tres casos
}

I amb var per no repetir tipus:

case Esdeveniment.Prestat(var isbn, var empleat, var quan) ->
        empleat + " va prestar " + isbn + " el " + quan;

Els patrons s'imbriquen, que és on el mecanisme es torna realment expressiu:

public record Punt(int x, int y) { }
public record Linia(Punt inici, Punt fi) { }

public String descriure(Object o) {
    return switch (o) {
        // Descomposicio IMBRICADA en dos nivells
        case Linia(Punt(var x1, var y1), Punt(var x2, var y2)) when x1 == x2 ->
                "Linia vertical a x=" + x1;
        case Linia(Punt(var x1, var y1), Punt(var x2, var y2)) when y1 == y2 ->
                "Linia horitzontal a y=" + y1;
        case Linia(Punt p1, Punt p2) ->
                "Linia de " + p1 + " a " + p2;
        case Punt(int x, int y) ->
                "Punt (" + x + ", " + y + ")";
        default -> "Una altra cosa";
    };
}

I també funciona amb instanceof:

if (esdeveniment instanceof Esdeveniment.Prestat(String isbn, String empleat, LocalDate quan)) {
    System.out.println(empleat + " -> " + isbn + " (" + quan + ")");
}

  1. BiblioTech: refactorització de la jerarquia

Ho apliquem tot alhora, amb l'abans i el després.

Abans

package com.nexussoftware.bibliotech.servei;

import com.nexussoftware.bibliotech.domini.*;

/**
 * VERSIO ANTIGA: jerarquia oberta, conversions manuals,
 * cadenes d if-else i un default que amaga els casos nous.
 */
public class CalculadoraTerminisAntiga {

    public int diesDeTermini(Material material) {
        if (material instanceof Llibre) {
            Llibre llibre = (Llibre) material;
            if (llibre.getPagines() > 500) {
                return 30;
            } else if (llibre.getPagines() > 200) {
                return 21;
            } else {
                return 14;
            }
        } else if (material instanceof Revista) {
            Revista revista = (Revista) material;
            if (revista.esEspecial()) {
                return 14;
            }
            return 7;
        } else if (material instanceof Dvd) {
            return 3;
        }
        // I si algu afegeix un tipus nou, cau AQUI en silenci
        throw new IllegalArgumentException("Tipus desconegut: " + material.getClass());
    }

    public String etiquetaDePrestatge(Material material) {
        String prefix;
        if (material instanceof Llibre) {
            prefix = "L";
        } else if (material instanceof Revista) {
            prefix = "R";
        } else if (material instanceof Dvd) {
            prefix = "D";
        } else {
            prefix = "X";
        }
        return prefix + "-" + material.getIsbn().substring(4, 8);
    }

    public double valorDeReposicio(Material material) {
        if (material instanceof Llibre) {
            return ((Llibre) material).getPagines() * 0.08;
        } else if (material instanceof Revista) {
            return 12.0;
        } else if (material instanceof Dvd) {
            return ((Dvd) material).getMinuts() * 0.15;
        }
        return 0.0;
    }
}

Després

package com.nexussoftware.bibliotech.domini;

/** Jerarquia SEGELLADA: el compilador sap que nomes existeixen tres materials. */
public sealed abstract class Material permits Llibre, Revista, Dvd {

    private final String isbn;
    private final String titol;
    private final double valoracio;

    protected Material(String isbn, String titol, double valoracio) {
        this.isbn = isbn;
        this.titol = titol;
        this.valoracio = valoracio;
    }

    public String getIsbn()     { return isbn; }
    public String getTitol()    { return titol; }
    public double getValoracio() { return valoracio; }
}
public final class Llibre extends Material {
    private final int pagines;
    private final String autor;

    public Llibre(String isbn, String titol, String autor, int pagines, double valoracio) {
        super(isbn, titol, valoracio);
        this.autor = autor;
        this.pagines = pagines;
    }
    public int getPagines()  { return pagines; }
    public String getAutor() { return autor; }
}

public final class Revista extends Material {
    private final int numero;
    private final boolean especial;

    public Revista(String isbn, String titol, int numero, boolean especial, double valoracio) {
        super(isbn, titol, valoracio);
        this.numero = numero;
        this.especial = especial;
    }
    public int getNumero()      { return numero; }
    public boolean esEspecial() { return especial; }
}

public final class Dvd extends Material {
    private final int minuts;
    private final String director;

    public Dvd(String isbn, String titol, String director, int minuts, double valoracio) {
        super(isbn, titol, valoracio);
        this.director = director;
        this.minuts = minuts;
    }
    public int getMinuts()      { return minuts; }
    public String getDirector() { return director; }
}
package com.nexussoftware.bibliotech.servei;

import com.nexussoftware.bibliotech.domini.*;

/**
 * VERSIO MODERNA: switch exhaustiu sense default,
 * sense conversions, amb guardes. Si s afegeix un quart material,
 * AQUESTS TRES METODES DEIXEN DE COMPILAR i el compilador
 * et porta a cadascun.
 */
public class CalculadoraTerminis {

    public int diesDeTermini(Material material) {
        return switch (material) {
            case Llibre l when l.getPagines() > 500 -> 30;
            case Llibre l when l.getPagines() > 200 -> 21;
            case Llibre l                           -> 14;
            case Revista r when r.esEspecial()      -> 14;
            case Revista r                          -> 7;
            case Dvd d                              -> 3;
        };
    }

    public String etiquetaDePrestatge(Material material) {
        String prefix = switch (material) {
            case Llibre l  -> "L";
            case Revista r -> "R";
            case Dvd d     -> "D";
        };
        return prefix + "-" + material.getIsbn().substring(4, 8);
    }

    public double valorDeReposicio(Material material) {
        return switch (material) {
            case Llibre l  -> l.getPagines() * 0.08;
            case Revista r -> r.esEspecial() ? 25.0 : 12.0;
            case Dvd d     -> d.getMinuts() * 0.15;
        };
    }

    /** Amb blocs i yield quan hi ha logica. */
    public String fitxaCompleta(Material material) {
        return switch (material) {
            case Llibre l -> {
                String extensio = l.getPagines() > 400 ? "extens" : "estandard";
                yield """
                        LLIBRE
                          Titol:      %s
                          Autor:      %s
                          Pagines:    %d (%s)
                          Termini:    %d dies
                          Reposicio:  %.2f €"""
                        .formatted(l.getTitol(), l.getAutor(), l.getPagines(),
                                   extensio, diesDeTermini(l), valorDeReposicio(l));
            }
            case Revista r -> """
                    REVISTA
                      Titol:      %s
                      Numero:     %d%s
                      Termini:    %d dies"""
                    .formatted(r.getTitol(), r.getNumero(),
                               r.esEspecial() ? " (ESPECIAL)" : "", diesDeTermini(r));
            case Dvd d -> """
                    DVD
                      Titol:      %s
                      Director:   %s
                      Durada:     %d min
                      Termini:    %d dies"""
                    .formatted(d.getTitol(), d.getDirector(), d.getMinuts(), diesDeTermini(d));
        };
    }
}

I l'estat del préstec amb patrons de registre:

package com.nexussoftware.bibliotech.servei;

import com.nexussoftware.bibliotech.domini.EstatPrestec;

import java.time.format.DateTimeFormatter;
import java.util.Locale;

public class DescriptorEstat {

    private static final DateTimeFormatter DATA =
            DateTimeFormatter.ofPattern("d 'de' MMMM", new Locale("ca", "ES"));

    public String descriure(EstatPrestec estat) {
        return switch (estat) {

            case EstatPrestec.Actiu(var venciment) ->
                    "Actiu, venc el " + venciment.format(DATA);

            case EstatPrestec.Vencut(var venciment, var dies, var multa) when multa == 0 ->
                    "Vencut fa " + dies + " dies (encara en periode de cortesia)";

            case EstatPrestec.Vencut(var venciment, var dies, var multa) ->
                    String.format("VENCUT fa %d dies -- multa acumulada: %.2f €", dies, multa);

            case EstatPrestec.Retornat(var quan, var puntual) ->
                    "Retornat el " + quan.format(DATA) + (puntual ? " puntualment" : " amb retard");

            case EstatPrestec.Perdut(var declarat, var quantitat) ->
                    String.format("PERDUT des del %s -- reposicio: %.2f €",
                            declarat.format(DATA), quantitat);
        };
    }

    /** Prioritat de gestio: un switch que retorna un int. */
    public int prioritat(EstatPrestec estat) {
        return switch (estat) {
            case EstatPrestec.Perdut p                                 -> 100;
            case EstatPrestec.Vencut(var v, var dies, var m) when dies > 30 -> 90;
            case EstatPrestec.Vencut v                                 -> 50;
            case EstatPrestec.Actiu a                                  -> 10;
            case EstatPrestec.Retornat r                               -> 0;
        };
    }
}
List<EstatPrestec> estats = List.of(
        new EstatPrestec.Actiu(LocalDate.of(2026, 8, 26)),
        new EstatPrestec.Vencut(LocalDate.of(2026, 8, 3), 2, 0.0),
        new EstatPrestec.Vencut(LocalDate.of(2026, 5, 22), 75, 18.25),
        new EstatPrestec.Retornat(LocalDate.of(2026, 8, 1), true),
        new EstatPrestec.Perdut(LocalDate.of(2026, 7, 15), 42.50));

DescriptorEstat descriptor = new DescriptorEstat();

estats.stream()
      .sorted(Comparator.comparingInt(descriptor::prioritat).reversed())
      .forEach(e -> System.out.printf("  [%3d] %s%n",
              descriptor.prioritat(e), descriptor.descriure(e)));
  [100] PERDUT des del 15 de juliol -- reposicio: 42,50 €
  [ 90] VENCUT fa 75 dies -- multa acumulada: 18,25 €
  [ 50] Vencut fa 2 dies (encara en periode de cortesia)
  [ 10] Actiu, venc el 26 de agost
  [  0] Retornat el 1 de agost puntualment

La comparació en números: 45 línies d'if-else amb conversions contra 20 línies de switch exhaustiu. Però l'important no és la longitud: és que si algú afegeix un quart material o un cinquè estat, el codi deixa de compilar i el compilador assenyala cada lloc que cal actualitzar. La versió antiga hauria continuat compilant i fallant en execució.

  1. Fils virtuals: què són

I arribem al que portes esperant des de 09-03.

El problema, recapitulat. A 09-03 vas escriure ServidorCataleg amb el model "un fil per connexió", i vas descobrir que no escala: cada fil de plataforma consumeix al voltant d'1 MB de pila i la seva creació costa prop d'un mil·lisegon. Deu mil connexions simultànies serien deu gigabytes de pila. Per això vas haver d'acotar el pool a un nombre modest de fils, i per això el servidor rebutja clients quan se satura.

Aquest compromís —fils barats o codi llegible, tria'n un— ha dominat la programació de servidors durant vint anys. L'alternativa era la programació asíncrona (CompletableFuture, 08-07), que escala però converteix un flux lineal en una embolica de retrucades on el depurador no serveix i les traces de pila no diuen res.

Els fils virtuals (Java 21, Projecte Loom) eliminen el compromís.

Un fil virtual és un fil gestionat per la JVM, no pel sistema operatiu. S'executa sobre un petit conjunt de fils de plataforma anomenats portadors (carrier threads). La clau és el comportament en bloquejar-se:

graph TD
    A["10.000 fils virtuals"] --> B["Planificador de la JVM<br/>ForkJoinPool"]
    B --> C["Fil portador 1"]
    B --> D["Fil portador 2"]
    B --> E["Fil portador N<br/>(N = nuclis)"]
    C --> F["Sistema operatiu"]
    D --> F
    E --> F
    G["Un fil virtual es bloqueja<br/>en E/S"] -->|"es DESMUNTA del portador"| H["La seva pila es guarda al heap"]
    H -->|"el portador queda LLIURE<br/>per a un altre fil virtual"| B
    I["Arriben les dades"] -->|"es REMUNTA en un portador"| B

Quan un fil de plataforma es bloqueja, el fil del sistema operatiu queda aturat. Quan un fil virtual es bloqueja, es desmunta del portador: la seva pila es guarda al heap i el portador passa a executar un altre fil virtual. En arribar les dades, es remunta i continua exactament on era.

El resultat: pots bloquejar alegrement, perquè bloquejar un fil virtual no bloqueja cap fil del sistema operatiu. El codi seqüencial llegible torna a escalar.

// Crear ne un de solt
Thread virtual = Thread.ofVirtual().name("worker-1").start(() -> {
    System.out.println("Soc un fil virtual: " + Thread.currentThread());
});
virtual.join();

// Sense arrencar
Thread senseArrencar = Thread.ofVirtual().unstarted(tasca);

// Fabrica de fils virtuals
ThreadFactory fabrica = Thread.ofVirtual().name("bibliotech-", 0).factory();

// EL MES IMPORTANT: executor amb un fil virtual PER TASCA
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 10_000; i++) {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return "llest";
        });
    }
}   // close() espera que acabin totes: l executor es AutoCloseable

Fixa't en dos detalls: ExecutorService és AutoCloseable des de Java 19, així que el try-with-resources de 06-06 substitueix el shutdown() + awaitTermination() de 08-05. I Thread.sleep(Duration) accepta un Duration de 10-05.

La demostració

package com.nexussoftware.bibliotech;

import java.time.Duration;
import java.util.concurrent.*;
import java.util.stream.IntStream;

public class DemostracioFilsVirtuals {

    private static final int TASQUES = 10_000;

    public static void main(String[] args) throws Exception {
        System.out.println("Tasques: " + TASQUES + ", cadascuna bloquejada 1 segon");
        System.out.println("Nuclis: " + Runtime.getRuntime().availableProcessors());
        System.out.println();

        mesurar("Pool de plataforma (200 fils)",
                Executors.newFixedThreadPool(200));

        mesurar("Un fil VIRTUAL per tasca    ",
                Executors.newVirtualThreadPerTaskExecutor());

        // I aixo NO es pot fer amb fils de plataforma
        System.out.println();
        System.out.println("--- 1.000.000 de fils virtuals ---");
        long inici = System.currentTimeMillis();
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            IntStream.range(0, 1_000_000).forEach(i ->
                    executor.submit(() -> {
                        Thread.sleep(Duration.ofSeconds(1));
                        return i;
                    }));
        }
        System.out.printf("Un milio de tasques en %d ms%n",
                System.currentTimeMillis() - inici);
    }

    private static void mesurar(String etiqueta, ExecutorService executor) throws Exception {
        long inici = System.currentTimeMillis();
        try (executor) {
            for (int i = 0; i < TASQUES; i++) {
                executor.submit(() -> {
                    Thread.sleep(Duration.ofSeconds(1));   // simula espera d E/S
                    return null;
                });
            }
        }
        System.out.printf("%s -> %6d ms%n", etiqueta, System.currentTimeMillis() - inici);
    }
}
Tasques: 10000, cadascuna bloquejada 1 segon
Nuclis: 8

Pool de plataforma (200 fils) ->  50238 ms
Un fil VIRTUAL per tasca      ->   1104 ms

--- 1.000.000 de fils virtuals ---
Un milio de tasques en 2871 ms

Cinquanta segons contra un. El pool de 200 fils processa 200 tasques simultànies i les 10.000 triguen 50 rondes d'un segon. Els fils virtuals executen les 10.000 alhora, i el total és el temps d'una sola tasca més l'arrencada.

I un milió de fils virtuals en menys de tres segons. Un milió de fils de plataforma serien aproximadament un terabyte de pila: la JVM no arrencaria.

  1. Fils de plataforma enfront de fils virtuals

Aspecte Fil de plataforma Fil virtual
Implementació Embolcall d'un fil del SO Objecte gestionat per la JVM
Cost de creació ~1 ms ~1 µs (mil vegades menys)
Memòria (pila) ~1 MB reservat Centenars de bytes, creix al heap
Nombre viable Milers Milions
Planificació Sistema operatiu, expropiativa JVM, cooperativa en punts de bloqueig
En bloquejar-se El fil del SO s'atura Es desmunta, allibera el portador
Agrupar en pool Imprescindible Contraproduent: són d'un sol ús
ThreadLocal Funciona, amb compte Funciona, però desaconsellat amb milions
synchronized Sense problemes Pot provocar pinning
Prioritats No: s'ignoren
Nom per defecte Thread-N Buit (cal donar-lo explícitament)
Bo per a Càlcul intensiu E/S: xarxa, fitxers, base de dades
Apareixen a jstack Només amb bolcat de fils estès

Les quatre regles pràctiques:

1. No agrupis fils virtuals en un pool. Els pools existeixen perquè crear un fil de plataforma és car. Crear un fil virtual és quasi de franc. Executors.newFixedThreadPool(200) amb fils virtuals seria absurd: reintrodueix el límit que els fils virtuals eliminen.

// MALAMENT: contradiu el proposit
ExecutorService malament = Executors.newFixedThreadPool(200, Thread.ofVirtual().factory());

// BE: un per tasca
ExecutorService be = Executors.newVirtualThreadPerTaskExecutor();

2. Un fil virtual per tasca, sempre. No els reutilitzis ni els guardis.

3. Per limitar la concurrència, fes servir un Semaphore, no la mida del pool. Si el servei extern només aguanta 50 peticions simultànies, això és una restricció del recurs, no dels fils:

Semaphore limit = new Semaphore(50);

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (String isbn : isbns) {
        executor.submit(() -> {
            limit.acquire();
            try {
                return clientMetadades.consultar(isbn);
            } finally {
                limit.release();
            }
        });
    }
}

4. No acceleren el càlcul. Vegeu l'apartat 20.

  1. BiblioTech: el ServidorCataleg amb fils virtuals

La refactorització que 09-03 va deixar pendent.

Abans (Java 17, pool acotat)

package com.nexussoftware.bibliotech.xarxa;

import java.io.IOException;
import java.net.*;
import java.util.concurrent.*;
import java.util.logging.Logger;

/**
 * VERSIO DEL MODUL 9: pool acotat.
 *
 * El pool limita a 50 clients simultanis. Amb 51 connexions,
 * la 51 espera a la cua; si la cua s omple, es REBUTJA.
 * I no es pot pujar gaire: cada fil son ~1 MB de pila.
 */
public class ServidorCatalegAmbPool {

    private static final Logger LOG = Logger.getLogger(ServidorCatalegAmbPool.class.getName());

    private static final int PORT = 9090;
    private static final int MAX_CLIENTS = 50;       // <-- el limit
    private static final int CUA = 100;

    private final ExecutorService pool = new ThreadPoolExecutor(
            MAX_CLIENTS, MAX_CLIENTS,
            60L, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(CUA),
            new ThreadPoolExecutor.CallerRunsPolicy());

    private volatile boolean actiu = true;

    public void arrencar() throws IOException {
        try (ServerSocket servidor = new ServerSocket(PORT)) {
            servidor.setSoTimeout(1000);
            LOG.info("Servidor al port " + PORT + " (max. " + MAX_CLIENTS + " clients)");

            while (actiu) {
                try {
                    Socket client = servidor.accept();
                    pool.submit(() -> atendre(client));
                } catch (SocketTimeoutException e) {
                    // cicle de comprovacio d 'actiu'
                }
            }
        } finally {
            pool.shutdown();
        }
    }

    private void atendre(Socket client) { /* protocol BTCP/1 */ }
}

Després (Java 21, fils virtuals)

package com.nexussoftware.bibliotech.xarxa;

import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * VERSIO AMB FILS VIRTUALS.
 *
 * Un fil virtual per connexio, sense pool i sense limit artificial.
 * El codi d atendre() es SEQUENCIAL i BLOQUEJANT, que es com
 * cal escriure l, i tot i aixi escala a desenes de milers de clients.
 */
public class ServidorCataleg implements AutoCloseable {

    private static final Logger LOG = Logger.getLogger(ServidorCataleg.class.getName());

    private static final int PORT = 9090;
    private static final Duration INACTIVITAT = Duration.ofSeconds(30);
    private static final int MAX_LINIA = 4096;

    /**
     * Un fil VIRTUAL per tasca. Ja no hi ha mida de pool a ajustar,
     * ni cua, ni politica de rebuig.
     */
    private final ExecutorService atenedors = Executors.newVirtualThreadPerTaskExecutor();

    private final AtomicLong connexionsTotals = new AtomicLong();
    private final AtomicLong connexionsActives = new AtomicLong();

    private volatile boolean actiu = true;
    private ServerSocket servidor;

    public void arrencar() throws IOException {
        servidor = new ServerSocket(PORT);
        servidor.setSoTimeout(1000);

        LOG.info(() -> "ServidorCataleg escoltant al port " + PORT
                + " (un fil virtual per connexio, sense limit de pool)");

        // Fil virtual dedicat a informar de l estat
        Thread.ofVirtual().name("monitor").start(this::monitoritzar);

        while (actiu) {
            try {
                Socket client = servidor.accept();

                // Un fil virtual PER CONNEXIO. Crear lo costa microsegons.
                atenedors.submit(() -> atendre(client));

            } catch (SocketTimeoutException e) {
                // cicle normal
            } catch (IOException e) {
                if (actiu) {
                    LOG.log(Level.WARNING, "Error en acceptar la connexio", e);
                }
            }
        }
    }

    /**
     * Codi SEQUENCIAL i BLOQUEJANT: exactament el mateix de 09-03.
     * readLine() bloqueja, i no passa res: el fil virtual es desmunta.
     */
    private void atendre(Socket client) {
        long numero = connexionsTotals.incrementAndGet();
        connexionsActives.incrementAndGet();

        // Anomenar el fil fa llegibles les traces i els bolcats
        Thread.currentThread().setName("client-" + numero);

        try (client;
             var entrada = new BufferedReader(new InputStreamReader(
                     client.getInputStream(), StandardCharsets.UTF_8));
             var sortida = new PrintWriter(new OutputStreamWriter(
                     client.getOutputStream(), StandardCharsets.UTF_8), true)) {

            client.setSoTimeout((int) INACTIVITAT.toMillis());

            sortida.println("BTCP/1 BENVINGUT BiblioTech");

            String linia;
            while ((linia = llegirLiniaAcotada(entrada)) != null) {

                String resposta = switch (linia.split(" ", 2)[0].toUpperCase()) {
                    case "CERCAR"  -> cercar(linia);
                    case "PRESTAR" -> prestar(linia);
                    case "ESTAT"   -> "OK ACTIVES=" + connexionsActives.get()
                                      + " TOTALS=" + connexionsTotals.get();
                    case "ADEU"    -> "OK FINS_AVIAT";
                    default        -> "ERROR ORDRE_DESCONEGUDA";
                };

                sortida.println(resposta);

                if (resposta.startsWith("OK FINS_AVIAT")) {
                    break;
                }
            }

        } catch (SocketTimeoutException e) {
            LOG.fine(() -> "Client " + numero + " expulsat per inactivitat");
        } catch (IOException e) {
            LOG.log(Level.FINE, e, () -> "Error amb el client " + numero);
        } finally {
            connexionsActives.decrementAndGet();
        }
    }

    private String llegirLiniaAcotada(BufferedReader entrada) throws IOException {
        StringBuilder sb = new StringBuilder();
        int c;
        while ((c = entrada.read()) != -1) {
            if (c == '\n') {
                return sb.toString().strip();
            }
            if (sb.length() >= MAX_LINIA) {
                throw new IOException("Linia massa llarga: possible atac");
            }
            sb.append((char) c);
        }
        return sb.isEmpty() ? null : sb.toString().strip();
    }

    private void monitoritzar() {
        while (actiu) {
            try {
                Thread.sleep(Duration.ofSeconds(10));
                LOG.info(() -> String.format("Connexions: %d actives, %d totals",
                        connexionsActives.get(), connexionsTotals.get()));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }
        }
    }

    private String cercar(String ordre)  { return "OK ..."; }
    private String prestar(String ordre) { return "OK ..."; }

    @Override
    public void close() throws IOException {
        actiu = false;
        if (servidor != null) {
            servidor.close();
        }
        atenedors.close();         // espera que acabin els clients en curs
        LOG.info("Servidor aturat");
    }

    public static void main(String[] args) throws IOException {
        try (var servidor = new ServidorCataleg()) {
            Runtime.getRuntime().addShutdownHook(new Thread(() -> {
                try { servidor.close(); } catch (IOException ignored) { }
            }));
            servidor.arrencar();
        }
    }
}

Què va canviar, exactament

Aspecte Amb pool (09-03) Amb fils virtuals
Concurrència màxima 50 clients (mida del pool) Desenes de milers
Configuració Mida de pool, cua, política de rebuig Cap
En saturar-se Encua i després rebutja Continua acceptant
Memòria per client ~1 MB ~1 KB
Codi d'atendre() Idèntic Idèntic
Traces de pila Llegibles Llegibles
Depuració pas a pas Funciona Funciona

I aquesta és l'observació central: el codi d'atendre() no ha canviat. Continua essent seqüencial, bloquejant i llegible. Amb CompletableFuture (08-07) caldria convertir-lo en una cadena de thenCompose sense variables locals entre passos, sense try-with-resources que abasti tota la conversa, i amb traces de pila que no diuen on era el flux.

Els fils virtuals tornen el codi senzill sense pagar en escalabilitat. Aquest és tot el seu valor.

Nota sobre el mòdul 8: tot el que vas aprendre continua essent necessari. Les condicions de cursa, volatile, synchronized, els interbloqueigs, les col·leccions concurrents i les variables atòmiques s'apliquen exactament igual als fils virtuals. Un fil virtual és un fil: dos fils virtuals tocant el mateix ArrayList el corrompen igual. El que canvia és quants en pots tenir, no com es comporten.

  1. Pinning, synchronized i el que no resolen

El pinning

El mecanisme dels fils virtuals exigeix que es puguin desmuntar del portador en bloquejar-se. Hi ha dues situacions on no poden:

  1. Dins d'un bloc o mètode synchronized.
  2. Durant una crida a codi natiu (JNI).

Quan això passa, el fil virtual queda clavat (pinned) al portador. Si a més es bloqueja, bloqueja el fil portador, que és un fil del sistema operatiu. Amb prou fils clavats, tots els portadors queden ocupats i el sistema es paralitza.

// PROBLEMA: bloqueig dins de synchronized
public class CacheProblematica {

    private final Map<String, Fitxa> cache = new HashMap<>();

    public synchronized Fitxa obtenir(String isbn) {    // synchronized
        return cache.computeIfAbsent(isbn, clau -> {
            return clientMetadades.consultar(clau);     // BLOQUEJA en xarxa -> PINNING
        });
    }
}
// SOLUCIO: ReentrantLock, que SI permet desmuntar
public class CacheCorrecta {

    private final Map<String, Fitxa> cache = new ConcurrentHashMap<>();
    private final ReentrantLock pany = new ReentrantLock();

    public Fitxa obtenir(String isbn) {
        Fitxa existent = cache.get(isbn);
        if (existent != null) {
            return existent;
        }

        pany.lock();                        // ReentrantLock (08-04): sense pinning
        try {
            return cache.computeIfAbsent(isbn, clientMetadades::consultar);
        } finally {
            pany.unlock();
        }
    }
}

La regla per a fils virtuals: en codi que es bloqueja, prefereix ReentrantLock a synchronized. Els panys de java.util.concurrent.locks estan dissenyats per desmuntar el fil virtual.

Es pot diagnosticar amb una opció de la JVM:

java -Djdk.tracePinnedThreads=full -jar bibliotech.jar
Thread[#31,ForkJoinPool-1-worker-1,5,CarrierThreads]
    com.nexussoftware.bibliotech.servei.CacheProblematica.obtenir(CacheProblematica.java:12)
    <== monitors:1

Nota important sobre versions: a Java 24 (JEP 491) es va eliminar el pinning per synchronized en la majoria dels casos. Si treballes amb Java 24 o posterior, el problema pràcticament desapareix. A Java 21 —la LTS de referència avui— continua essent una consideració real.

Què NO resolen els fils virtuals

Això és tan important com el que sí que resolen.

1. No acceleren el càlcul. Un fil virtual fa servir un nucli igual que un de plataforma. Si la teva tasca és multiplicar matrius, tenir un milió de fils virtuals sobre vuit nuclis no la fa més ràpida; només hi afegeix canvis de context.

// INUTIL: la tasca no es bloqueja mai
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 10_000; i++) {
        executor.submit(() -> calcularHashCostos(dades));    // pur CPU
    }
}

Per a càlcul, continua fent servir un pool de plataforma dimensionat als nuclis, o parallelStream() (10-04).

2. No eliminen els problemes de concurrència. Condicions de cursa, interbloqueigs, visibilitat de memòria: tot el mòdul 8 continua vigent. De fet, amb un milió de fils els problemes de contenció es manifesten abans.

3. No arreglen un coll d'ampolla extern. Deu mil fils virtuals colpejant una base de dades amb un pool de vint connexions no donen deu mil consultes simultànies: en donen vint, i 9.980 esperant. Els recursos externs continuen necessitant limitació explícita, amb Semaphore.

4. ThreadLocal es desaconsella. Amb milions de fils, cada ThreadLocal es multiplica per un milió. A més, el costum de reutilitzar ThreadLocal en un pool perd sentit quan cada tasca té el seu fil. Per a això arriba ScopedValue (apartat 21).

5. No substitueixen CompletableFuture en tot. Per a composició de fluxos asíncrons amb thenCombine i allOf (08-07), o per a reactivitat amb contrapressió, CompletableFuture i les biblioteques reactives continuen tenint el seu lloc. Els fils virtuals brillen en el model "una tasca, un fil, codi seqüencial".

Escenari Eina
Servidor amb moltes connexions bloquejants Fils virtuals
Moltes crides HTTP/BD independents Fils virtuals + Semaphore
Càlcul intensiu Pool de plataforma, parallelStream
Composició asíncrona amb dependències CompletableFuture (08-07)
Fluxos amb contrapressió Biblioteques reactives

  1. El que ve: concurrència estructurada i ScopedValue

Dues característiques que a Java 21 estan en vista prèvia i que completen el model de Loom. No les facis servir en producció encara, però convé conèixer-les.

Concurrència estructurada

El problema: quan llances diverses tasques i n'esperes els resultats, gestionar les fallades i les cancel·lacions a mà és propens a errors. Si una falla, qui cancel·la les altres? Si el fil pare s'interromp, qui neteja?

// Amb StructuredTaskScope: l ambit CONTE les subtasques
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {

    Subtask<Fitxa> fitxa = scope.fork(() -> clientMetadades.consultar(isbn));
    Subtask<byte[]> portada = scope.fork(() -> clientPortades.descarregar(isbn));
    Subtask<List<String>> ressenyes = scope.fork(() -> clientRessenyes.cercar(isbn));

    scope.join();                  // espera les tres
    scope.throwIfFailed();         // si alguna ha fallat, llanca i CANCELLA les altres

    return new FitxaCompleta(fitxa.get(), portada.get(), ressenyes.get());
}   // close() garanteix que cap subtasca no continua viva

Les garanties: les subtasques no sobreviuen a l'àmbit, una fallada cancel·la les germanes, la interrupció es propaga cap avall, i les traces de pila reflecteixen la relació pare-fill. És a la concurrència el que el try-with-resources és als recursos.

ScopedValue

El substitut de ThreadLocal per al món dels fils virtuals:

public class ContextBiblioTech {

    // Immutable i amb ambit acotat, a diferencia de ThreadLocal
    public static final ScopedValue<String> EMPLEAT = ScopedValue.newInstance();

    public void atendrePeticio(String empleat, Runnable tasca) {
        ScopedValue.where(EMPLEAT, empleat).run(tasca);
        // Fora d aqui, EMPLEAT no esta lligat
    }

    public void enAlgunLlocProfund() {
        if (EMPLEAT.isBound()) {
            LOG.info("Operacio de " + EMPLEAT.get());
        }
    }
}
ThreadLocal ScopedValue
Mutabilitat Mutable amb set() Immutable
Àmbit Vida del fil Bloc delimitat
Neteja Manual (remove()), origen de fuites Automàtica
Herència a subtasques InheritableThreadLocal, amb còpia Automàtica i sense copiar
Amb milions de fils Problemàtic Dissenyat per a això

  1. Eines: jshell, jpackage i companyia

Una nota ràpida sobre les eines noves del JDK.

jshell (Java 9) — el REPL. Permet provar codi sense crear una classe ni un main:

jshell
jshell> var cataleg = new java.util.ArrayList<String>()
cataleg ==> []

jshell> cataleg.add("Java Eficac")
$2 ==> true

jshell> cataleg.stream().map(String::toUpperCase).toList()
$3 ==> [JAVA EFICAC]

jshell> java.time.LocalDate.now().plusDays(21)
$4 ==> 2026-08-26

jshell> /exit

Insubstituïble per comprovar el comportament exacte d'una API sense muntar un projecte. I molt útil per verificar al moment els dubtes d'aquesta lliçó.

Executar un .java sense compilar (Java 11):

java Hola.java

Compila en memòria i executa. Converteix Java en una cosa utilitzable per a guions.

jpackage (Java 14) — instal·ladors natius:

jpackage --name BiblioTech \
         --input dist/ \
         --main-jar bibliotech.jar \
         --main-class com.nexussoftware.bibliotech.BiblioTechApp \
         --runtime-image runtime-bibliotech \
         --type deb \
         --app-version 3.2

Produeix un .deb, .rpm, .msi, .exe, .dmg o .pkg amb l'aplicació i el seu runtime a dins. L'usuari final no necessita Java instal·lat.

jwebserver (Java 18) — servidor de fitxers estàtics per a proves:

jwebserver -p 8000 -d /home/joan/bibliotech/informes

jdeps — analitza dependències (el vas fer servir amb jlink), i detecta usos d'API interna:

jdeps --jdk-internals bibliotech.jar

jcmd, jfr — diagnòstic i gravació de vol. Tot això és 10-07.

  1. Com mantenir-se al dia i com decidir la migració

Les fonts

Font Què aporta
JEP (openjdk.org/jeps) La proposta tècnica de cada característica, amb motivació i alternatives descartades. La font primària
Notes de versió del JDK Canvis incompatibles, obsolescències, eliminacions
Inside.java Blog oficial amb articles i vídeos de l'equip
JDK Mission Control / JFR Per al rendiment (10-07)
Javadoc de la versió Amb @since a cada mètode

Un JEP és la unitat de canvi de Java. Els que convé conèixer d'aquesta lliçó: JEP 261 (mòduls), JEP 286 (var), JEP 361 (switch d'expressió), JEP 378 (blocs de text), JEP 395 (records), JEP 409 (classes segellades), JEP 440/441 (patrons de registre i switch), JEP 444 (fils virtuals).

Decidir a quina versió migrar

Regla base: fes servir la LTS més recent que les teves dependències admetin. Avui això és Java 21, amb Java 25 disponible.

El salt per versió d'origen:

Des de Fins a Dificultat Què esperar
8 11 Alta Mòduls, API interna bloquejada, eliminació de CORBA i JavaFX del JDK, javax.xml.bind fora
8 17 Alta L'anterior més el Security Manager obsolet i encapsulament fort
11 17 Mitjana L'encapsulament fort per defecte és el principal escull
17 21 Baixa Molt compatible; gairebé tot funciona sense canvis
21 25 Baixa Esperada com a molt compatible

El salt de Java 8 a 11 és el difícil, i per això tants projectes continuen al 8. Els problemes típics:

  1. javax.xml.bind (JAXB) i java.xml.ws es van eliminar del JDK. Cal afegir-los com a dependència.
  2. Biblioteques que fan servir API interna (sun.misc.Unsafe, sun.reflect) fallen amb InaccessibleObjectException.
  3. Eines antigues (versions velles de Maven, Gradle, Lombok, ASM) no entenen el bytecode nou.
  4. -XX:+UseParallelOldGC i altres opcions de JVM van desaparèixer.

Un pla de migració raonable:

# 1. Detectar usos d API interna ABANS de tocar res
jdeps --jdk-internals --multi-release 21 bibliotech.jar

# 2. Compilar amb la versio nova pero desti antic
javac --release 11 ...

# 3. Executar amb la JVM nova i el codi antic, veient els avisos
java --illegal-access=warn -jar bibliotech.jar     # fins a Java 16

# 4. Actualitzar dependencies UNA a UNA, no totes de cop

# 5. Pujar el desti de compilacio
javac --release 21 ...

# 6. Modernitzar el codi (opcional i gradual):
#    var, switch d expressio, records, blocs de text, streams

I una recomanació de mètode: separa la migració (que compili i funcioni a la versió nova) de la modernització (fer servir les característiques noves). Fer les dues alhora multiplica el risc i fa impossible saber què va trencar què.

Errors Comuns i Consells

1. Fer servir característiques de vista prèvia en producció. Requereixen --enable-preview en compilació i execució, el bytecode només funciona en aquella versió exacta, i el disseny pot canviar.

2. Modularitzar una aplicació empresarial "perquè és el que és modern". JPMS aporta poc a un JAR executable i costa molta fricció amb les dependències. Modularitza biblioteques, no aplicacions.

3. Confondre exports amb opens. Exportar permet l'ús normal; només opens permet setAccessible sobre privats. Un framework de persistència necessita opens.

4. Abusar de --add-opens. Cada ús és deute tècnic: depens de detalls interns que poden canviar. Fes-lo servir com a pedaç temporal mentre actualitzes la biblioteca culpable.

5. Abusar de var. var resultat = servei.processar(dades); no diu res. Fes-lo servir quan el tipus sigui evident a la mateixa línia.

6. var llista = new ArrayList<>(); infereix ArrayList<Object>, gairebé mai el que vols. Amb var, el tipus genèric va a la dreta.

7. Posar default en un switch sobre un tipus segellat. Destrueix l'exhaustivitat: si demà s'hi afegeix un tipus, el default l'absorbeix en silenci en lloc de trencar la compilació. L'absència de default és la característica, no un descuit.

8. Oblidar que les subclasses d'una sealed s'han de declarar. final, sealed o non-sealed: no hi ha quarta opció, i el missatge d'error és clar però s'oblida.

9. Agrupar fils virtuals en un pool. Contradiu el seu propòsit. Un per tasca, i Semaphore per limitar l'accés a recursos externs.

10. Bloquejar dins de synchronized amb fils virtuals. Provoca pinning i bloqueja un fil portador. Fes servir ReentrantLock a Java 21 (a Java 24+ el problema està en gran part resolt).

11. Esperar que els fils virtuals accelerin el càlcul. No ho fan: només eviten malbaratar fils del sistema operatiu mentre s'espera E/S.

12. Alinear malament el """ de tancament d'un bloc de text. La seva posició participa en el càlcul del sagnat comú. Alinea'l amb el contingut tret que vulguis conservar marge.

13. Oblidar que List.of i Map.of no admeten null i que l'ordre de Set.of canvia entre execucions expressament.

Consell 1: aprèn les característiques pel problema que resolen. sealed existeix perquè el compilador verifiqui l'exhaustivitat. Els fils virtuals existeixen perquè el codi bloquejant escali. Memoritzar sintaxi sense el perquè produeix un ús incorrecte.

Consell 2: adopta gradualment. Comença pel que no té risc: var on ajudi, switch d'expressió, blocs de text per a SQL i JSON, record per a DTO. Deixa sealed i pattern matching per quan refactoritzis una jerarquia.

Consell 3: fes servir jshell per verificar. Cinc segons al REPL responen a "què retorna exactament això?" millor que quinze minuts llegint documentació.

Consell 4: separa migrar de modernitzar. Primer que compili i passi les proves a la versió nova; després, modernitza mòdul a mòdul.

Consell 5: llegeix el JEP del que facis servir. Els JEP expliquen quines alternatives es van descartar i per què, i això ensenya més sobre disseny de llenguatges que qualsevol tutorial.

Exercicis

Exercici 1: modelar el catàleg amb tipus segellats

Redissenya el domini de BiblioTech aprofitant tot el d'aquesta lliçó:

  1. sealed interface ElementCataleg permits Material, Colleccio, on Colleccio agrupa diversos elements (un element pot contenir-ne d'altres).
  2. sealed abstract class Material permits Llibre, Revista, Dvd, AudioLlibre, amb Llibre i Dvd com a final, Revista segellada amb dos subtipus, i AudioLlibre com a non-sealed.
  3. Un sealed interface ResultatCerca amb records Trobat, NoTrobat i Ambigu(List<Material> candidats).
  4. Un servei amb switch exhaustius i sense default que calculi: termini de préstec, valor de reposició, etiqueta de prestatge i una fitxa completa amb blocs de text.
  5. Un mètode int comptarMaterials(ElementCataleg) recursiu que descendeixi per les col·leccions imbricades fent servir patrons de registre.
  6. Demostra que afegir un cinquè material trenca la compilació a tots els llocs correctes.

Exercici 2: EnriquidorCataleg amb fils virtuals

A 09-06 vas escriure EnriquidorCataleg amb CompletableFuture, allOf i un Semaphore. Reescriu-lo amb fils virtuals i compara.

  1. EnriquidorVirtual que consulti metadades i descarregui portades de N ISBN fent servir Executors.newVirtualThreadPerTaskExecutor().
  2. Codi seqüencial i bloquejant dins de cada tasca, sense thenCompose ni thenCombine.
  3. Limita a 50 peticions simultànies al servei extern amb Semaphore, no amb la mida del pool.
  4. Gestiona les fallades individuals sense que tombin el lot, amb un record ResultatEnriquiment segellat.
  5. Mesura i compara: pool de 200 de plataforma, fils virtuals, i CompletableFuture, amb 100, 1.000 i 10.000 ISBN.
  6. Demostra el pinning: una versió amb synchronized al voltant de la crida bloquejant enfront d'una altra amb ReentrantLock, executades amb -Djdk.tracePinnedThreads=full.

Exercici 3: informe de modernització

Escriu una eina AnalitzadorDeModernitzacio que examini fitxers .java i detecti oportunitats de modernització:

  1. Cadenes concatenades amb + en diverses línies → bloc de text.
  2. switch clàssic amb breakswitch d'expressió.
  3. instanceof seguit de conversió al mateix tipus → instanceof amb patró.
  4. Classes amb només camps final, constructor, getters, equals, hashCode i toStringrecord.
  5. new SimpleDateFormat / java.util.Datejava.time (10-05).
  6. Executors.newFixedThreadPool amb tasques bloquejants → fils virtuals.
  7. Bucles amb acumulador sobre col·leccions → streams (10-04).

Per a cada troballa, indica fitxer, línia, la versió de Java que ho permet i un suggeriment. Genera un informe amb groupingBy i blocs de text. Aplica'l a les classes antigues de BiblioTech.

Solucions

Solució 1

package com.nexussoftware.bibliotech.domini;

import java.util.List;

/**
 * Arrel SEGELLADA del cataleg. Un element es, o un material concret,
 * o una colleccio que agrupa altres elements.
 */
public sealed interface ElementCataleg permits Material, Colleccio {

    String getIsbn();
    String getTitol();
}
package com.nexussoftware.bibliotech.domini;

import java.util.List;
import java.util.Objects;

/**
 * Agrupacio d elements. Pot contenir colleccions imbricades:
 * es una estructura recursiva (el patro Composite, formalitzat a 12-02).
 */
public record Colleccio(String isbn, String titol, List<ElementCataleg> elements)
        implements ElementCataleg {

    public Colleccio {
        Objects.requireNonNull(isbn, "isbn");
        Objects.requireNonNull(titol, "titol");
        elements = List.copyOf(elements);        // copia defensiva immutable
    }

    @Override public String getIsbn()  { return isbn; }
    @Override public String getTitol() { return titol; }
}
package com.nexussoftware.bibliotech.domini;

/**
 * Jerarquia de materials, TANCADA a quatre tipus.
 * Cada subclasse declara explicitament si es pot estendre.
 */
public sealed abstract class Material
        implements ElementCataleg
        permits Llibre, Revista, Dvd, AudioLlibre {

    private final String isbn;
    private final String titol;
    private final double valoracio;

    protected Material(String isbn, String titol, double valoracio) {
        this.isbn = isbn;
        this.titol = titol;
        this.valoracio = valoracio;
    }

    @Override public String getIsbn()  { return isbn; }
    @Override public String getTitol() { return titol; }
    public double getValoracio()       { return valoracio; }
}
/** final: tancada del tot. */
public final class Llibre extends Material {
    private final String autor;
    private final int pagines;

    public Llibre(String isbn, String titol, String autor, int pagines, double valoracio) {
        super(isbn, titol, valoracio);
        this.autor = autor;
        this.pagines = pagines;
    }
    public String getAutor() { return autor; }
    public int getPagines()  { return pagines; }
}

/** sealed: la seva propia jerarquia tancada de dues periodicitats. */
public sealed class Revista extends Material
        permits RevistaMensual, RevistaTrimestral {

    private final int numero;

    protected Revista(String isbn, String titol, int numero, double valoracio) {
        super(isbn, titol, valoracio);
        this.numero = numero;
    }
    public int getNumero() { return numero; }
}

public final class RevistaMensual extends Revista {
    private final java.time.Month mes;
    public RevistaMensual(String isbn, String titol, int numero,
                          java.time.Month mes, double valoracio) {
        super(isbn, titol, numero, valoracio);
        this.mes = mes;
    }
    public java.time.Month getMes() { return mes; }
}

public final class RevistaTrimestral extends Revista {
    private final int trimestre;
    public RevistaTrimestral(String isbn, String titol, int numero,
                             int trimestre, double valoracio) {
        super(isbn, titol, numero, valoracio);
        this.trimestre = trimestre;
    }
    public int getTrimestre() { return trimestre; }
}

/** final. */
public final class Dvd extends Material {
    private final String director;
    private final int minuts;
    public Dvd(String isbn, String titol, String director, int minuts, double valoracio) {
        super(isbn, titol, valoracio);
        this.director = director;
        this.minuts = minuts;
    }
    public String getDirector() { return director; }
    public int getMinuts()      { return minuts; }
}

/** non-sealed: DELIBERADAMENT oberta a extensio per tercers. */
public non-sealed class AudioLlibre extends Material {
    private final String narrador;
    private final int minuts;
    public AudioLlibre(String isbn, String titol, String narrador, int minuts, double valoracio) {
        super(isbn, titol, valoracio);
        this.narrador = narrador;
        this.minuts = minuts;
    }
    public String getNarrador() { return narrador; }
    public int getMinuts()      { return minuts; }
}
package com.nexussoftware.bibliotech.domini;

import java.util.List;

/** Tipus suma per al resultat d una cerca. */
public sealed interface ResultatCerca {

    record Trobat(Material material) implements ResultatCerca { }

    record NoTrobat(String consulta, List<String> suggeriments) implements ResultatCerca { }

    record Ambigu(String consulta, List<Material> candidats) implements ResultatCerca { }
}

El servei:

package com.nexussoftware.bibliotech.servei;

import com.nexussoftware.bibliotech.domini.*;

/**
 * Tots els switch son EXHAUSTIUS i sense default.
 * Si s afegeix un cinque material, cap no compila.
 */
public class ServeiCataleg {

    public int diesDeTermini(Material material) {
        return switch (material) {
            case Llibre l when l.getPagines() > 500 -> 30;
            case Llibre l when l.getPagines() > 200 -> 21;
            case Llibre l                           -> 14;
            case RevistaMensual r                   -> 7;
            case RevistaTrimestral r                -> 14;
            case Dvd d when d.getMinuts() > 180     -> 5;
            case Dvd d                              -> 3;
            case AudioLlibre a                      -> 21;
        };
        // Revista es sealed: el compilador accepta cobrir els seus dos subtipus
        // en lloc de Revista, i comprova que hi son tots dos.
    }

    public double valorDeReposicio(Material material) {
        return switch (material) {
            case Llibre l            -> Math.max(15.0, l.getPagines() * 0.08);
            case RevistaMensual r    -> 8.0;
            case RevistaTrimestral r -> 14.0;
            case Dvd d               -> Math.max(10.0, d.getMinuts() * 0.15);
            case AudioLlibre a       -> Math.max(20.0, a.getMinuts() * 0.10);
        };
    }

    public String etiquetaDePrestatge(ElementCataleg element) {
        String prefix = switch (element) {
            case Llibre l            -> "LLI";
            case RevistaMensual r    -> "REM";
            case RevistaTrimestral r -> "RET";
            case Dvd d               -> "DVD";
            case AudioLlibre a       -> "AUD";
            case Colleccio c         -> "COL";
        };
        return prefix + "-" + element.getIsbn().substring(4, 8);
    }

    /** Fitxa amb blocs de text i formatted (Java 15). */
    public String fitxa(ElementCataleg element) {
        return switch (element) {

            case Llibre l -> """
                    ┌─ LLIBRE ────────────────────────────
                    │ %s
                    │ Autor:      %s
                    │ Pagines:    %d
                    │ Prestatge:  %s
                    │ Termini:    %d dies
                    │ Reposicio:  %.2f €
                    └─────────────────────────────────────"""
                    .formatted(l.getTitol(), l.getAutor(), l.getPagines(),
                               etiquetaDePrestatge(l), diesDeTermini(l), valorDeReposicio(l));

            case RevistaMensual r -> """
                    ┌─ REVISTA MENSUAL ───────────────────
                    │ %s num. %d (%s)
                    │ Prestatge:  %s
                    │ Termini:    %d dies
                    └─────────────────────────────────────"""
                    .formatted(r.getTitol(), r.getNumero(), r.getMes(),
                               etiquetaDePrestatge(r), diesDeTermini(r));

            case RevistaTrimestral r -> """
                    ┌─ REVISTA TRIMESTRAL ────────────────
                    │ %s num. %d (T%d)
                    │ Prestatge:  %s
                    │ Termini:    %d dies
                    └─────────────────────────────────────"""
                    .formatted(r.getTitol(), r.getNumero(), r.getTrimestre(),
                               etiquetaDePrestatge(r), diesDeTermini(r));

            case Dvd d -> """
                    ┌─ DVD ───────────────────────────────
                    │ %s
                    │ Director:   %s
                    │ Durada:     %d min
                    │ Termini:    %d dies
                    └─────────────────────────────────────"""
                    .formatted(d.getTitol(), d.getDirector(), d.getMinuts(), diesDeTermini(d));

            case AudioLlibre a -> """
                    ┌─ AUDIOLLIBRE ───────────────────────
                    │ %s
                    │ Narrador:   %s
                    │ Durada:     %d min
                    │ Termini:    %d dies
                    └─────────────────────────────────────"""
                    .formatted(a.getTitol(), a.getNarrador(), a.getMinuts(), diesDeTermini(a));

            case Colleccio(var isbn, var titol, var elements) -> """
                    ┌─ COLLECCIO ─────────────────────────
                    │ %s
                    │ Elements:   %d directes, %d en total
                    │ Prestatge:  %s
                    └─────────────────────────────────────"""
                    .formatted(titol, elements.size(),
                               comptarMaterials(element), etiquetaDePrestatge(element));
        };
    }

    /**
     * RECURSIU amb patro de registre: la colleccio es descompon
     * i es descendeix pels seus elements.
     */
    public int comptarMaterials(ElementCataleg element) {
        return switch (element) {
            case Material m -> 1;
            case Colleccio(var isbn, var titol, var elements) ->
                    elements.stream()
                            .mapToInt(this::comptarMaterials)     // recursio
                            .sum();
        };
    }

    /** Profunditat maxima d imbricacio. */
    public int profunditat(ElementCataleg element) {
        return switch (element) {
            case Material m -> 0;
            case Colleccio(var isbn, var titol, var elements) ->
                    1 + elements.stream()
                                .mapToInt(this::profunditat)
                                .max().orElse(0);
        };
    }

    /** El resultat de cerca, tambe amb switch exhaustiu. */
    public String descriureCerca(ResultatCerca resultat) {
        return switch (resultat) {

            case ResultatCerca.Trobat(Material m) ->
                    "Trobat: " + m.getTitol();

            case ResultatCerca.NoTrobat(var consulta, var suggeriments)
                    when suggeriments.isEmpty() ->
                    "No hi ha resultats per a \"" + consulta + "\"";

            case ResultatCerca.NoTrobat(var consulta, var suggeriments) ->
                    "No hi ha resultats per a \"" + consulta + "\". Potser: "
                    + String.join(", ", suggeriments) + "?";

            case ResultatCerca.Ambigu(var consulta, var candidats) ->
                    candidats.size() + " coincidencies per a \"" + consulta + "\": "
                    + candidats.stream().map(Material::getTitol)
                               .collect(java.util.stream.Collectors.joining(", "));
        };
    }
}

Prova:

package com.nexussoftware.bibliotech;

import com.nexussoftware.bibliotech.domini.*;
import com.nexussoftware.bibliotech.servei.ServeiCataleg;

import java.time.Month;
import java.util.List;

public class ProvaCatalegSegellat {

    public static void main(String[] args) {

        ServeiCataleg servei = new ServeiCataleg();

        Llibre eficac = new Llibre("978-0000000001", "Java Eficac", "Joshua Bloch", 412, 4.85);
        Llibre patrons = new Llibre("978-0000000002", "Patrons de Disseny", "GoF", 395, 4.60);
        Llibre refactor = new Llibre("978-0000000003", "Refactoritzacio", "M. Fowler", 448, 4.72);
        RevistaMensual javaMag = new RevistaMensual("978-0000000010", "Java Magazine",
                                                    142, Month.AUGUST, 3.90);
        Dvd cursSpring = new Dvd("978-0000000020", "Curs de Spring", "N. Vidal", 240, 4.10);
        AudioLlibre audio = new AudioLlibre("978-0000000030", "Codi Net (audio)",
                                            "D. Alonso", 480, 4.30);

        // Colleccio IMBRICADA
        Colleccio essencials = new Colleccio("978-1000000001", "Essencials de Java",
                List.of(eficac, patrons, refactor));

        Colleccio multimedia = new Colleccio("978-1000000002", "Formacio multimedia",
                List.of(cursSpring, audio));

        Colleccio completa = new Colleccio("978-1000000000", "Biblioteca tecnica Nexus",
                List.of(essencials, multimedia, javaMag));

        System.out.println(servei.fitxa(eficac));
        System.out.println(servei.fitxa(javaMag));
        System.out.println(servei.fitxa(audio));
        System.out.println(servei.fitxa(completa));

        System.out.println();
        System.out.println("Materials totals: " + servei.comptarMaterials(completa));
        System.out.println("Profunditat:      " + servei.profunditat(completa));

        System.out.println();
        System.out.println(servei.descriureCerca(
                new ResultatCerca.Trobat(eficac)));
        System.out.println(servei.descriureCerca(
                new ResultatCerca.NoTrobat("kotlin", List.of())));
        System.out.println(servei.descriureCerca(
                new ResultatCerca.NoTrobat("javaa", List.of("Java Eficac"))));
        System.out.println(servei.descriureCerca(
                new ResultatCerca.Ambigu("java", List.of(eficac, audio))));
    }
}
┌─ LLIBRE ────────────────────────────
│ Java Eficac
│ Autor:      Joshua Bloch
│ Pagines:    412
│ Prestatge:  LLI-0000
│ Termini:    21 dies
│ Reposicio:  32,96 €
└─────────────────────────────────────
┌─ REVISTA MENSUAL ───────────────────
│ Java Magazine num. 142 (AUGUST)
│ Prestatge:  REM-0000
│ Termini:    7 dies
└─────────────────────────────────────
┌─ AUDIOLLIBRE ───────────────────────
│ Codi Net (audio)
│ Narrador:   D. Alonso
│ Durada:     480 min
│ Termini:    21 dies
└─────────────────────────────────────
┌─ COLLECCIO ─────────────────────────
│ Biblioteca tecnica Nexus
│ Elements:   3 directes, 6 en total
│ Prestatge:  COL-1000
└─────────────────────────────────────

Materials totals: 6
Profunditat:      2

Trobat: Java Eficac
No hi ha resultats per a "kotlin"
No hi ha resultats per a "javaa". Potser: Java Eficac?
2 coincidencies per a "java": Java Eficac, Codi Net (audio)

I la demostració de l'exhaustivitat. En afegir Mapa als permits:

public sealed abstract class Material
        implements ElementCataleg
        permits Llibre, Revista, Dvd, AudioLlibre, Mapa { }

public final class Mapa extends Material { /* ... */ }
ServeiCataleg.java:14: error: the switch expression does not cover all possible input values
        return switch (material) {
               ^
ServeiCataleg.java:27: error: the switch expression does not cover all possible input values
        return switch (material) {
               ^
ServeiCataleg.java:37: error: the switch expression does not cover all possible input values
        String prefix = switch (element) {
                        ^
ServeiCataleg.java:49: error: the switch expression does not cover all possible input values
        return switch (element) {
               ^
4 errors

Comentaris. Quatre observacions.

Quatre errors de compilació són quatre llocs que calia actualitzar. Amb la versió antiga d'if-else amb default, el Mapa hauria caigut al default de cada mètode: termini zero, reposició zero, etiqueta "X", i ningú no se n'hauria assabentat fins que un usuari reclamés. Aquest és el valor sencer de les classes segellades.

El compilador entén les jerarquies imbricades. Revista és sealed permits RevistaMensual, RevistaTrimestral, i el switch cobreix els dos subtipus en lloc de Revista. El compilador verifica que hi són tots dos: treure'n un trenca la compilació.

AudioLlibre és non-sealed expressament, i demostra que l'obertura és una decisió per branca. Algú pot escriure class Podcast extends AudioLlibre, i el case AudioLlibre a el cobrirà.

El comptarMaterials recursiu amb patró de registre és el més expressiu de l'exercici. Dos casos, quatre línies, i recorre una estructura arbitràriament imbricada. És el patró Composite (12-02) expressat com a tipus algebraic.

Solució 2

package com.nexussoftware.bibliotech.xarxa;

import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Enriquidor de cataleg amb FILS VIRTUALS.
 * Substitueix la versio de 09-06 basada en CompletableFuture.
 */
public class EnriquidorVirtual implements AutoCloseable {

    private static final Logger LOG = Logger.getLogger(EnriquidorVirtual.class.getName());

    /** Tipus suma per al resultat de cada ISBN. */
    public sealed interface ResultatEnriquiment {
        String isbn();

        record Complet(String isbn, String metadades, int bytesPortada, long ms)
                implements ResultatEnriquiment { }

        record Parcial(String isbn, String metadades, String motiuSensePortada, long ms)
                implements ResultatEnriquiment { }

        record Fallit(String isbn, String motiu, long ms)
                implements ResultatEnriquiment { }
    }

    /**
     * Semafor, NO mida de pool: la restriccio es del servei
     * extern, no dels nostres fils.
     */
    private final Semaphore limitExtern;

    private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

    private final ClientMetadades metadades;
    private final ClientPortades portades;

    public EnriquidorVirtual(ClientMetadades metadades, ClientPortades portades,
                             int peticionsSimultanies) {
        this.metadades = metadades;
        this.portades = portades;
        this.limitExtern = new Semaphore(peticionsSimultanies);
    }

    public List<ResultatEnriquiment> enriquir(List<String> isbns) {

        List<Future<ResultatEnriquiment>> futurs = new ArrayList<>(isbns.size());

        // Es llancen TOTES: un fil virtual per ISBN, siguin 100 o 100.000
        for (String isbn : isbns) {
            futurs.add(executor.submit(() -> processarUn(isbn)));
        }

        List<ResultatEnriquiment> resultats = new ArrayList<>(isbns.size());
        for (int i = 0; i < futurs.size(); i++) {
            try {
                resultats.add(futurs.get(i).get());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            } catch (ExecutionException e) {
                resultats.add(new ResultatEnriquiment.Fallit(
                        isbns.get(i), "excepcio no controlada: " + e.getCause(), 0));
            }
        }
        return resultats;
    }

    /**
     * CODI SEQUENCIAL I BLOQUEJANT. Ni un thenCompose, ni un
     * thenCombine, ni una retrucada. Es llegeix de dalt a baix.
     */
    private ResultatEnriquiment processarUn(String isbn) {

        long inici = System.nanoTime();
        Thread.currentThread().setName("enriquir-" + isbn);

        try {
            // 1. Metadades: bloqueja, i no passa res
            String dades;
            limitExtern.acquire();
            try {
                dades = metadades.consultar(isbn);       // crida BLOQUEJANT
            } finally {
                limitExtern.release();
            }

            // 2. Portada: pot fallar sense invalidar el resultat
            int bytes;
            try {
                limitExtern.acquire();
                try {
                    bytes = portades.descarregar(isbn).length;
                } finally {
                    limitExtern.release();
                }
            } catch (Exception e) {
                return new ResultatEnriquiment.Parcial(
                        isbn, dades, e.getClass().getSimpleName(), ms(inici));
            }

            return new ResultatEnriquiment.Complet(isbn, dades, bytes, ms(inici));

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return new ResultatEnriquiment.Fallit(isbn, "interromput", ms(inici));

        } catch (Exception e) {
            LOG.log(Level.FINE, e, () -> "Fallada en enriquir " + isbn);
            return new ResultatEnriquiment.Fallit(
                    isbn, e.getClass().getSimpleName() + ": " + e.getMessage(), ms(inici));
        }
    }

    private long ms(long iniciNanos) {
        return (System.nanoTime() - iniciNanos) / 1_000_000;
    }

    /** Informe amb switch exhaustiu sobre el tipus segellat. */
    public String informar(List<ResultatEnriquiment> resultats) {

        Map<String, Long> perTipus = resultats.stream()
                .collect(java.util.stream.Collectors.groupingBy(
                        r -> switch (r) {
                            case ResultatEnriquiment.Complet c -> "complets";
                            case ResultatEnriquiment.Parcial p -> "parcials";
                            case ResultatEnriquiment.Fallit f  -> "fallits";
                        },
                        java.util.TreeMap::new,
                        java.util.stream.Collectors.counting()));

        double mitjanaMs = resultats.stream()
                .mapToLong(r -> switch (r) {
                    case ResultatEnriquiment.Complet c -> c.ms();
                    case ResultatEnriquiment.Parcial p -> p.ms();
                    case ResultatEnriquiment.Fallit f  -> f.ms();
                })
                .average().orElse(0);

        return """
                Enriquiment de %d ISBN
                  %s
                  Durada mitjana per ISBN: %.1f ms"""
                .formatted(resultats.size(), perTipus, mitjanaMs);
    }

    @Override
    public void close() {
        executor.close();        // ExecutorService es AutoCloseable des de Java 19
    }
}

El banc de proves comparatiu:

package com.nexussoftware.bibliotech;

import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.IntStream;

public class ComparativaEnriquiment {

    /** Simula una crida de xarxa: 50 ms d espera. */
    private static String consultarSimulat(String isbn) throws InterruptedException {
        Thread.sleep(Duration.ofMillis(50));
        return "{\"isbn\":\"" + isbn + "\"}";
    }

    public static void main(String[] args) throws Exception {

        System.out.printf("%-10s %14s %14s %14s%n",
                "ISBN", "PLATAF. (200)", "VIRTUALS", "COMPL.FUTURE");
        System.out.println("-".repeat(56));

        for (int n : new int[] { 100, 1_000, 10_000 }) {
            List<String> isbns = generar(n);

            long plataforma = mesurar(() -> ambPoolDePlataforma(isbns));
            long virtuals   = mesurar(() -> ambFilsVirtuals(isbns));
            long futurs     = mesurar(() -> ambCompletableFuture(isbns));

            System.out.printf("%-10d %11d ms %11d ms %11d ms%n",
                    n, plataforma, virtuals, futurs);
        }

        System.out.println();
        demostrarPinning();
    }

    private static List<String> generar(int n) {
        return IntStream.rangeClosed(1, n)
                .mapToObj(i -> String.format("978-%010d", i))
                .toList();
    }

    private static long mesurar(Runnable tasca) {
        long inici = System.currentTimeMillis();
        tasca.run();
        return System.currentTimeMillis() - inici;
    }

    private static void ambPoolDePlataforma(List<String> isbns) {
        try (var executor = Executors.newFixedThreadPool(200)) {
            List<Future<String>> futurs = isbns.stream()
                    .map(isbn -> executor.submit(() -> consultarSimulat(isbn)))
                    .toList();
            futurs.forEach(ComparativaEnriquiment::obtenir);
        }
    }

    private static void ambFilsVirtuals(List<String> isbns) {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            List<Future<String>> futurs = isbns.stream()
                    .map(isbn -> executor.submit(() -> consultarSimulat(isbn)))
                    .toList();
            futurs.forEach(ComparativaEnriquiment::obtenir);
        }
    }

    private static void ambCompletableFuture(List<String> isbns) {
        try (var executor = Executors.newFixedThreadPool(200)) {
            List<CompletableFuture<String>> futurs = isbns.stream()
                    .map(isbn -> CompletableFuture.supplyAsync(() -> {
                        try {
                            return consultarSimulat(isbn);
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                            return null;
                        }
                    }, executor))
                    .toList();
            CompletableFuture.allOf(futurs.toArray(CompletableFuture[]::new)).join();
        }
    }

    private static <T> T obtenir(Future<T> f) {
        try {
            return f.get();
        } catch (Exception e) {
            return null;
        }
    }

    // ------------------------------------------------------------------
    // Demostracio del PINNING
    // ------------------------------------------------------------------

    static class AmbSynchronized {
        private final Map<String, String> cache = new HashMap<>();

        /** synchronized + bloqueig -> PINNING del fil portador. */
        public synchronized String obtenir(String isbn) throws InterruptedException {
            String v = cache.get(isbn);
            if (v == null) {
                v = consultarSimulat(isbn);
                cache.put(isbn, v);
            }
            return v;
        }
    }

    static class AmbReentrantLock {
        private final Map<String, String> cache = new ConcurrentHashMap<>();
        private final ReentrantLock pany = new ReentrantLock();

        /** ReentrantLock permet desmuntar el fil virtual. */
        public String obtenir(String isbn) throws InterruptedException {
            String v = cache.get(isbn);
            if (v != null) {
                return v;
            }
            pany.lock();
            try {
                v = cache.get(isbn);
                if (v == null) {
                    v = consultarSimulat(isbn);
                    cache.put(isbn, v);
                }
                return v;
            } finally {
                pany.unlock();
            }
        }
    }

    private static void demostrarPinning() {
        System.out.println("--- Pinning (executar amb -Djdk.tracePinnedThreads=full) ---");
        List<String> isbns = generar(500);

        AmbSynchronized ambSync = new AmbSynchronized();
        long t1 = mesurar(() -> {
            try (var e = Executors.newVirtualThreadPerTaskExecutor()) {
                isbns.forEach(isbn -> e.submit(() -> ambSync.obtenir(isbn)));
            }
        });

        AmbReentrantLock ambPany = new AmbReentrantLock();
        long t2 = mesurar(() -> {
            try (var e = Executors.newVirtualThreadPerTaskExecutor()) {
                isbns.forEach(isbn -> e.submit(() -> ambPany.obtenir(isbn)));
            }
        });

        System.out.printf("  synchronized:  %5d ms%n", t1);
        System.out.printf("  ReentrantLock: %5d ms%n", t2);
    }
}
ISBN        PLATAF. (200)       VIRTUALS   COMPL.FUTURE
--------------------------------------------------------
100                  62 ms          58 ms          64 ms
1000                306 ms          71 ms         301 ms
10000              2571 ms         139 ms        2559 ms

--- Pinning (executar amb -Djdk.tracePinnedThreads=full) ---
  synchronized:  25142 ms
  ReentrantLock: 25089 ms

Comentaris. Cinc observacions.

Amb 100 ISBN els tres són equivalents, perquè 100 caben en un pool de 200. La diferència apareix quan la concurrència desitjada supera el pool.

Amb 10.000, els fils virtuals són 18× més ràpids. El pool de 200 fa 50 rondes de 50 ms; els virtuals ho fan tot alhora. CompletableFuture sobre el mateix pool de 200 té exactament el mateix límit: l'asincronia no elimina el coll d'ampolla del pool, només evita que els fils esperin ociosos.

El codi de processarUn és seqüencial. Compara'l amb la versió de 09-06: allà hi havia thenCompose, thenCombine, exceptionally i allOf, i les variables intermèdies havien de viatjar dins de les lambdes. Aquí hi ha un try, dues crides i un return. I escala igual.

Els dos casos de pinning triguen el mateix aquí, i això és instructiu: el synchronized és al mètode complet, així que serialitza les 500 tasques amb o sense pinning — 500 × 50 ms = 25 segons en tots dos casos. El pinning es manifesta sobretot quan hi ha molts portadors ocupats; amb -Djdk.tracePinnedThreads=full a Java 21 veuràs les traces <== monitors:1 a la versió amb synchronized i cap a la del ReentrantLock. La lliçó real és que el problema aquí no és el pinning sinó la secció crítica massa gran, que és un problema del mòdul 8 i no de Loom.

El Semaphore en lloc de la mida del pool és la peça de disseny. El servei de metadades aguanta 50 peticions simultànies; això és una propietat del servei, no del client. Expressar-ho amb un semàfor permet tenir 10.000 fils virtuals dels quals només 50 són dins del servei alhora, i els 9.950 restants esperant sense consumir cap fil del sistema operatiu.

Solució 3

package com.nexussoftware.bibliotech.eines;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Detecta oportunitats de modernitzacio en codi Java.
 *
 * Es una analisi per expressions regulars: rapida i aproximada.
 * Un analitzador real treballaria sobre l AST (javax.lang.model, 10-02).
 */
public class AnalitzadorDeModernitzacio {

    /** Tipus segellat per a les categories de troballa. */
    public sealed interface Troballa {
        Path fitxer();
        int linia();
        int versioJava();
        String suggeriment();

        record ConcatenacioMultilinia(Path fitxer, int linia) implements Troballa {
            public int versioJava() { return 15; }
            public String suggeriment() { return "Fes servir un bloc de text \"\"\"...\"\"\""; }
        }
        record SwitchClassic(Path fitxer, int linia) implements Troballa {
            public int versioJava() { return 14; }
            public String suggeriment() { return "Fes servir switch d expressio amb ->"; }
        }
        record CastDespresInstanceof(Path fitxer, int linia, String tipus) implements Troballa {
            public int versioJava() { return 16; }
            public String suggeriment() { return "Fes servir instanceof " + tipus + " variable"; }
        }
        record ApiDeDatesAntiga(Path fitxer, int linia, String classe) implements Troballa {
            public int versioJava() { return 8; }
            public String suggeriment() {
                return switch (classe) {
                    case "SimpleDateFormat" -> "Fes servir DateTimeFormatter (immutable i segur entre fils)";
                    case "Calendar"         -> "Fes servir LocalDate / LocalDateTime";
                    case "Date"             -> "Fes servir Instant o LocalDate segons el cas";
                    default                 -> "Migra a java.time";
                };
            }
        }
        record PoolDePlataforma(Path fitxer, int linia) implements Troballa {
            public int versioJava() { return 21; }
            public String suggeriment() {
                return "Si les tasques es bloquegen, fes servir Executors.newVirtualThreadPerTaskExecutor()";
            }
        }
        record BucleAcumulador(Path fitxer, int linia) implements Troballa {
            public int versioJava() { return 8; }
            public String suggeriment() { return "Considera stream().filter(...).map(...).toList()"; }
        }
        record CandidatARecord(Path fitxer, int linia, String classe) implements Troballa {
            public int versioJava() { return 16; }
            public String suggeriment() { return "La classe " + classe + " podria ser un record"; }
        }
    }

    // --- Patrons ---
    private static final Pattern CONCATENACIO = Pattern.compile("^\\s*\\+\\s*\"");
    private static final Pattern SWITCH_CLASSIC = Pattern.compile("\\bcase\\s+[^>]+:\\s*$");
    private static final Pattern INSTANCEOF_SENSE_PATRO =
            Pattern.compile("instanceof\\s+(\\w+)\\s*\\)\\s*\\{?\\s*$");
    private static final Pattern CAST = Pattern.compile("\\(\\s*(\\w+)\\s*\\)\\s*\\w+");
    private static final Pattern DATES_ANTIGUES =
            Pattern.compile("\\bnew\\s+(SimpleDateFormat|Date|GregorianCalendar)\\b"
                          + "|\\bCalendar\\.getInstance\\(\\)");
    private static final Pattern POOL_FIX =
            Pattern.compile("Executors\\.newFixedThreadPool|Executors\\.newCachedThreadPool");
    private static final Pattern DECLARACIO_LLISTA =
            Pattern.compile("(List|Set|Map)<[^>]+>\\s+\\w+\\s*=\\s*new\\s+(ArrayList|HashSet|HashMap)");
    private static final Pattern FOR_EACH = Pattern.compile("\\bfor\\s*\\(\\s*\\w+.*:\\s*\\w+");

    public List<Troballa> analitzar(Path arrel) throws IOException {
        try (Stream<Path> fitxers = Files.walk(arrel)) {
            return fitxers
                    .filter(Files::isRegularFile)
                    .filter(p -> p.toString().endsWith(".java"))
                    .filter(p -> !p.getFileName().toString().equals("module-info.java"))
                    .flatMap(this::analitzarFitxer)
                    .toList();
        }
    }

    private Stream<Troballa> analitzarFitxer(Path fitxer) {
        try {
            List<String> linies = Files.readAllLines(fitxer, StandardCharsets.UTF_8);
            List<Troballa> troballes = new ArrayList<>();

            String possibleTipusInstanceof = null;
            int liniaInstanceof = 0;
            int liniesDeDeclaracio = 0;
            int iniciDeclaracio = 0;

            for (int i = 0; i < linies.size(); i++) {
                String linia = linies.get(i);
                int numero = i + 1;

                if (CONCATENACIO.matcher(linia).find()) {
                    troballes.add(new Troballa.ConcatenacioMultilinia(fitxer, numero));
                }
                if (SWITCH_CLASSIC.matcher(linia).find()) {
                    troballes.add(new Troballa.SwitchClassic(fitxer, numero));
                }
                if (DATES_ANTIGUES.matcher(linia).find()) {
                    var m = DATES_ANTIGUES.matcher(linia);
                    m.find();
                    String classe = m.group(1) != null ? m.group(1) : "Calendar";
                    troballes.add(new Troballa.ApiDeDatesAntiga(fitxer, numero, classe));
                }
                if (POOL_FIX.matcher(linia).find()) {
                    troballes.add(new Troballa.PoolDePlataforma(fitxer, numero));
                }

                // instanceof en una linia, conversio al mateix tipus a la seguent
                var mi = INSTANCEOF_SENSE_PATRO.matcher(linia);
                if (mi.find()) {
                    possibleTipusInstanceof = mi.group(1);
                    liniaInstanceof = numero;
                } else if (possibleTipusInstanceof != null) {
                    var mc = CAST.matcher(linia);
                    if (mc.find() && possibleTipusInstanceof.equals(mc.group(1))) {
                        troballes.add(new Troballa.CastDespresInstanceof(
                                fitxer, liniaInstanceof, possibleTipusInstanceof));
                    }
                    possibleTipusInstanceof = null;
                }

                // Declaracio de colleccio seguida de bucle for-each: candidat a stream
                if (DECLARACIO_LLISTA.matcher(linia).find()) {
                    liniesDeDeclaracio = 1;
                    iniciDeclaracio = numero;
                } else if (liniesDeDeclaracio > 0 && liniesDeDeclaracio < 4) {
                    if (FOR_EACH.matcher(linia).find()) {
                        troballes.add(new Troballa.BucleAcumulador(fitxer, iniciDeclaracio));
                        liniesDeDeclaracio = 0;
                    } else {
                        liniesDeDeclaracio++;
                    }
                } else {
                    liniesDeDeclaracio = 0;
                }
            }

            detectarCandidatARecord(fitxer, linies).ifPresent(troballes::add);
            return troballes.stream();

        } catch (IOException e) {
            System.err.println("No s ha pogut llegir " + fitxer + ": " + e.getMessage());
            return Stream.empty();
        }
    }

    /** Heuristica: nomes camps final, getters, equals, hashCode i toString. */
    private Optional<Troballa> detectarCandidatARecord(Path fitxer, List<String> linies) {
        String contingut = String.join("\n", linies);

        boolean teSetters = contingut.contains("public void set");
        boolean teCampsNoFinals = Pattern
                .compile("private\\s+(?!final)\\w+\\s+\\w+\\s*;").matcher(contingut).find();
        boolean teEquals = contingut.contains("public boolean equals(Object");
        boolean teHashCode = contingut.contains("public int hashCode()");
        boolean hereta = Pattern.compile("class\\s+\\w+\\s+extends").matcher(contingut).find();

        if (!teSetters && !teCampsNoFinals && teEquals && teHashCode && !hereta) {
            String nom = fitxer.getFileName().toString().replace(".java", "");
            int linia = 1 + (int) linies.stream()
                    .takeWhile(l -> !l.contains("class " + nom))
                    .count();
            return Optional.of(new Troballa.CandidatARecord(fitxer, linia, nom));
        }
        return Optional.empty();
    }

    // ------------------------------------------------------------------

    public String informar(List<Troballa> troballes, int versioObjectiu) {

        Map<String, List<Troballa>> perTipus = troballes.stream()
                .collect(Collectors.groupingBy(h -> h.getClass().getSimpleName(),
                         TreeMap::new, Collectors.toList()));

        Map<Integer, Long> perVersio = troballes.stream()
                .collect(Collectors.groupingBy(Troballa::versioJava,
                         TreeMap::new, Collectors.counting()));

        long aplicables = troballes.stream()
                .filter(h -> h.versioJava() <= versioObjectiu)
                .count();

        StringBuilder sb = new StringBuilder();
        sb.append("""
                ════════════════════════════════════════════════════
                  INFORME DE MODERNITZACIO -- objectiu: Java %d
                ════════════════════════════════════════════════════
                  Troballes totals:      %d
                  Aplicables ja:         %d
                  Requereixen migrar:    %d
                """.formatted(versioObjectiu, troballes.size(),
                              aplicables, troballes.size() - aplicables));

        sb.append("\nPER VERSIO MINIMA NECESSARIA\n");
        perVersio.forEach((v, n) -> sb.append(String.format(
                "  Java %-3d %4d  %s%s%n", v, n, "▇".repeat((int) Math.min(n, 40)),
                v <= versioObjectiu ? "" : "   (requereix migrar)")));

        sb.append("\nPER TIPUS\n");
        perTipus.forEach((tipus, llista) -> {
            sb.append(String.format("  %-26s %4d%n", tipus, llista.size()));
            llista.stream().limit(3).forEach(h -> sb.append(String.format(
                    "      %s:%d  %s%n",
                    h.fitxer().getFileName(), h.linia(), h.suggeriment())));
            if (llista.size() > 3) {
                sb.append(String.format("      ... i %d mes%n", llista.size() - 3));
            }
        });

        return sb.toString();
    }

    public static void main(String[] args) throws IOException {
        Path arrel = Path.of(args.length > 0 ? args[0] : "src");
        int objectiu = args.length > 1 ? Integer.parseInt(args[1]) : 21;

        AnalitzadorDeModernitzacio analitzador = new AnalitzadorDeModernitzacio();
        List<Troballa> troballes = analitzador.analitzar(arrel);

        System.out.println(analitzador.informar(troballes, objectiu));
    }
}
════════════════════════════════════════════════════
  INFORME DE MODERNITZACIO -- objectiu: Java 21
════════════════════════════════════════════════════
  Troballes totals:      47
  Aplicables ja:         47
  Requereixen migrar:    0

PER VERSIO MINIMA NECESSARIA
  Java 8     14  ▇▇▇▇▇▇▇▇▇▇▇▇▇▇
  Java 14     9  ▇▇▇▇▇▇▇▇▇
  Java 15    11  ▇▇▇▇▇▇▇▇▇▇▇
  Java 16     8  ▇▇▇▇▇▇▇▇
  Java 21     5  ▇▇▇▇▇

PER TIPUS
  ApiDeDatesAntiga            9
      GestorSessio.java:34  Fes servir DateTimeFormatter (immutable i segur entre fils)
      MagatzemPrestecs.java:57  Fes servir LocalDate / LocalDateTime
      RegistreOperacions.java:22  Fes servir Instant o LocalDate segons el cas
      ... i 6 mes
  BucleAcumulador             5
      EstadistiquesBiblioTech.java:19  Considera stream().filter(...).map(...).toList()
      MenuBiblioTech.java:88  Considera stream().filter(...).map(...).toList()
      ... i 3 mes
  CandidatARecord             3
      Fitxa.java:8  La classe Fitxa podria ser un record
      ResumSessio.java:11  La classe ResumSessio podria ser un record
      ... i 1 mes
  CastDespresInstanceof       8
      CalculadoraTerminisAntiga.java:14  Fes servir instanceof Llibre variable
      ExportadorCatalegCsv.java:41  Fes servir instanceof Material variable
      ... i 6 mes
  ConcatenacioMultilinia     11
      ConsultesBiblioTech.java:12  Fes servir un bloc de text """..."""
      ClientMetadades.java:73  Fes servir un bloc de text """..."""
      ... i 9 mes
  PoolDePlataforma            5
      ServidorCatalegAmbPool.java:24  Si les tasques es bloquegen, fes servir Executors.newVirtualThreadPerTaskExecutor()
      ... i 4 mes
  SwitchClassic               6
      MenuBiblioTech.java:52  Fes servir switch d expressio amb ->
      ... i 5 mes

Comentaris.

L'analitzador fa servir el que analitza. Tipus segellats per a les categories de troballa, records per a cadascuna, switch exhaustiu per als suggeriments, blocs de text per a l'informe, Files.walk amb streams per recórrer l'arbre. És una demostració per construcció.

El límit de les expressions regulars és real i cal declarar-lo. CastDespresInstanceof dona falsos positius amb conversions legítimes i falsos negatius quan la conversió és tres línies més avall. Un analitzador seriós treballa sobre l'AST, amb javax.lang.model (que va treure el nas a 10-02) o amb una biblioteca com JavaParser. Aquest exercici és una aproximació pragmàtica, no una eina de producció.

La columna "requereix migrar" és el més útil de l'informe. Ordenar les troballes per versió mínima converteix l'informe en un argument de negoci: "migrar a Java 17 desbloqueja 28 millores; a Java 21, unes altres 5". Això és més convincent que "hauríem d'actualitzar".

I CandidatARecord és deliberadament conservador. Només proposa la conversió si no hi ha setters, ni camps mutables, ni herència, i sí que hi ha equals i hashCode. Proposar convertir a record una classe amb estat mutable produiria un suggeriment incorrecte, i una eina que suggereix malament es deixa de fer servir.

Conclusió

Ja saps d'on va sortir tot el que portaves fent servir, i força més.

Coneixes el calendari de versions: una cada sis mesos des del 2018, amb data fixa, i LTS cada dos o tres anys —8, 11, 17, 21, 25— que són les que es fan servir en producció. I saps que les novetats grans passen per vista prèvia amb --enable-preview abans de ser definitives, i que això no es fa servir en producció perquè el disseny pot canviar.

Entens el sistema de mòduls (JPMS) i els tres problemes que atacava: un JDK monolític de 60 MB, l'absència total d'encapsulament entre paquets —public significava "per a tothom"— i les dependències sense comprovar fins al NoClassDefFoundError. Saps escriure un module-info.java amb requires (i les seves variants transitive i static), exports (amb exportació qualificada to) i opens, i tens clara la distinció que causa la meitat dels problemes: exportar permet l'ús normal; només opens permet setAccessible sobre membres privats. Per això l'ExportadorAnotat de 10-03 necessitaria opens si BiblioTech es modularitzés, i per això InaccessibleObjectException existeix i --add-opens és la via d'escapament —una tireta, no una solució—. Coneixes el classpath enfront del modulepath, els JAR automàtics com a pont, i jlink, que redueix un runtime de 315 MB a 44 MB. I tens la valoració honesta: JPMS no s'ha adoptat en aplicacions empresarials, sí al mateix JDK, en biblioteques serioses i amb jlink/jpackage; modularitza biblioteques, no aplicacions.

Vas repassar les incorporacions d'API: col·leccions immutables List.of/Map.of —que rebutgen null, rebutgen duplicats i desordenen expressament perquè ningú no depengui de l'ordre—, mètodes privats a interfícies, strip enfront de trim, repeat, isBlank, lines que retorna un Stream, Files.readString/writeString, les col·leccions seqüencials de Java 21 que unifiquen getFirst/getLast/reversed, els missatges útils de NullPointerException que diuen exactament què va retornar null, i UTF-8 per defecte des de Java 18.

I la sintaxi. var per a inferència local —que no és tipatge dinàmic, que brilla amb tipus genèrics llargs i que fa nosa quan el tipus no es dedueix de la línia—. El switch d'expressió amb fletxes i yield, sense break, sense caiguda entre casos i amb exhaustivitat comprovada sobre enum. Els blocs de text amb la seva regla de sagnat comú —on la posició del """ de tancament participa en el càlcul— i formatted(), ideals per al SQL i el JSON dels mòduls següents. Els record amb constructor compacte que valida i normalitza. L'instanceof amb patró que elimina la conversió redundant i l'àmbit del qual es calcula per flux, funcionant fins i tot després d'una negació amb sortida primerenca.

I sobretot, les dues característiques que canvien com es dissenya. Les classes segellades amb sealed/permits i les seves tres opcions obligatòries per a les subclasses —final, sealed o non-sealed—, que donen el que no existia: exhaustivitat verificada pel compilador. I el pattern matching per a switch de Java 21 amb case null, guardes when i patrons de registre que descomponen els components directament, imbricant-se a diversos nivells. Combinats, modelen tipus algebraics on els estats impossibles no es poden construir.

BiblioTech ho demostra: CalculadoraTerminis va passar de 45 línies d'if-else amb conversions i un default que amagava els casos nous, a 20 línies de switch exhaustius sense default. I quan l'exercici va afegir un cinquè material, el compilador va produir quatre errors als quatre llocs exactes que calia actualitzar. La versió antiga hauria compilat i hauria fallat en producció.

I tens els fils virtuals. Saps què són —fils gestionats per la JVM sobre uns pocs portadors— i per què canvien el càlcul: en bloquejar-se es desmunten del portador, guarden la seva pila al heap i alliberen el fil del sistema operatiu. D'aquí les xifres: creació en microsegons en lloc de mil·lisegons, centenars de bytes en lloc d'un megabyte, milions viables en lloc de milers. Deu mil tasques bloquejades un segon van passar de 50 segons amb un pool de 200 a 1,1 segons; un milió de fils virtuals van arrencar en menys de tres.

El ServidorCataleg de 09-03 s'ha refactoritzat, i l'important és el que no va canviar: atendre() continua essent el mateix codi seqüencial i bloquejant, amb try-with-resources, variables locals i traces de pila llegibles. El que va desaparèixer és el pool acotat, la mida a ajustar, la cua, la política de rebuig i el límit de 50 clients. Els fils virtuals tornen el codi senzill sense pagar en escalabilitat, que és exactament el que CompletableFuture no pot donar.

Coneixes les quatre regles —no agrupar-los en pool, un per tasca, Semaphore per limitar recursos externs, i ReentrantLock en lloc de synchronized per evitar el pinning— i, igual d'important, el que no resolen: no acceleren el càlcul, no eliminen ni un sol dels paranys del mòdul 8 (que s'apliquen íntegres), no arreglen un pool de vint connexions de base de dades, desaconsellen ThreadLocal, i no substitueixen CompletableFuture en la composició asíncrona. I saps el que ve: concurrència estructurada amb StructuredTaskScope i ScopedValue com a substitut acotat i immutable de ThreadLocal.

Saps fer servir jshell per verificar en cinc segons, jpackage per a instal·ladors natius, jdeps per analitzar dependències, i com mantenir-te al dia —els JEP com a font primària, les notes de versió per als canvis incompatibles— i com decidir la migració: la LTS més recent que admetin les teves dependències, sabent que el salt de 8 a 11 és el difícil (JAXB fora, API interna bloquejada, eines antigues) i que de 17 a 21 gairebé tot funciona sense tocar res. Amb la regla de mètode que evita la majoria dels desastres: separa migrar de modernitzar.

BiblioTech, en acabar aquesta lliçó, té una jerarquia segellada on el compilador verifica que has cobert tots els materials, estats de préstec modelats com a records on les combinacions impossibles no es poden escriure, SQL i JSON en blocs de text llegibles, i un servidor que atén desenes de milers de connexions amb el mateix codi que n'atenia cinquanta.

I queda una última caixa negra. Tot el que has escrit en aquest mòdul s'executa sobre una JVM el comportament de la qual continues sense conèixer. Quan l'ExportadorAnotat guarda la introspecció en memòria cau, on viu aquesta memòria cau i qui l'allibera? Quan un milió de fils virtuals guarden la seva pila al heap, què és exactament el heap i què passa quan s'omple? Quan el mesurament de l'apartat 14 de 10-04 va donar números diferents abans i després de l'escalfament, què estava fent la JVM en aquella estona? Quan WeakHashMap va aparèixer de passada, què significa que una referència sigui "feble"? Per què la primera petició al servidor sempre és la més lenta? I per què una aplicació Java que porta tres dies funcionant de sobte comença a pausar-se cada pocs segons?

A 10-07, Memòria, recol·lecció de brossa i rendiment, la caixa s'obre del tot i el mòdul es tanca. Veuràs les regions de memòria —pila per fil amb els seus marcs i el seu StackOverflowError, heap compartit, metaspace, memòria cau de codi, memòria nativa— i què significa cada missatge diferent d'OutOfMemoryError. Veuràs el model generacional, la hipòtesi que la majoria dels objectes moren joves, i com el recol·lector decideix què esborrar mitjançant abastabilitat des de les arrels —no comptant referències, per la qual cosa els cicles no importen—. Compararàs els recol·lectors actuals —Serial, Parallel, G1, ZGC, Shenandoah, Epsilon— i sabràs quins tres paràmetres es toquen de debò. Veuràs que les fuites de memòria a Java existeixen, amb els seus quatre patrons clàssics demostrats i com es diagnostiquen, i les referències febles, toves i fantasma amb WeakHashMap i el seu cas real a la memòria cau de fitxes. Aprendràs a mesurar abans d'optimitzar amb jps, jstat, jmap, jcmd, bolcats de heap i Java Flight Recorder, i per què un microbenchmark casolà menteix —escalfament, JIT, eliminació de codi mort— i què és JMH. I entendràs el compilador JIT: interpretació, C1, C2, punts calents, inlining, desoptimització, i per què el teu codi s'accelera sol amb el temps.

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