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
- El calendari de versions i les LTS
- Taula de novetats per versió
- El sistema de mòduls (JPMS)
module-info.java:requires,exports,opens- Encapsulament fort i la reflexió bloquejada
- Classpath enfront de modulepath, i
jlink - L'adopció real de JPMS: una valoració honesta
- Novetats d'API
var: inferència de tipus localsswitchd'expressió iyield- Blocs de text
recordrevisitatinstanceofamb patró- Classes segellades:
sealed,permits,non-sealed - Pattern matching per a
switchi patrons de registre - BiblioTech: refactorització de la jerarquia
- Fils virtuals: què són
- Fils de plataforma enfront de fils virtuals
- BiblioTech: el
ServidorCatalegamb fils virtuals - Pinning,
synchronizedi el que no resolen - El que ve: concurrència estructurada i
ScopedValue - Eines:
jshell,jpackagei companyia - Com mantenir-se al dia i com decidir la migració
- Errors Comuns i Consells
- Exercicis
- 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:
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.
- 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).
- 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:
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
...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.
module-info.java: requires, exports, opens
module-info.java: requires, exports, opensUn 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) |
Sí | No |
| Reflexió sobre membres públics | Sí | Sí |
Reflexió sobre membres privats (setAccessible) |
No | Sí |
| 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.
- 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 @0x1b6d3586Què 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.
- Classpath enfront de modulepath, i
jlink
jlinkConviuen 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# 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/...BiblioTechAppDe 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).
- 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
setAccessiblenecessitenopensper 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.javaper declarar la seva API pública de manera verificable. - Amb
jlinkijpackage, 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 | Sí: publica module-info.java. És documentació verificada de la teva API |
| Aplicació d'escriptori per distribuir | Sí: 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.
- 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:
- Són realment immutables.
add,removeisetllancenUnsupportedOperationException. - No admeten
null, ni com a element ni com a clau ni com a valor. LlancenNullPointerException. Set.ofiMap.ofrebutgen duplicats ambIllegalArgumentException, en lloc d'ignorar-los silenciosament.- L'ordre d'iteració de
Set.ofiMap.ofno 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 programaArrays.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:
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ó:
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.
var: inferència de tipus locals
var: inferència de tipus localsJava 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:
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: NOQuan 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:
switch d'expressió i yield — reprèn 02-03
switch d'expressió i yield — reprèn 02-03A 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:
- Sense
break, perquè no hi ha caiguda entre casos. - És una expressió: s'assigna, es retorna, es passa com a argument.
- Diverses etiquetes per cas:
case SATURDAY, SUNDAY ->. - Exhaustivitat comprovada: sobre un
enum, si falten casos i no hi hadefault, 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);
};
- 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:
- S'obre amb
"""seguit d'un salt de línia obligatori."""textno compila. - Es tanca amb
""", a la mateixa línia que l'últim contingut o en una línia pròpia. - 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.
record revisitat — reprèn 04-07
record revisitat — reprèn 04-07Els 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:
El compilador genera automàticament:
- Camps
private finalper a cada component. - Constructor canònic amb tots els components.
- Mètodes d'accés amb el nom del component (
isbn(), nogetIsbn()). equalsihashCodebasats en tots els components.toStringamb 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.Recordimplí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.
instanceof amb patró — reprèn 03-06
instanceof amb patró — reprèn 03-06Definitiu 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 COMPILAI 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.
- Classes segellades:
sealed, permits, non-sealed
sealed, permits, non-sealedDefinitives 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:
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:
- Tota subclasse d'una classe
sealedha de serfinal,sealedonon-sealed. No hi ha quarta opció. - Les classes permeses han de ser al mateix mòdul (o al mateix paquet si no hi ha mòduls).
- Han d'estendre-la realment: llistar una classe que no l'estengui és error de compilació.
permitses 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.
- Pattern matching per a
switch i patrons de registre
switch i patrons de registreDefinitius 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:
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 + ")");
}
- 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 puntualmentLa 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ó.
- 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 AutoCloseableFixa'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 msCinquanta 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.
- 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 | Sí | 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 |
Sí | 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.
- BiblioTech: el
ServidorCataleg amb fils virtuals
ServidorCataleg amb fils virtualsLa 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.
- Pinning,
synchronized i el que no resolen
synchronized i el que no resolenEl pinning
El mecanisme dels fils virtuals exigeix que es puguin desmuntar del portador en bloquejar-se. Hi ha dues situacions on no poden:
- Dins d'un bloc o mètode
synchronized. - 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:
Thread[#31,ForkJoinPool-1-worker-1,5,CarrierThreads]
com.nexussoftware.bibliotech.servei.CacheProblematica.obtenir(CacheProblematica.java:12)
<== monitors:1Nota 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 |
- El que ve: concurrència estructurada i
ScopedValue
ScopedValueDues 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 vivaLes 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ò |
- Eines:
jshell, jpackage i companyia
jshell, jpackage i companyiaUna 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> 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> /exitInsubstituï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):
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.2Produeix 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:
jdeps — analitza dependències (el vas fer servir amb jlink), i detecta usos d'API interna:
jcmd, jfr — diagnòstic i gravació de vol. Tot això és 10-07.
- 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:
javax.xml.bind(JAXB) ijava.xml.wses van eliminar del JDK. Cal afegir-los com a dependència.- Biblioteques que fan servir API interna (
sun.misc.Unsafe,sun.reflect) fallen ambInaccessibleObjectException. - Eines antigues (versions velles de Maven, Gradle, Lombok, ASM) no entenen el bytecode nou.
-XX:+UseParallelOldGCi 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, streamsI 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çó:
sealed interface ElementCataleg permits Material, Colleccio, onColleccioagrupa diversos elements (un element pot contenir-ne d'altres).sealed abstract class Material permits Llibre, Revista, Dvd, AudioLlibre, ambLlibreiDvdcom afinal,Revistasegellada amb dos subtipus, iAudioLlibrecom anon-sealed.- Un
sealed interface ResultatCercaamb recordsTrobat,NoTrobatiAmbigu(List<Material> candidats). - Un servei amb
switchexhaustius i sensedefaultque calculi: termini de préstec, valor de reposició, etiqueta de prestatge i una fitxa completa amb blocs de text. - Un mètode
int comptarMaterials(ElementCataleg)recursiu que descendeixi per les col·leccions imbricades fent servir patrons de registre. - 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.
EnriquidorVirtualque consulti metadades i descarregui portades de N ISBN fent servirExecutors.newVirtualThreadPerTaskExecutor().- Codi seqüencial i bloquejant dins de cada tasca, sense
thenComposenithenCombine. - Limita a 50 peticions simultànies al servei extern amb
Semaphore, no amb la mida del pool. - Gestiona les fallades individuals sense que tombin el lot, amb un
record ResultatEnriquimentsegellat. - Mesura i compara: pool de 200 de plataforma, fils virtuals, i
CompletableFuture, amb 100, 1.000 i 10.000 ISBN. - Demostra el pinning: una versió amb
synchronizedal voltant de la crida bloquejant enfront d'una altra ambReentrantLock, executades amb-Djdk.tracePinnedThreads=full.
Exercici 3: informe de modernització
Escriu una eina AnalitzadorDeModernitzacio que examini fitxers .java i detecti oportunitats de modernització:
- Cadenes concatenades amb
+en diverses línies → bloc de text. switchclàssic ambbreak→switchd'expressió.instanceofseguit de conversió al mateix tipus →instanceofamb patró.- Classes amb només camps
final, constructor, getters,equals,hashCodeitoString→record. new SimpleDateFormat/java.util.Date→java.time(10-05).Executors.newFixedThreadPoolamb tasques bloquejants → fils virtuals.- 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 errorsComentaris. 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 msComentaris. 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 mesComentaris.
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
- Introducció a Java
- Configuració de l'entorn de desenvolupament
- Sintaxi i estructura bàsica
- Variables i tipus de dades
- Operadors
- Entrada i sortida per consola
- El teu primer programa complet: BiblioTech
Mòdul 2: Flux de control
- Sentències condicionals
- Bucles
- Sentències switch
- Break i continue
- Depuració i traces d'execució
- Projecte: menú interactiu de BiblioTech
Mòdul 3: Programació orientada a objectes
- Introducció a la POO
- Classes i objectes
- Mètodes
- Constructors
- Herència
- Polimorfisme
- Encapsulament
- Abstracció
- La classe Object: equals, hashCode i toString
Mòdul 4: Programació orientada a objectes avançada
- Interfícies
- Classes abstractes
- Classes internes
- Classes anònimes
- Expressions lambda
- Interfícies funcionals i referències a mètodes
- Enumeracions i registres
Mòdul 5: Estructures de dades i col·leccions
- Arrays
- El framework de col·leccions
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cua i Deque
- Pila
- Ordenació i cerca en col·leccions
Mòdul 6: Gestió d'excepcions
- Introducció a les excepcions
- Bloc try-catch
- Throw i throws
- Excepcions personalitzades
- Bloc finally
- Try-with-resources i AutoCloseable
- Estratègies de gestió d'errors i logging
Mòdul 7: Entrada/sortida de fitxers
- Lectura de fitxers
- Escriptura de fitxers
- Fluxos de fitxers
- BufferedReader i BufferedWriter
- Serialització
- L'API NIO.2: Path i Files
- Formats d'intercanvi: CSV i Properties
Mòdul 8: Multifil i concurrència
- Introducció al multifil
- Creació de fils
- Cicle de vida d'un fil
- Sincronització
- Utilitats de concurrència
- Col·leccions concurrents i variables atòmiques
- Tasques asíncrones amb CompletableFuture
Mòdul 9: Xarxes
- Introducció a les xarxes
- Sockets
- ServerSocket
- DatagramSocket i DatagramPacket
- URL i HttpURLConnection
- El client HTTP modern
Mòdul 10: Temes avançats
- Genèrics
- Anotacions
- Reflexió
- Característiques de Java 8: Streams i Optional
- Dates i hores amb java.time
- Java 9 i més enllà
- Memòria, recol·lecció de brossa i rendiment
Mòdul 11: Frameworks i llibreries de Java
- Introducció als frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Proves avançades amb Mockito
- Llibreries essencials de l'ecosistema
