La lección anterior terminó señalando algo que llevas arrastrando desde el principio del curso: has estado usando características modernas de Java sin saber de dónde salieron.
switch con flechas. record. var. List.of. instanceof con patrón. String.repeat. Files.readString. HttpClient. Stream.toList. Todas aparecieron en algún momento, se usaron con naturalidad y nadie dijo en qué versión llegaron ni qué problema resolvían. Y hay más que aún no has visto: bloques de texto, clases selladas, pattern matching para switch, y los hilos virtuales, que llevas dos módulos esperando desde que en 09-03 dijimos que cambiarían por completo el cálculo de "un hilo por conexión".
Esta lección ordena todo eso.
Y no es solo un catálogo. Java cambió su modelo de desarrollo en 2018: pasó de publicar una versión grande cada tres años a una versión cada seis meses, con versiones de soporte extendido cada dos o tres años. Eso cambió por completo el ritmo del lenguaje y también la pregunta práctica que todo equipo se hace: ¿a qué versión migramos y cuándo?
Al terminar, sabrás qué trajo cada versión desde Java 9, qué merece la pena usar hoy y qué no, y BiblioTech tendrá una jerarquía sellada con pattern matching exhaustivo y un ServidorCatalogo que atiende diez mil conexiones simultáneas sin un pool de hilos.
Contenido
- El calendario de versiones y las LTS
- Tabla de novedades por versión
- El sistema de módulos (JPMS)
module-info.java:requires,exports,opens- Encapsulamiento fuerte y la reflexión bloqueada
- Classpath frente a modulepath, y
jlink - La adopción real de JPMS: una valoración honesta
- Novedades de API
var: inferencia de tipos localesswitchde expresión yyield- Bloques de texto
recordrevisitadoinstanceofcon patrón- Clases selladas:
sealed,permits,non-sealed - Pattern matching para
switchy patrones de registro - BiblioTech: refactorización de la jerarquía
- Hilos virtuales: qué son
- Hilos de plataforma frente a hilos virtuales
- BiblioTech: el
ServidorCatalogocon hilos virtuales - Pinning,
synchronizedy lo que no resuelven - Lo que viene: concurrencia estructurada y
ScopedValue - Herramientas:
jshell,jpackagey compañía - Cómo mantenerse al día y cómo decidir la migración
- Errores Comunes y Consejos
- Ejercicios
- El calendario de versiones y las LTS
Hasta Java 8, las versiones salían cuando estaban listas: Java 5 en 2004, Java 6 en 2006, Java 7 en 2011, Java 8 en 2014. Java 9 se retrasó tres años y medio por el sistema de módulos, y esa experiencia motivó un cambio radical.
Desde Java 10 (marzo de 2018), sale una versión cada seis meses, en marzo y septiembre, con fecha fija. Lo que está listo entra; lo que no, espera al siguiente tren.
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 tipos de versión:
| Tipo | Frecuencia | Soporte | Uso recomendado |
|---|---|---|---|
| Normal (feature release) | Cada 6 meses | 6 meses, hasta la siguiente | Probar novedades, proyectos personales |
| LTS (Long-Term Support) | Cada 2-3 años | Años (varía según proveedor) | Producción |
Las LTS hasta hoy son 8, 11, 17, 21 y 25. Y el consejo práctico es claro: en producción, usa una LTS. Una versión normal deja de recibir parches de seguridad a los seis meses, lo que obliga a migrar dos veces al año.
Las características de vista previa. Desde Java 12, las novedades importantes de lenguaje pasan por una o varias rondas de preview antes de ser definitivas. Se activan con una opción del compilador:
Esto permite al equipo de Java recoger opiniones y cambiar el diseño antes de comprometerse para siempre. Los record estuvieron dos versiones en vista previa; los hilos virtuales, tres. No uses características de vista previa en producción: pueden cambiar o desaparecer, y el bytecode generado solo funciona en esa versión exacta.
- Tabla de novedades por versión
Un mapa completo para consulta. Las que ya conoces llevan la lección donde las viste.
| Versión | Año | Novedades relevantes |
|---|---|---|
| 9 | 2017 | Sistema de módulos (JPMS), List.of/Set.of/Map.of (05-02), métodos privados en interfaces, Stream.takeWhile/dropWhile/ofNullable (10-04), Optional.stream/or/ifPresentOrElse (10-04), jshell, jlink, diamante en clases anónimas (10-01) |
| 10 | 2018 | var, List.copyOf, Collectors.toUnmodifiableList, Optional.orElseThrow() sin argumentos |
| 11 | 2018 LTS | HttpClient definitivo (09-06), String.repeat/strip/isBlank/lines, Files.readString/writeString (07-06), var en parámetros de lambda, lanzar un .java sin compilar |
| 12 | 2019 | Collectors.teeing (10-04), String.indent, switch de expresión (vista previa) |
| 13 | 2019 | Bloques de texto (vista previa), switch con yield (vista previa) |
| 14 | 2020 | switch de expresión definitivo, record (vista previa), instanceof con patrón (vista previa), mensajes útiles de NullPointerException |
| 15 | 2020 | Bloques de texto definitivos, clases selladas (vista previa), CharSequence.isEmpty |
| 16 | 2021 | record definitivo, instanceof con patrón definitivo, Stream.toList (10-04), Stream.mapMulti, encapsulamiento fuerte por defecto |
| 17 | 2021 LTS | Clases selladas definitivas, RandomGenerator, eliminación del Security Manager, switch con patrones (vista previa) |
| 18 | 2022 | UTF-8 por defecto (07-01), servidor web simple jwebserver, @snippet en Javadoc |
| 19 | 2022 | Hilos virtuales (vista previa), concurrencia estructurada (incubación), API de funciones foráneas (vista previa) |
| 20 | 2023 | Refinamientos de vista previa, ScopedValue (incubación) |
| 21 | 2023 LTS | Hilos virtuales definitivos, pattern matching para switch definitivo, patrones de registro definitivos, colecciones secuenciales, String.repeat mejorado |
| 22 | 2024 | API de funciones foráneas definitiva, sentencias antes de super() (vista previa) |
| 23 | 2024 | Javadoc en Markdown, refinamientos |
| 24 | 2025 | Criptografía post-cuántica, mejoras del GC generacional ZGC |
| 25 | 2025 LTS | Consolidación de vista previas, ficheros fuente compactos y métodos de instancia principales |
Los tres hitos que cambian cómo se escribe Java son: Java 8 (lambdas y streams), Java 17 (records, selladas, pattern matching incipiente, switch de expresión) y Java 21 (hilos virtuales y pattern matching completo).
- El sistema de módulos (JPMS)
Java 9 introdujo el cambio estructural más grande desde el nacimiento del lenguaje: el Java Platform Module System, conocido como JPMS o por su nombre en clave, Proyecto Jigsaw.
El problema que resolvía, en tres partes:
1. El JDK era monolítico. El rt.jar de Java 8 pesaba unos 60 MB y contenía todo: colecciones, red, XML, CORBA, Swing, applets. Una aplicación de servidor que solo usa colecciones y red arrastraba Swing y CORBA. En un contenedor Docker, eso son cientos de megabytes de imagen por nada.
2. No había encapsulamiento entre paquetes (JAR hell). El modificador public significa "visible para todo el mundo". Si tu biblioteca tiene un paquete com.miempresa.interno con clases de apoyo, no había forma de impedir que un usuario las importara. Y una vez que alguien depende de tu clase interna, ya no puedes cambiarla. El propio JDK sufría esto con sun.misc.Unsafe, usada por medio ecosistema.
3. No había comprobación de dependencias al arrancar. El classpath es una lista plana de JAR. Si falta uno, el programa arranca y falla con NoClassDefFoundError en el momento en que llega a usarlo — quizá horas después, quizá solo en un caso poco frecuente.
La solución: un módulo es una unidad con nombre que declara qué necesita y qué expone.
graph TD
A["module com.nexussoftware.bibliotech"] -->|"requires"| B["java.base<br/>implicito"]
A -->|"requires"| C["java.logging"]
A -->|"requires"| D["java.net.http"]
A -->|"exports"| E["com...dominio<br/>PUBLICO"]
A -->|"exports"| F["com...servicio<br/>PUBLICO"]
A -.->|"NO exporta"| G["com...interno<br/>INACCESIBLE desde fuera"]
A -->|"opens"| H["com...dominio<br/>reflexion profunda permitida"]
El JDK mismo se dividió en unos 70 módulos. Se pueden listar:
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
...java.base es especial: contiene java.lang, java.util, java.io, java.nio, java.time y java.util.concurrent, y todos los módulos lo requieren implícitamente. Nunca hay que declararlo.
module-info.java: requires, exports, opens
module-info.java: requires, exports, opensUn módulo se declara en un fichero llamado exactamente module-info.java, en la raíz del árbol de fuentes:
src/
module-info.java
com/nexussoftware/bibliotech/
BiblioTechApp.java
dominio/
servicio/
persistencia/
red/
interno//**
* Modulo principal de BiblioTech.
*/
module com.nexussoftware.bibliotech {
// --- QUE NECESITO ---
requires java.logging; // java.util.logging (06-07)
requires java.net.http; // HttpClient (09-06)
requires java.sql; // JDBC, para el modulo 11
// transitive: quien me requiera a mi obtiene tambien esto
requires transitive com.nexussoftware.comun;
// static: necesario para compilar, opcional en ejecucion
requires static com.nexussoftware.herramientas;
// --- QUE OFREZCO ---
exports com.nexussoftware.bibliotech.dominio;
exports com.nexussoftware.bibliotech.servicio;
// Exportacion cualificada: SOLO a estos modulos
exports com.nexussoftware.bibliotech.persistencia
to com.nexussoftware.bibliotech.pruebas;
// com.nexussoftware.bibliotech.interno NO se exporta:
// es inaccesible desde fuera aunque sus clases sean public
// --- REFLEXION ---
// opens permite reflexion PROFUNDA (setAccessible sobre privados)
opens com.nexussoftware.bibliotech.dominio;
// Apertura cualificada
opens com.nexussoftware.bibliotech.persistencia to com.fasterxml.jackson.databind;
// --- SERVICIOS (ServiceLoader) ---
uses com.nexussoftware.bibliotech.servicio.PoliticaMultas;
provides com.nexussoftware.bibliotech.servicio.PoliticaMultas
with com.nexussoftware.bibliotech.interno.PoliticaEstandar;
}Las directivas, una a una:
| Directiva | Significado |
|---|---|
requires X |
Necesito el módulo X para compilar y ejecutar |
requires transitive X |
Además, quien me requiera obtiene X automáticamente |
requires static X |
Necesario en compilación, opcional en ejecución |
exports P |
El paquete P es accesible para todos |
exports P to M1, M2 |
El paquete P es accesible solo para M1 y M2 |
opens P |
Reflexión profunda permitida sobre P |
opens P to M |
Reflexión profunda permitida solo a M |
open module |
Todo el módulo abierto a reflexión |
uses S |
Consumo el servicio S vía ServiceLoader |
provides S with I |
Ofrezco I como implementación de S |
La distinción clave, que causa la mitad de los problemas prácticos:
exports |
opens |
|
|---|---|---|
Uso normal en compilación (import, new) |
Sí | No |
| Reflexión sobre miembros públicos | Sí | Sí |
Reflexión sobre miembros privados (setAccessible) |
No | Sí |
| Se comprueba | En compilación y ejecución | Solo en ejecución |
Un framework de persistencia como Hibernate necesita opens, porque lee campos privados de tus entidades. Exportar el paquete no basta.
- Encapsulamiento fuerte y la reflexión bloqueada
Retomamos 10-03. Este código funcionó durante veinte años:
Field valor = String.class.getDeclaredField("value");
valor.setAccessible(true);
byte[] bytes = (byte[]) valor.get("Java Efectivo");Y desde Java 16 (donde el encapsulamiento fuerte pasó a ser el valor por defecto):
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é cambió: los paquetes internos del JDK ya no están abiertos. java.base exporta java.lang (puedes usar String) pero no lo abre (no puedes hurgar en sus campos privados).
Esto rompió muchas bibliotecas antiguas, y por eso existe la salida de emergencia:
# Abrir un paquete concreto a todo el codigo sin modulo
java --add-opens java.base/java.lang=ALL-UNNAMED -cp app.jar com.ejemplo.App
# Exportar (uso normal, no reflexion profunda)
java --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED ...
# Varias a la vez
java --add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.util=ALL-UNNAMED \
-jar app.jar--add-opens es una tirita, no una solución. Cada uso indica que algo depende de detalles internos que pueden cambiar en cualquier versión. Si lo necesitas para una biblioteca de terceros, actualízala; si es de tu código, replantéalo.
Para tu propio código modularizado, la solución correcta es declarar opens en module-info.java:
module com.nexussoftware.bibliotech {
requires java.logging;
exports com.nexussoftware.bibliotech.dominio;
// Nuestro ExportadorAnotado de 10-03 hace setAccessible sobre
// los campos privados de las entidades: necesita opens.
opens com.nexussoftware.bibliotech.dominio;
}Sin ese opens, el ExportadorAnotado que escribiste en 10-03 lanzaría InaccessibleObjectException en cuanto BiblioTech se modularizara. Es exactamente el escenario que se anticipó allí.
- Classpath frente a modulepath, y
jlink
jlinkConviven dos formas de ejecutar:
# CLASSPATH: modo clasico. Todo va al "modulo sin nombre".
javac -cp lib/*.jar -d clases $(find src -name "*.java")
java -cp clases:lib/*.jar com.nexussoftware.bibliotech.BiblioTechApp
# MODULEPATH: modo modular
javac -d clases --module-source-path src $(find src -name "*.java")
java --module-path clases -m com.nexussoftware.bibliotech/com.nexussoftware.bibliotech.BiblioTechApp| Aspecto | Classpath | Modulepath |
|---|---|---|
| Nombre | Módulo sin nombre (unnamed) | Módulos con nombre |
| Encapsulamiento | Ninguno: todo public es accesible |
Fuerte: solo lo exportado |
| Dependencias | Sin comprobar | Comprobadas al arrancar |
| Ficheros duplicados | El primero gana, silenciosamente | Error: split packages |
| Compatibilidad | Total | Requiere que las bibliotecas cooperen |
Los JAR automáticos son el puente: un JAR normal en el modulepath se convierte en un módulo cuyo nombre se deriva del nombre del fichero, que exporta todos sus paquetes y requiere todos los módulos. Permite migrar poco a poco, aunque el nombre derivado del fichero es frágil.
jlink: un runtime a medida
La ventaja más tangible de JPMS. jlink construye una imagen de Java que contiene solo los módulos que tu aplicación usa:
# Descubrir que modulos necesita la aplicacion
jdeps --print-module-deps --ignore-missing-deps clases/bibliotech.jar# Construir el runtime minimo
jlink --add-modules java.base,java.logging,java.net.http \
--strip-debug \
--compress=zip-6 \
--no-header-files \
--no-man-pages \
--output runtime-bibliotech
# Y ejecutar con el
./runtime-bibliotech/bin/java -m com.nexussoftware.bibliotech/...BiblioTechAppDe 315 MB a 44 MB. En una imagen de contenedor multiplicada por cien despliegues al día, eso es tiempo de descarga, coste de almacenamiento y superficie de ataque. Además, el runtime resultante no necesita Java instalado en la máquina destino.
Y jpackage (Java 14) va un paso más allá: empaqueta ese runtime junto a tu aplicación en un instalador nativo (.deb, .rpm, .msi, .dmg).
- La adopción real de JPMS: una valoración honesta
Toca ser sincero, porque el tema genera confusión.
JPMS no se ha adoptado ampliamente en aplicaciones. Casi diez años después de Java 9, la inmensa mayoría de las aplicaciones empresariales Java siguen ejecutándose en el classpath, sin module-info.java. Spring Boot, la plataforma dominante, no exige modularización. Muchas bibliotecas populares no publican módulos con nombre.
Por qué:
- Coste de migración alto. Modularizar una aplicación grande con cincuenta dependencias exige que las cincuenta cooperen.
- Los split packages rompen mucho. Dos JAR con clases en el mismo paquete son legales en el classpath e ilegales en el modulepath. Es frecuente en bibliotecas antiguas.
- La reflexión sufre. Frameworks que dependen de
setAccessiblenecesitanopenspor todas partes, lo que erosiona el beneficio. - El beneficio no es evidente para una aplicación. Un JAR ejecutable no gana mucho encapsulando paquetes que solo él usa.
Dónde sí ha triunfado:
- En el propio JDK. Es lo que permitió eliminar CORBA y las applets, dividir la plataforma y activar el encapsulamiento fuerte que te protege de depender de internos.
- En bibliotecas serias, que publican
module-info.javapara declarar su API pública de forma verificable. - Con
jlinkyjpackage, para aplicaciones de escritorio y contenedores mínimos.
Qué hacer en la práctica:
| Caso | Recomendación |
|---|---|
| Aplicación empresarial con Spring Boot | No modularices. Classpath, y --add-opens si alguna biblioteca lo pide |
| Biblioteca de uso público | Sí: publica module-info.java. Es documentación verificada de tu API |
| Aplicación de escritorio a distribuir | Sí: jlink + jpackage justifican el esfuerzo |
| Contenedor donde el tamaño importa | Considera jlink aunque no modularices tu código |
| Código nuevo desde cero, un solo artefacto | Opcional. Aporta disciplina, cuesta fricción |
Lo que sí tienes que saber sí o sí es la parte que te afecta aunque no modularices: el encapsulamiento fuerte está activo, InaccessibleObjectException existe, y --add-opens es la vía de escape.
- Novedades de API
Repaso de las incorporaciones a la biblioteca estándar, muchas ya usadas.
Colecciones inmutables (Java 9) — retoma 05-02
List<String> empleados = List.of("Marta Ruiz", "Diego Alonso", "Nuria Vidal");
Set<String> isbns = Set.of("978-0000000001", "978-0000000002");
Map<String, Integer> prestamos = Map.of("Marta Ruiz", 4, "Diego Alonso", 2);
Map<String, Integer> muchos = Map.ofEntries(
Map.entry("Marta Ruiz", 4),
Map.entry("Diego Alonso", 2),
Map.entry("Nuria Vidal", 3));
// Copia inmutable de una coleccion existente (Java 10)
List<String> copia = List.copyOf(listaModificable);Cuatro reglas que sorprenden:
- Son realmente inmutables.
add,removeysetlanzanUnsupportedOperationException. - No admiten
null, ni como elemento ni como clave ni como valor. LanzanNullPointerException. Set.ofyMap.ofrechazan duplicados conIllegalArgumentException, en lugar de ignorarlos silenciosamente.- El orden de iteración de
Set.ofyMap.ofno está especificado y cambia entre ejecuciones a propósito, para que nadie dependa de él.
Set<String> conjunto = Set.of("a", "b", "c", "d");
System.out.println(conjunto); // orden distinto en cada ejecucion del programaArrays.asList sigue siendo distinta: tamaño fijo pero permite set, y acepta null.
Métodos privados en interfaces (Java 9) — retoma 04-01
public interface ServicioPrestamos {
Resultado<Prestamo> prestar(String isbn, String empleado);
default Resultado<Prestamo> prestarValidando(String isbn, String empleado) {
if (!esIsbnValido(isbn)) {
return Resultado.fallo("ISBN mal formado: " + isbn);
}
if (!esEmpleadoValido(empleado)) {
return Resultado.fallo("Empleado no válido");
}
return prestar(isbn, empleado);
}
// PRIVADO: logica compartida entre los default, invisible desde fuera
private boolean esIsbnValido(String isbn) {
return isbn != null && isbn.matches("978-\\d{10}");
}
private static boolean esEmpleadoValido(String empleado) {
return empleado != null && !empleado.isBlank();
}
}Antes de Java 9, la lógica común entre métodos default tenía que ser un método default público más, contaminando la API.
Métodos de String (Java 11-12)
String titulo = " Java Efectivo ";
System.out.println(titulo.strip()); // "Java Efectivo" (Unicode-aware)
System.out.println(titulo.stripLeading()); // "Java Efectivo "
System.out.println(titulo.stripTrailing()); // " Java Efectivo"
System.out.println(" ".isBlank()); // true
System.out.println("".isEmpty()); // true
System.out.println("=".repeat(40)); // línea separadora
System.out.println("uno\ndos".lines().count()); // 2 (devuelve Stream<String>)
System.out.println("texto".indent(4)); // sangrado (Java 12)strip() frente a trim(): trim() elimina caracteres con código menor o igual a 32 (heredado de 1996); strip() usa la definición Unicode de espacio en blanco y maneja correctamente espacios no separables y caracteres de otros alfabetos. Usa strip() en código nuevo.
Y lines() devuelve un Stream<String>, lo que conecta con 10-04:
long lineasNoVacias = textoCompleto.lines()
.map(String::strip)
.filter(l -> !l.isBlank())
.count();Files.readString y writeString (Java 11) — retoma 07-06
// ANTES
List<String> lineas = Files.readAllLines(ruta, StandardCharsets.UTF_8);
String contenido = String.join("\n", lineas);
// AHORA
String contenido = Files.readString(ruta, StandardCharsets.UTF_8);
Files.writeString(ruta, contenido, StandardCharsets.UTF_8);Para ficheros pequeños de configuración es la forma más directa. Para ficheros grandes, sigue siendo Files.lines() con procesamiento perezoso (10-04).
Colecciones secuenciales (Java 21)
Una incorporación pequeña que arregla una incoherencia vieja de la API de colecciones (módulo 5):
List<Libro> catalogo = new ArrayList<>(...);
// ANTES: cada coleccion tenia su forma
Libro primero = catalogo.get(0);
Libro ultimo = catalogo.get(catalogo.size() - 1);
Libro primeroDeque = deque.getFirst();
Libro primeroSet = conjuntoOrdenado.iterator().next(); // ¡así!
// AHORA: SequencedCollection unifica
Libro primero2 = catalogo.getFirst();
Libro ultimo2 = catalogo.getLast();
catalogo.addFirst(nuevo);
catalogo.addLast(otro);
List<Libro> alReves = catalogo.reversed(); // vista, no copia
// Y funciona igual en LinkedHashSet, TreeSet, Deque, LinkedHashMap
LinkedHashMap<String, Libro> porIsbn = new LinkedHashMap<>();
var primeraEntrada = porIsbn.firstEntry();Mensajes útiles de NullPointerException (Java 14)
Un cambio sin sintaxis nueva que ahorra horas reales de depuración:
En Java 8:
Exception in thread "main" java.lang.NullPointerException
at com.nexussoftware.bibliotech.BiblioTechApp.main(BiblioTechApp.java:42)¿Cuál de los tres era null? A depurar.
En Java 14+:
Exception in thread "main" java.lang.NullPointerException:
Cannot invoke "String.strip()" because the return value of
"com.nexussoftware.bibliotech.dominio.Editorial.getNombre()" is null
at com.nexussoftware.bibliotech.BiblioTechApp.main(BiblioTechApp.java:42)Te dice exactamente qué devolvió null y qué se intentó llamar sobre él. Está activo por defecto desde Java 15, y es una de las mejores razones para migrar desde Java 8.
UTF-8 por defecto (Java 18) — retoma 07-01
Desde Java 18, el juego de caracteres por defecto de la plataforma es UTF-8 en todos los sistemas operativos. Antes dependía de la configuración regional, y era la causa del bug clásico: un CSV escrito en Windows con windows-1252 que se leía mal en un servidor Linux con UTF-8.
Aun así, sigue especificando el charset explícitamente. Es lo que insistimos en el módulo 7, y sigue siendo la práctica correcta: hace el código independiente de la versión de Java y de la configuración del entorno.
var: inferencia de tipos locales
var: inferencia de tipos localesJava 10 introdujo var para variables locales. El compilador infiere el tipo de la expresión de inicialización.
var catalogo = new ArrayList<Libro>(); // ArrayList<Libro>
var titulo = "Java Efectivo"; // String
var paginas = 412; // int
var fecha = LocalDate.of(2026, 8, 5); // LocalDate
// Donde mas brilla: tipos genericos largos
var porEmpleadoYTipo = new HashMap<String, Map<TipoMaterial, List<Prestamo>>>();
// En bucles
for (var entrada : porEmpleadoYTipo.entrySet()) { }
for (var i = 0; i < 10; i++) { }
// En try-with-resources
try (var lineas = Files.lines(ruta, StandardCharsets.UTF_8)) { }var NO es tipado dinámico. El tipo se fija en compilación y no cambia nunca:
Dónde NO se puede usar:
// var campo = "x"; // campos de clase: NO
// public var metodo() { } // retorno de método: NO
// void m(var parametro) { } // parámetros: NO (salvo en lambdas, Java 11)
// var sinValor; // sin inicializador: NO
// var nulo = null; // no hay nada que inferir: NO
// var array = { 1, 2, 3 }; // inicializador de array: NOCuándo usarlo y cuándo no:
// BIEN: el tipo es evidente por el lado derecho
var catalogo = new ArrayList<Libro>();
var reserva = new Reserva("R-001", "Madrid-A", "Marta Ruiz", inicio, duracion);
var contador = new AtomicInteger(0);
// BIEN: el tipo explícito sería ilegible
var indice = new HashMap<String, Map<TipoMaterial, List<Prestamo>>>();
// MAL: el tipo no se deduce del nombre del método
var resultado = servicio.procesar(datos); // ¿qué es 'resultado'?
var x = calcular(); // ¿int? ¿double? ¿BigDecimal?
// MAL: se pierde la abstracción a la interfaz
var lista = new ArrayList<Libro>(); // el tipo es ArrayList
List<Libro> lista2 = new ArrayList<>(); // el tipo es List: mejor API
// PELIGROSO: el diamante vacío infiere Object
var vacia = new ArrayList<>(); // ArrayList<Object>La regla: usa var cuando el tipo sea evidente leyendo la línea. Si hay que ir a mirar la firma de un método para saber qué es la variable, escribe el tipo.
Y desde Java 11, var funciona en parámetros de lambda, con el único propósito de poder anotarlos:
switch de expresión y yield — retoma 02-03
switch de expresión y yield — retoma 02-03En 02-03 usaste el switch clásico con sus dos problemas: la caída entre casos (fall-through) por olvidar un break, y que es una sentencia, no una expresión, así que no puede devolver un valor sin una variable auxiliar.
// CLASICO: verboso y con trampas
String descripcion;
switch (gravedad) {
case ALTA:
descripcion = "Requiere intervención inmediata";
break;
case MEDIA:
descripcion = "Revisar esta semana";
break;
case BAJA:
descripcion = "Anotar y continuar";
break;
default:
throw new IllegalStateException("Gravedad desconocida: " + gravedad);
}Java 14 lo convirtió en expresión, con flechas y sin caída:
// EXPRESION: devuelve un valor, sin break, sin caida
String descripcion = switch (gravedad) {
case ALTA -> "Requiere intervención inmediata";
case MEDIA -> "Revisar esta semana";
case BAJA -> "Anotar y continuar";
};Cuatro ventajas concretas:
- Sin
break, porque no hay caída entre casos. - Es una expresión: se asigna, se devuelve, se pasa como argumento.
- Varias etiquetas por caso:
case SATURDAY, SUNDAY ->. - Exhaustividad comprobada: sobre un
enum, si faltan casos y no haydefault, no compila.
Esa última es la más valiosa. Si mañana alguien añade Gravedad.CRITICA, el compilador señalará todos los switch que hay que actualizar. Con el switch clásico y un default, el caso nuevo caería silenciosamente en el default.
yield para bloques con lógica:
double multa = switch (gravedad) {
case ALTA -> {
double base = dias * 0.50;
double recargo = dias > 30 ? 10.0 : 0.0;
yield Math.min(base + recargo, 50.0); // yield devuelve el valor del bloque
}
case MEDIA -> dias * 0.25;
case BAJA -> 0.0;
};yield es a un bloque de switch lo que return a un método. No se puede usar return dentro de un switch de expresión: saldría del método entero, que no es lo que se quiere.
Y el switch de expresión también funciona sobre String e int, con la diferencia de que ahí sí hace falta default, porque los valores posibles son infinitos:
int diasDePlazo = switch (tipo) {
case "LIBRO" -> 21;
case "REVISTA" -> 7;
case "DVD" -> 3;
default -> throw new IllegalArgumentException("Tipo desconocido: " + tipo);
};
- Bloques de texto
Java 15 incorporó las cadenas multilínea, y la diferencia en legibilidad es enorme.
// ANTES: ilegible, con escapes y concatenacion
String json = "{\n"
+ " \"isbn\": \"978-0000000001\",\n"
+ " \"titulo\": \"Java Efectivo\",\n"
+ " \"disponible\": true\n"
+ "}";
// AHORA
String json = """
{
"isbn": "978-0000000001",
"titulo": "Java Efectivo",
"disponible": true
}""";Reglas de sintaxis:
- Se abre con
"""seguido de un salto de línea obligatorio."""textono compila. - Se cierra con
""", en la misma línea que el último contenido o en una línea propia. - Las comillas dobles no necesitan escape.
La regla de indentación (la que importa). El compilador calcula el margen común mínimo de todas las líneas no vacías incluida la del cierre, y lo elimina:
public class Ejemplo {
public String consulta() {
String sql = """
SELECT p.referencia, p.isbn, m.titulo
FROM prestamos p
JOIN materiales m ON m.isbn = p.isbn
WHERE p.fecha_devolucion IS NULL
ORDER BY p.fecha_vencimiento
""";
return sql;
}
}La cadena resultante no lleva los 16 espacios de indentación del código Java, solo la sangría relativa de las líneas del SQL. Y aquí está el detalle que confunde: la posición del """ de cierre participa en el cálculo.
// El cierre a la izquierda del contenido: se conserva mas sangria
String a = """
línea
"""; // el margen comun es 2: " línea\n"
// El cierre alineado con el contenido: se elimina toda la sangria
String b = """
línea
"""; // el margen comun es 8: "línea\n"
// El cierre en la misma linea: sin salto final
String c = """
línea"""; // "línea" (sin \n al final)Secuencias de escape propias de los bloques de texto:
// \ al final de linea: UNE las lineas (sin salto)
String unaSolaLinea = """
Este texto está escrito en varias líneas \
del código fuente pero es una sola \
línea de resultado.""";
// \s: espacio que NO se elimina como sangria final
String conEspacios = """
columna1 \s
columna2 \s""";El uso estrella son SQL y JSON, que verás continuamente en los módulos 11 y 12:
public class ConsultasBiblioTech {
static final String PRESTAMOS_VENCIDOS = """
SELECT p.referencia,
p.isbn,
m.titulo,
p.empleado,
p.fecha_vencimiento,
CURRENT_DATE - p.fecha_vencimiento AS dias_retraso
FROM prestamos p
JOIN materiales m ON m.isbn = p.isbn
WHERE p.fecha_devolucion IS NULL
AND p.fecha_vencimiento < CURRENT_DATE
ORDER BY dias_retraso DESC
""";
static String peticionDeMetadatos(String isbn) {
return """
{
"jsonrpc": "2.0",
"method": "catalogo.buscar",
"params": { "isbn": "%s" },
"id": 1
}""".formatted(isbn); // formatted: como String.format
}
}formatted() (Java 15) es String.format como método de instancia, y encaja perfectamente con los bloques de texto.
record revisitado — retoma 04-07
record revisitado — retoma 04-07Los conociste en 04-07. Aquí conviene fijar qué son exactamente y qué se les puede hacer, porque en el apartado 15 serán la pieza clave del pattern matching.
Un record es un portador transparente de datos inmutables:
El compilador genera automáticamente:
- Campos
private finalpara cada componente. - Constructor canónico con todos los componentes.
- Métodos de acceso con el nombre del componente (
isbn(), nogetIsbn()). equalsyhashCodebasados en todos los componentes.toStringcon todos los componentes.
Compacto pero completo:
public record Prestamo(String referencia, String isbn, String empleado,
LocalDate fechaPrestamo, LocalDate fechaVencimiento) {
/** Constructor COMPACTO: valida y normaliza antes de asignar. */
public Prestamo {
Objects.requireNonNull(referencia, "referencia");
Objects.requireNonNull(isbn, "isbn");
if (fechaVencimiento.isBefore(fechaPrestamo)) {
throw new IllegalArgumentException("Vencimiento anterior al préstamo");
}
empleado = empleado.strip(); // se puede NORMALIZAR el parametro
}
/** Constructor adicional. */
public Prestamo(String referencia, String isbn, String empleado, LocalDate desde) {
this(referencia, isbn, empleado, desde, desde.plusDays(21));
}
/** Metodos derivados. */
public long diasDeRetraso(LocalDate hoy) {
return Math.max(0, ChronoUnit.DAYS.between(fechaVencimiento, hoy));
}
/** "Modificar" un record = crear otro. */
public Prestamo conVencimiento(LocalDate nuevo) {
return new Prestamo(referencia, isbn, empleado, fechaPrestamo, nuevo);
}
/** Metodos y campos estaticos SI se permiten. */
public static final Period PLAZO = Period.ofDays(21);
public static Prestamo nuevo(String ref, String isbn, String empleado, LocalDate desde) {
return new Prestamo(ref, isbn, empleado, desde, desde.plus(PLAZO));
}
}Limitaciones (que son decisiones de diseño, no carencias):
- No pueden extender ninguna clase (extienden
java.lang.Recordimplícitamente). Sí pueden implementar interfaces. - Son implícitamente
final: nadie puede extenderlos. - No pueden tener campos de instancia aparte de los componentes.
- Los componentes son
final: no hay setters.
Cuándo usar record y cuándo una clase:
Usa record |
Usa una clase |
|---|---|
| Portador de datos inmutable | Hay estado mutable |
| La identidad es el valor de los campos | La identidad es la referencia |
| DTO, valor de retorno, clave de mapa | Necesitas herencia |
| Resultado de un cálculo | Encapsulas la representación interna |
En BiblioTech: Ficha y ResumenSesion son records; Material es una clase abstracta porque hay jerarquía; Prestamo puede ser cualquiera de las dos según si su estado cambia.
instanceof con patrón — retoma 03-06
instanceof con patrón — retoma 03-06Definitivo en Java 16. Elimina el cast redundante tras un instanceof:
// ANTES: el cast repite lo que el instanceof ya comprobo
if (material instanceof Libro) {
Libro libro = (Libro) material;
System.out.println(libro.getPaginas());
}
// AHORA: la variable de patron se declara y asigna a la vez
if (material instanceof Libro libro) {
System.out.println(libro.getPaginas());
}El ámbito de la variable de patrón se calcula con lógica de flujo, lo que produce comportamientos muy útiles:
// Con && : la variable esta disponible a la derecha
if (material instanceof Libro libro && libro.getPaginas() > 400) {
System.out.println("Libro largo: " + libro.getTitulo());
}
// Con negacion: disponible DESPUES del if (salida temprana)
public String describir(Object objeto) {
if (!(objeto instanceof Material material)) {
return "No es un material";
}
// 'material' esta en ambito aqui, porque si no lo fuera ya habriamos vuelto
return material.getTitulo() + " (" + material.getTipo() + ")";
}
// Con || : NO esta disponible, y el compilador lo impide
// if (o instanceof Libro l || l.getPaginas() > 0) { } // NO COMPILAY equals queda especialmente limpio (03-09):
@Override
public boolean equals(Object otro) {
return otro instanceof Material material
&& isbn.equals(material.getIsbn());
}De seis líneas a dos, sin perder nada.
- Clases selladas:
sealed, permits, non-sealed
sealed, permits, non-sealedDefinitivas en Java 17. Resuelven un problema real que hasta ahora no tenía solución.
El problema. En BiblioTech, Material es abstracta con tres subclases: Libro, Revista y Dvd. Pero cualquiera puede crear una cuarta:
Y todos los switch y cadenas de instanceof que escribiste asumiendo tres tipos quedan incompletos, sin que el compilador diga nada.
Las alternativas antes de Java 17 eran malas: hacer el constructor package-private (frágil y no impide subclases en el mismo paquete) o simplemente confiar y documentar.
La solución:
package com.nexussoftware.bibliotech.dominio;
/**
* Jerarquia CERRADA: solo existen estos tres materiales.
* Ninguna otra clase puede extender Material, ni en este
* proyecto ni en ninguno.
*/
public sealed abstract class Material
permits Libro, Revista, Dvd {
private final String isbn;
private final String titulo;
protected Material(String isbn, String titulo) {
this.isbn = isbn;
this.titulo = titulo;
}
public String getIsbn() { return isbn; }
public String getTitulo() { return titulo; }
public abstract TipoMaterial getTipo();
}Cada subclase debe declarar su propia apertura, y esa es la parte que se olvida:
/** final: nadie extiende Libro. Es lo mas habitual. */
public final class Libro extends Material {
private final int paginas;
private final String autor;
// ...
}
/** sealed: Revista tiene a su vez una jerarquia cerrada. */
public sealed class Revista extends Material permits RevistaMensual, RevistaTrimestral {
// ...
}
/** non-sealed: Dvd vuelve a estar abierto a cualquiera. */
public non-sealed class Dvd extends Material {
// ...
}| Modificador | Significado |
|---|---|
final |
Nadie puede extenderla. El caso más común |
sealed ... permits |
Solo las clases listadas pueden extenderla |
non-sealed |
Cualquiera puede extenderla: reabre la jerarquía |
Las reglas que impone el compilador:
- Toda subclase de una clase
sealeddebe serfinal,sealedonon-sealed. No hay cuarta opción. - Las clases permitidas deben estar en el mismo módulo (o en el mismo paquete si no hay módulos).
- Deben extenderla realmente: listar una clase que no extienda es error de compilación.
permitsse puede omitir si las subclases están en el mismo fichero.
// Todo en un fichero: permits implicito
public sealed interface Evento {
record Prestado(String isbn, String empleado, LocalDate cuando) implements Evento { }
record Devuelto(String isbn, LocalDate cuando, boolean conRetraso) implements Evento { }
record Perdido(String isbn, String empleado, double importe) implements Evento { }
}Lo que ganas: exhaustividad. El compilador sabe que solo hay tres tipos, y por tanto puede verificar que los has cubierto todos:
// SIN default: el compilador comprueba que estan los tres
String etiqueta = switch (material) {
case Libro l -> "Libro de " + l.getPaginas() + " páginas";
case Revista r -> "Revista nº " + r.getNumero();
case Dvd d -> "DVD de " + d.getMinutos() + " minutos";
};Si mañana alguien añade Mapa a la lista de permits y no actualiza este switch:
error: the switch expression does not cover all possible input values
String etiqueta = switch (material) {
^El compilador te lleva de la mano por todos los sitios que hay que tocar. Eso es lo que hace que las jerarquías selladas se llamen "tipos algebraicos" o "tipos suma": el conjunto de posibilidades es cerrado y conocido.
Y combinadas con record, modelan estados de forma muy expresiva:
package com.nexussoftware.bibliotech.dominio;
import java.time.LocalDate;
/**
* Estado de un prestamo modelado como tipo suma.
* Cada estado lleva EXACTAMENTE los datos que tiene sentido
* que lleve, en lugar de una clase con campos que a veces son null.
*/
public sealed interface EstadoPrestamo {
record Activo(LocalDate vencimiento) implements EstadoPrestamo { }
record Vencido(LocalDate vencimiento, long diasDeRetraso, double multa)
implements EstadoPrestamo { }
record Devuelto(LocalDate cuando, boolean puntual) implements EstadoPrestamo { }
record Perdido(LocalDate declaradoEl, double importeReposicion)
implements EstadoPrestamo { }
}Compara con la alternativa clásica: una clase Prestamo con boolean devuelto, LocalDate fechaDevolucion (null si no se ha devuelto), double multa (0 si no aplica) y boolean perdido. Combinaciones imposibles representables: devuelto y perdido a la vez, multa sin vencimiento. Con el tipo sellado, esos estados no se pueden construir.
- Pattern matching para
switch y patrones de registro
switch y patrones de registroDefinitivos en Java 21, cierran el círculo.
Patrones de tipo en switch
// ANTES: cadena de instanceof, con cast en cada rama
public String describir(Object objeto) {
if (objeto instanceof Libro) {
Libro l = (Libro) objeto;
return "Libro: " + l.getTitulo() + " (" + l.getPaginas() + " págs.)";
} else if (objeto instanceof Revista) {
Revista r = (Revista) objeto;
return "Revista: " + r.getTitulo() + " nº " + r.getNumero();
} else if (objeto instanceof Dvd) {
Dvd d = (Dvd) objeto;
return "DVD: " + d.getTitulo() + " (" + d.getMinutos() + " min)";
} else if (objeto == null) {
return "(nada)";
}
return "Desconocido";
}
// AHORA
public String describir(Object objeto) {
return switch (objeto) {
case null -> "(nada)";
case Libro l -> "Libro: " + l.getTitulo() + " (" + l.getPaginas() + " págs.)";
case Revista r -> "Revista: " + r.getTitulo() + " nº " + r.getNumero();
case Dvd d -> "DVD: " + d.getTitulo() + " (" + d.getMinutos() + " min)";
default -> "Desconocido: " + objeto.getClass().getSimpleName();
};
}case null es una novedad importante. El switch clásico lanzaba NullPointerException con un valor nulo; ahora se puede tratar como un caso más. Si no se declara case null, sigue lanzando NullPointerException por compatibilidad.
Guardas con when
public String clasificar(Material material) {
return switch (material) {
case Libro l when l.getPaginas() > 500 -> "Libro extenso";
case Libro l when l.getPaginas() > 200 -> "Libro estándar";
case Libro l -> "Libro breve";
case Revista r when r.esEspecial() -> "Número especial";
case Revista r -> "Revista ordinaria";
case Dvd d -> "Material audiovisual";
};
}El orden importa: los casos se evalúan de arriba abajo, y el más específico va primero. El compilador impide poner un caso inalcanzable:
Patrones de registro (record patterns)
La pieza más potente: descomponer un record directamente en el patrón.
public sealed interface Evento {
record Prestado(String isbn, String empleado, LocalDate cuando) implements Evento { }
record Devuelto(String isbn, LocalDate cuando, boolean conRetraso) implements Evento { }
record Perdido(String isbn, String empleado, double importe) implements Evento { }
}public String registrar(Evento evento) {
return switch (evento) {
// Se DESCOMPONE el record: isbn, empleado y cuando salen directamente
case Evento.Prestado(String isbn, String empleado, LocalDate cuando) ->
String.format("%s prestó %s el %s", empleado, isbn, cuando);
// Con guarda sobre un componente descompuesto
case Evento.Devuelto(String isbn, LocalDate cuando, boolean retraso) when retraso ->
String.format("%s devuelto el %s CON RETRASO", isbn, cuando);
case Evento.Devuelto(String isbn, LocalDate cuando, boolean retraso) ->
String.format("%s devuelto puntualmente el %s", isbn, cuando);
case Evento.Perdido(String isbn, String empleado, double importe) ->
String.format("%s declaró perdido %s (reposición: %.2f €)",
empleado, isbn, importe);
};
// SIN default: la interfaz es sealed y estan los tres casos
}Y con var para no repetir tipos:
case Evento.Prestado(var isbn, var empleado, var cuando) ->
empleado + " prestó " + isbn + " el " + cuando;Los patrones se anidan, que es donde el mecanismo se vuelve realmente expresivo:
public record Punto(int x, int y) { }
public record Linea(Punto inicio, Punto fin) { }
public String describir(Object o) {
return switch (o) {
// Descomposicion ANIDADA en dos niveles
case Linea(Punto(var x1, var y1), Punto(var x2, var y2)) when x1 == x2 ->
"Línea vertical en x=" + x1;
case Linea(Punto(var x1, var y1), Punto(var x2, var y2)) when y1 == y2 ->
"Línea horizontal en y=" + y1;
case Linea(Punto p1, Punto p2) ->
"Línea de " + p1 + " a " + p2;
case Punto(int x, int y) ->
"Punto (" + x + ", " + y + ")";
default -> "Otra cosa";
};
}Y también funciona con instanceof:
if (evento instanceof Evento.Prestado(String isbn, String empleado, LocalDate cuando)) {
System.out.println(empleado + " -> " + isbn + " (" + cuando + ")");
}
- BiblioTech: refactorización de la jerarquía
Aplicamos todo a la vez, con el antes y el después.
Antes
package com.nexussoftware.bibliotech.servicio;
import com.nexussoftware.bibliotech.dominio.*;
/**
* VERSION ANTIGUA: jerarquia abierta, casts manuales,
* cadenas de if-else y un default que oculta los casos nuevos.
*/
public class CalculadoraPlazosAntigua {
public int diasDePlazo(Material material) {
if (material instanceof Libro) {
Libro libro = (Libro) material;
if (libro.getPaginas() > 500) {
return 30;
} else if (libro.getPaginas() > 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;
}
// Y si alguien anade un tipo nuevo, cae AQUI en silencio
throw new IllegalArgumentException("Tipo desconocido: " + material.getClass());
}
public String etiquetaDeEstante(Material material) {
String prefijo;
if (material instanceof Libro) {
prefijo = "L";
} else if (material instanceof Revista) {
prefijo = "R";
} else if (material instanceof Dvd) {
prefijo = "D";
} else {
prefijo = "X";
}
return prefijo + "-" + material.getIsbn().substring(4, 8);
}
public double valorDeReposicion(Material material) {
if (material instanceof Libro) {
return ((Libro) material).getPaginas() * 0.08;
} else if (material instanceof Revista) {
return 12.0;
} else if (material instanceof Dvd) {
return ((Dvd) material).getMinutos() * 0.15;
}
return 0.0;
}
}Después
package com.nexussoftware.bibliotech.dominio;
/** Jerarquia SELLADA: el compilador sabe que solo existen tres materiales. */
public sealed abstract class Material permits Libro, Revista, Dvd {
private final String isbn;
private final String titulo;
private final double valoracion;
protected Material(String isbn, String titulo, double valoracion) {
this.isbn = isbn;
this.titulo = titulo;
this.valoracion = valoracion;
}
public String getIsbn() { return isbn; }
public String getTitulo() { return titulo; }
public double getValoracion() { return valoracion; }
}public final class Libro extends Material {
private final int paginas;
private final String autor;
public Libro(String isbn, String titulo, String autor, int paginas, double valoracion) {
super(isbn, titulo, valoracion);
this.autor = autor;
this.paginas = paginas;
}
public int getPaginas() { return paginas; }
public String getAutor() { return autor; }
}
public final class Revista extends Material {
private final int numero;
private final boolean especial;
public Revista(String isbn, String titulo, int numero, boolean especial, double valoracion) {
super(isbn, titulo, valoracion);
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 minutos;
private final String director;
public Dvd(String isbn, String titulo, String director, int minutos, double valoracion) {
super(isbn, titulo, valoracion);
this.director = director;
this.minutos = minutos;
}
public int getMinutos() { return minutos; }
public String getDirector() { return director; }
}package com.nexussoftware.bibliotech.servicio;
import com.nexussoftware.bibliotech.dominio.*;
/**
* VERSION MODERNA: switch exhaustivo sin default,
* sin casts, con guardas. Si se anade un cuarto material,
* ESTOS TRES METODOS DEJAN DE COMPILAR y el compilador
* te lleva a cada uno.
*/
public class CalculadoraPlazos {
public int diasDePlazo(Material material) {
return switch (material) {
case Libro l when l.getPaginas() > 500 -> 30;
case Libro l when l.getPaginas() > 200 -> 21;
case Libro l -> 14;
case Revista r when r.esEspecial() -> 14;
case Revista r -> 7;
case Dvd d -> 3;
};
}
public String etiquetaDeEstante(Material material) {
String prefijo = switch (material) {
case Libro l -> "L";
case Revista r -> "R";
case Dvd d -> "D";
};
return prefijo + "-" + material.getIsbn().substring(4, 8);
}
public double valorDeReposicion(Material material) {
return switch (material) {
case Libro l -> l.getPaginas() * 0.08;
case Revista r -> r.esEspecial() ? 25.0 : 12.0;
case Dvd d -> d.getMinutos() * 0.15;
};
}
/** Con bloques y yield cuando hay logica. */
public String fichaCompleta(Material material) {
return switch (material) {
case Libro l -> {
String extension = l.getPaginas() > 400 ? "extenso" : "estándar";
yield """
LIBRO
Título: %s
Autor: %s
Páginas: %d (%s)
Plazo: %d días
Reposición: %.2f €"""
.formatted(l.getTitulo(), l.getAutor(), l.getPaginas(),
extension, diasDePlazo(l), valorDeReposicion(l));
}
case Revista r -> """
REVISTA
Título: %s
Número: %d%s
Plazo: %d días"""
.formatted(r.getTitulo(), r.getNumero(),
r.esEspecial() ? " (ESPECIAL)" : "", diasDePlazo(r));
case Dvd d -> """
DVD
Título: %s
Director: %s
Duración: %d min
Plazo: %d días"""
.formatted(d.getTitulo(), d.getDirector(), d.getMinutos(), diasDePlazo(d));
};
}
}Y el estado del préstamo con patrones de registro:
package com.nexussoftware.bibliotech.servicio;
import com.nexussoftware.bibliotech.dominio.EstadoPrestamo;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public class DescriptorDeEstado {
private static final DateTimeFormatter FECHA =
DateTimeFormatter.ofPattern("d 'de' MMMM", new Locale("es", "ES"));
public String describir(EstadoPrestamo estado) {
return switch (estado) {
case EstadoPrestamo.Activo(var vencimiento) ->
"Activo, vence el " + vencimiento.format(FECHA);
case EstadoPrestamo.Vencido(var vencimiento, var dias, var multa) when multa == 0 ->
"Vencido hace " + dias + " días (todavía en período de cortesía)";
case EstadoPrestamo.Vencido(var vencimiento, var dias, var multa) ->
String.format("VENCIDO hace %d días -- multa acumulada: %.2f €", dias, multa);
case EstadoPrestamo.Devuelto(var cuando, var puntual) ->
"Devuelto el " + cuando.format(FECHA) + (puntual ? " puntualmente" : " con retraso");
case EstadoPrestamo.Perdido(var declarado, var importe) ->
String.format("PERDIDO desde el %s -- reposición: %.2f €",
declarado.format(FECHA), importe);
};
}
/** Prioridad de gestion: un switch que devuelve un int. */
public int prioridad(EstadoPrestamo estado) {
return switch (estado) {
case EstadoPrestamo.Perdido p -> 100;
case EstadoPrestamo.Vencido(var v, var dias, var m) when dias > 30 -> 90;
case EstadoPrestamo.Vencido v -> 50;
case EstadoPrestamo.Activo a -> 10;
case EstadoPrestamo.Devuelto d -> 0;
};
}
}List<EstadoPrestamo> estados = List.of(
new EstadoPrestamo.Activo(LocalDate.of(2026, 8, 26)),
new EstadoPrestamo.Vencido(LocalDate.of(2026, 8, 3), 2, 0.0),
new EstadoPrestamo.Vencido(LocalDate.of(2026, 5, 22), 75, 18.25),
new EstadoPrestamo.Devuelto(LocalDate.of(2026, 8, 1), true),
new EstadoPrestamo.Perdido(LocalDate.of(2026, 7, 15), 42.50));
DescriptorDeEstado descriptor = new DescriptorDeEstado();
estados.stream()
.sorted(Comparator.comparingInt(descriptor::prioridad).reversed())
.forEach(e -> System.out.printf(" [%3d] %s%n",
descriptor.prioridad(e), descriptor.describir(e))); [100] PERDIDO desde el 15 de julio -- reposición: 42,50 €
[ 90] VENCIDO hace 75 días -- multa acumulada: 18,25 €
[ 50] Vencido hace 2 días (todavía en período de cortesía)
[ 10] Activo, vence el 26 de agosto
[ 0] Devuelto el 1 de agosto puntualmenteLa comparación en números: 45 líneas de if-else con casts frente a 20 líneas de switch exhaustivo. Pero lo importante no es la longitud: es que si alguien añade un cuarto material o un quinto estado, el código deja de compilar y el compilador señala cada sitio que hay que actualizar. La versión antigua habría seguido compilando y fallando en ejecución.
- Hilos virtuales: qué son
Y llegamos a lo que llevas esperando desde 09-03.
El problema, recapitulado. En 09-03 escribiste ServidorCatalogo con el modelo "un hilo por conexión", y descubriste que no escala: cada hilo de plataforma consume alrededor de 1 MB de pila y su creación cuesta cerca de un milisegundo. Diez mil conexiones simultáneas serían diez gigabytes de pila. Por eso tuviste que acotar el pool a un número modesto de hilos, y por eso el servidor rechaza clientes cuando se satura.
Ese compromiso —hilos baratos o código legible, elige uno— ha dominado la programación de servidores durante veinte años. La alternativa era la programación asíncrona (CompletableFuture, 08-07), que escala pero convierte un flujo lineal en una maraña de retrollamadas donde el depurador no sirve y las trazas de pila no dicen nada.
Los hilos virtuales (Java 21, Proyecto Loom) eliminan el compromiso.
Un hilo virtual es un hilo gestionado por la JVM, no por el sistema operativo. Se ejecuta sobre un pequeño conjunto de hilos de plataforma llamados portadores (carrier threads). La clave es el comportamiento al bloquearse:
graph TD
A["10.000 hilos virtuales"] --> B["Planificador de la JVM<br/>ForkJoinPool"]
B --> C["Hilo portador 1"]
B --> D["Hilo portador 2"]
B --> E["Hilo portador N<br/>(N = nucleos)"]
C --> F["Sistema operativo"]
D --> F
E --> F
G["Un hilo virtual se bloquea<br/>en E/S"] -->|"se DESMONTA del portador"| H["Su pila se guarda en el heap"]
H -->|"el portador queda LIBRE<br/>para otro hilo virtual"| B
I["Llegan los datos"] -->|"se REMONTA en un portador"| B
Cuando un hilo de plataforma se bloquea, el hilo del sistema operativo queda parado. Cuando un hilo virtual se bloquea, se desmonta del portador: su pila se guarda en el heap y el portador pasa a ejecutar otro hilo virtual. Al llegar los datos, se remonta y continúa exactamente donde estaba.
El resultado: puedes bloquear alegremente, porque bloquear un hilo virtual no bloquea ningún hilo del sistema operativo. El código secuencial legible vuelve a escalar.
// Crear uno suelto
Thread virtual = Thread.ofVirtual().name("worker-1").start(() -> {
System.out.println("Soy un hilo virtual: " + Thread.currentThread());
});
virtual.join();
// Sin arrancar
Thread sinArrancar = Thread.ofVirtual().unstarted(tarea);
// Fabrica de hilos virtuales
ThreadFactory fabrica = Thread.ofVirtual().name("bibliotech-", 0).factory();
// EL MAS IMPORTANTE: executor con un hilo virtual POR TAREA
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return "listo";
});
}
} // close() espera a que terminen todas: el executor es AutoCloseableFíjate en dos detalles: ExecutorService es AutoCloseable desde Java 19, así que el try-with-resources de 06-06 sustituye al shutdown() + awaitTermination() de 08-05. Y Thread.sleep(Duration) acepta un Duration de 10-05.
La demostración
package com.nexussoftware.bibliotech;
import java.time.Duration;
import java.util.concurrent.*;
import java.util.stream.IntStream;
public class DemostracionHilosVirtuales {
private static final int TAREAS = 10_000;
public static void main(String[] args) throws Exception {
System.out.println("Tareas: " + TAREAS + ", cada una bloqueada 1 segundo");
System.out.println("Núcleos: " + Runtime.getRuntime().availableProcessors());
System.out.println();
medir("Pool de plataforma (200 hilos)",
Executors.newFixedThreadPool(200));
medir("Un hilo VIRTUAL por tarea ",
Executors.newVirtualThreadPerTaskExecutor());
// Y esto NO se puede hacer con hilos de plataforma
System.out.println();
System.out.println("--- 1.000.000 de hilos virtuales ---");
long inicio = 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 millón de tareas en %d ms%n",
System.currentTimeMillis() - inicio);
}
private static void medir(String etiqueta, ExecutorService executor) throws Exception {
long inicio = System.currentTimeMillis();
try (executor) {
for (int i = 0; i < TAREAS; i++) {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1)); // simula espera de E/S
return null;
});
}
}
System.out.printf("%s -> %6d ms%n", etiqueta, System.currentTimeMillis() - inicio);
}
}Tareas: 10000, cada una bloqueada 1 segundo
Núcleos: 8
Pool de plataforma (200 hilos) -> 50238 ms
Un hilo VIRTUAL por tarea -> 1104 ms
--- 1.000.000 de hilos virtuales ---
Un millón de tareas en 2871 msCincuenta segundos frente a uno. El pool de 200 hilos procesa 200 tareas simultáneas y las 10.000 tardan 50 rondas de un segundo. Los hilos virtuales ejecutan las 10.000 a la vez, y el total es el tiempo de una sola tarea más el arranque.
Y un millón de hilos virtuales en menos de tres segundos. Un millón de hilos de plataforma serían aproximadamente un terabyte de pila: la JVM no arrancaría.
- Hilos de plataforma frente a hilos virtuales
| Aspecto | Hilo de plataforma | Hilo virtual |
|---|---|---|
| Implementación | Envoltorio de un hilo del SO | Objeto gestionado por la JVM |
| Coste de creación | ~1 ms | ~1 µs (mil veces menos) |
| Memoria (pila) | ~1 MB reservado | Cientos de bytes, crece en el heap |
| Número viable | Miles | Millones |
| Planificación | Sistema operativo, expropiativa | JVM, cooperativa en puntos de bloqueo |
| Al bloquearse | El hilo del SO se para | Se desmonta, libera el portador |
| Agrupar en pool | Imprescindible | Contraproducente: son desechables |
ThreadLocal |
Funciona, con cuidado | Funciona, pero desaconsejado con millones |
synchronized |
Sin problemas | Puede provocar pinning |
| Prioridades | Sí | No: se ignoran |
| Nombre por defecto | Thread-N |
Vacío (hay que darlo explícitamente) |
| Bueno para | Cálculo intensivo | E/S: red, ficheros, base de datos |
Aparecen en jstack |
Sí | Solo con volcado de hilos extendido |
Las cuatro reglas prácticas:
1. No agrupes hilos virtuales en un pool. Los pools existen porque crear un hilo de plataforma es caro. Crear un hilo virtual es casi gratis. Executors.newFixedThreadPool(200) con hilos virtuales sería absurdo: reintroduce el límite que los hilos virtuales eliminan.
// MAL: contradice el propósito
ExecutorService mal = Executors.newFixedThreadPool(200, Thread.ofVirtual().factory());
// BIEN: uno por tarea
ExecutorService bien = Executors.newVirtualThreadPerTaskExecutor();2. Un hilo virtual por tarea, siempre. No los reutilices ni los guardes.
3. Para limitar la concurrencia, usa un Semaphore, no el tamaño del pool. Si el servicio externo solo aguanta 50 peticiones simultáneas, eso es una restricción del recurso, no de los hilos:
Semaphore limite = new Semaphore(50);
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (String isbn : isbns) {
executor.submit(() -> {
limite.acquire();
try {
return clienteMetadatos.consultar(isbn);
} finally {
limite.release();
}
});
}
}4. No aceleran el cálculo. Ver apartado 20.
- BiblioTech: el
ServidorCatalogo con hilos virtuales
ServidorCatalogo con hilos virtualesLa refactorización que 09-03 dejó pendiente.
Antes (Java 17, pool acotado)
package com.nexussoftware.bibliotech.red;
import java.io.IOException;
import java.net.*;
import java.util.concurrent.*;
import java.util.logging.Logger;
/**
* VERSION DEL MODULO 9: pool acotado.
*
* El pool limita a 50 clientes simultaneos. Con 51 conexiones,
* la 51 espera en la cola; si la cola se llena, se RECHAZA.
* Y no se puede subir mucho: cada hilo son ~1 MB de pila.
*/
public class ServidorCatalogoConPool {
private static final Logger LOG = Logger.getLogger(ServidorCatalogoConPool.class.getName());
private static final int PUERTO = 9090;
private static final int MAX_CLIENTES = 50; // <-- el limite
private static final int COLA = 100;
private final ExecutorService pool = new ThreadPoolExecutor(
MAX_CLIENTES, MAX_CLIENTES,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(COLA),
new ThreadPoolExecutor.CallerRunsPolicy());
private volatile boolean activo = true;
public void arrancar() throws IOException {
try (ServerSocket servidor = new ServerSocket(PUERTO)) {
servidor.setSoTimeout(1000);
LOG.info("Servidor en el puerto " + PUERTO + " (máx. " + MAX_CLIENTES + " clientes)");
while (activo) {
try {
Socket cliente = servidor.accept();
pool.submit(() -> atender(cliente));
} catch (SocketTimeoutException e) {
// ciclo de comprobacion de 'activo'
}
}
} finally {
pool.shutdown();
}
}
private void atender(Socket cliente) { /* protocolo BTCP/1 */ }
}Después (Java 21, hilos virtuales)
package com.nexussoftware.bibliotech.red;
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;
/**
* VERSION CON HILOS VIRTUALES.
*
* Un hilo virtual por conexion, sin pool y sin limite artificial.
* El codigo de atender() es SECUENCIAL y BLOQUEANTE, que es como
* hay que escribirlo, y aun asi escala a decenas de miles de clientes.
*/
public class ServidorCatalogo implements AutoCloseable {
private static final Logger LOG = Logger.getLogger(ServidorCatalogo.class.getName());
private static final int PUERTO = 9090;
private static final Duration INACTIVIDAD = Duration.ofSeconds(30);
private static final int MAX_LINEA = 4096;
/**
* Un hilo VIRTUAL por tarea. Ya no hay tamaño de pool que ajustar,
* ni cola, ni politica de rechazo.
*/
private final ExecutorService atendedores = Executors.newVirtualThreadPerTaskExecutor();
private final AtomicLong conexionesTotales = new AtomicLong();
private final AtomicLong conexionesActivas = new AtomicLong();
private volatile boolean activo = true;
private ServerSocket servidor;
public void arrancar() throws IOException {
servidor = new ServerSocket(PUERTO);
servidor.setSoTimeout(1000);
LOG.info(() -> "ServidorCatalogo escuchando en el puerto " + PUERTO
+ " (un hilo virtual por conexión, sin límite de pool)");
// Hilo virtual dedicado a informar del estado
Thread.ofVirtual().name("monitor").start(this::monitorizar);
while (activo) {
try {
Socket cliente = servidor.accept();
// Un hilo virtual POR CONEXION. Crearlo cuesta microsegundos.
atendedores.submit(() -> atender(cliente));
} catch (SocketTimeoutException e) {
// ciclo normal
} catch (IOException e) {
if (activo) {
LOG.log(Level.WARNING, "Error al aceptar conexión", e);
}
}
}
}
/**
* Codigo SECUENCIAL y BLOQUEANTE: exactamente el mismo de 09-03.
* readLine() bloquea, y no pasa nada: el hilo virtual se desmonta.
*/
private void atender(Socket cliente) {
long numero = conexionesTotales.incrementAndGet();
conexionesActivas.incrementAndGet();
// Nombrar el hilo hace legibles las trazas y los volcados
Thread.currentThread().setName("cliente-" + numero);
try (cliente;
var entrada = new BufferedReader(new InputStreamReader(
cliente.getInputStream(), StandardCharsets.UTF_8));
var salida = new PrintWriter(new OutputStreamWriter(
cliente.getOutputStream(), StandardCharsets.UTF_8), true)) {
cliente.setSoTimeout((int) INACTIVIDAD.toMillis());
salida.println("BTCP/1 BIENVENIDO BiblioTech");
String linea;
while ((linea = leerLineaAcotada(entrada)) != null) {
String respuesta = switch (linea.split(" ", 2)[0].toUpperCase()) {
case "BUSCAR" -> buscar(linea);
case "PRESTAR" -> prestar(linea);
case "ESTADO" -> "OK ACTIVAS=" + conexionesActivas.get()
+ " TOTALES=" + conexionesTotales.get();
case "ADIOS" -> "OK HASTA_LUEGO";
default -> "ERROR COMANDO_DESCONOCIDO";
};
salida.println(respuesta);
if (respuesta.startsWith("OK HASTA_LUEGO")) {
break;
}
}
} catch (SocketTimeoutException e) {
LOG.fine(() -> "Cliente " + numero + " expulsado por inactividad");
} catch (IOException e) {
LOG.log(Level.FINE, e, () -> "Error con el cliente " + numero);
} finally {
conexionesActivas.decrementAndGet();
}
}
private String leerLineaAcotada(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_LINEA) {
throw new IOException("Línea demasiado larga: posible ataque");
}
sb.append((char) c);
}
return sb.isEmpty() ? null : sb.toString().strip();
}
private void monitorizar() {
while (activo) {
try {
Thread.sleep(Duration.ofSeconds(10));
LOG.info(() -> String.format("Conexiones: %d activas, %d totales",
conexionesActivas.get(), conexionesTotales.get()));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
private String buscar(String comando) { return "OK ..."; }
private String prestar(String comando) { return "OK ..."; }
@Override
public void close() throws IOException {
activo = false;
if (servidor != null) {
servidor.close();
}
atendedores.close(); // espera a que terminen los clientes en curso
LOG.info("Servidor detenido");
}
public static void main(String[] args) throws IOException {
try (var servidor = new ServidorCatalogo()) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try { servidor.close(); } catch (IOException ignored) { }
}));
servidor.arrancar();
}
}
}Qué cambió, exactamente
| Aspecto | Con pool (09-03) | Con hilos virtuales |
|---|---|---|
| Concurrencia máxima | 50 clientes (tamaño del pool) | Decenas de miles |
| Configuración | Tamaño de pool, cola, política de rechazo | Ninguna |
| Al saturarse | Encola y luego rechaza | Sigue aceptando |
| Memoria por cliente | ~1 MB | ~1 KB |
Código de atender() |
Idéntico | Idéntico |
| Trazas de pila | Legibles | Legibles |
| Depuración paso a paso | Funciona | Funciona |
Y esa es la observación central: el código de atender() no ha cambiado. Sigue siendo secuencial, bloqueante y legible. Con CompletableFuture (08-07) habría que convertirlo en una cadena de thenCompose sin variables locales entre pasos, sin try-with-resources que abarque toda la conversación, y con trazas de pila que no dicen dónde estaba el flujo.
Los hilos virtuales devuelven el código sencillo sin pagar en escalabilidad. Ese es todo su valor.
Nota sobre el módulo 8: todo lo que aprendiste sigue siendo necesario. Las condiciones de carrera, volatile, synchronized, los interbloqueos, las colecciones concurrentes y las variables atómicas se aplican exactamente igual a los hilos virtuales. Un hilo virtual es un hilo: dos hilos virtuales tocando el mismo ArrayList lo corrompen igual. Lo que cambia es cuántos puedes tener, no cómo se comportan.
- Pinning,
synchronized y lo que no resuelven
synchronized y lo que no resuelvenEl pinning
El mecanismo de los hilos virtuales exige que se puedan desmontar del portador al bloquearse. Hay dos situaciones donde no pueden:
- Dentro de un bloque o método
synchronized. - Durante una llamada a código nativo (JNI).
Cuando eso ocurre, el hilo virtual queda clavado (pinned) al portador. Si además se bloquea, bloquea el hilo portador, que es un hilo del sistema operativo. Con suficientes hilos clavados, todos los portadores quedan ocupados y el sistema se paraliza.
// PROBLEMA: bloqueo dentro de synchronized
public class CacheProblematica {
private final Map<String, Ficha> cache = new HashMap<>();
public synchronized Ficha obtener(String isbn) { // synchronized
return cache.computeIfAbsent(isbn, clave -> {
return clienteMetadatos.consultar(clave); // BLOQUEA en red -> PINNING
});
}
}// SOLUCION: ReentrantLock, que SI permite desmontar
public class CacheCorrecta {
private final Map<String, Ficha> cache = new ConcurrentHashMap<>();
private final ReentrantLock cerrojo = new ReentrantLock();
public Ficha obtener(String isbn) {
Ficha existente = cache.get(isbn);
if (existente != null) {
return existente;
}
cerrojo.lock(); // ReentrantLock (08-04): sin pinning
try {
return cache.computeIfAbsent(isbn, clienteMetadatos::consultar);
} finally {
cerrojo.unlock();
}
}
}La regla para hilos virtuales: en código que se bloquea, prefiere ReentrantLock a synchronized. Los cerrojos de java.util.concurrent.locks están diseñados para desmontar el hilo virtual.
Se puede diagnosticar con una opción de la JVM:
Thread[#31,ForkJoinPool-1-worker-1,5,CarrierThreads]
com.nexussoftware.bibliotech.servicio.CacheProblematica.obtener(CacheProblematica.java:12)
<== monitors:1Nota importante sobre versiones: en Java 24 (JEP 491) se eliminó el pinning por synchronized en la mayoría de los casos. Si trabajas con Java 24 o posterior, el problema prácticamente desaparece. En Java 21 —la LTS de referencia hoy— sigue siendo una consideración real.
Qué NO resuelven los hilos virtuales
Esto es tan importante como lo que sí resuelven.
1. No aceleran el cálculo. Un hilo virtual usa un núcleo igual que uno de plataforma. Si tu tarea es multiplicar matrices, tener un millón de hilos virtuales sobre ocho núcleos no la hace más rápida; solo añade cambios de contexto.
// INUTIL: la tarea no se bloquea nunca
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> calcularHashCostoso(datos)); // puro CPU
}
}Para cálculo, sigue usando un pool de plataforma dimensionado a los núcleos, o parallelStream() (10-04).
2. No eliminan los problemas de concurrencia. Condiciones de carrera, interbloqueos, visibilidad de memoria: todo el módulo 8 sigue vigente. De hecho, con un millón de hilos los problemas de contención se manifiestan antes.
3. No arreglan un cuello de botella externo. Diez mil hilos virtuales golpeando una base de datos con un pool de veinte conexiones no dan diez mil consultas simultáneas: dan veinte, y 9.980 esperando. Los recursos externos siguen necesitando limitación explícita, con Semaphore.
4. ThreadLocal se desaconseja. Con millones de hilos, cada ThreadLocal se multiplica por un millón. Además, la costumbre de reutilizar ThreadLocal en un pool pierde sentido cuando cada tarea tiene su hilo. Para eso llega ScopedValue (apartado 21).
5. No sustituyen a CompletableFuture en todo. Para composición de flujos asíncronos con thenCombine y allOf (08-07), o para reactividad con contrapresión, CompletableFuture y las bibliotecas reactivas siguen teniendo su sitio. Los hilos virtuales brillan en el modelo "una tarea, un hilo, código secuencial".
| Escenario | Herramienta |
|---|---|
| Servidor con muchas conexiones bloqueantes | Hilos virtuales |
| Muchas llamadas HTTP/BD independientes | Hilos virtuales + Semaphore |
| Cálculo intensivo | Pool de plataforma, parallelStream |
| Composición asíncrona con dependencias | CompletableFuture (08-07) |
| Flujos con contrapresión | Bibliotecas reactivas |
- Lo que viene: concurrencia estructurada y
ScopedValue
ScopedValueDos características que en Java 21 están en vista previa y que completan el modelo de Loom. No las uses en producción todavía, pero conviene conocerlas.
Concurrencia estructurada
El problema: cuando lanzas varias tareas y esperas sus resultados, gestionar los fallos y las cancelaciones a mano es propenso a errores. Si una falla, ¿quién cancela las otras? Si el hilo padre se interrumpe, ¿quién limpia?
// Con StructuredTaskScope: el ambito CONTIENE a las subtareas
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<Ficha> ficha = scope.fork(() -> clienteMetadatos.consultar(isbn));
Subtask<byte[]> portada = scope.fork(() -> clientePortadas.descargar(isbn));
Subtask<List<String>> resenas = scope.fork(() -> clienteResenas.buscar(isbn));
scope.join(); // espera a las tres
scope.throwIfFailed(); // si alguna fallo, lanza y CANCELA las demas
return new FichaCompleta(ficha.get(), portada.get(), resenas.get());
} // close() garantiza que ninguna subtarea sigue vivaLas garantías: las subtareas no sobreviven al ámbito, un fallo cancela a las hermanas, la interrupción se propaga hacia abajo, y las trazas de pila reflejan la relación padre-hijo. Es a la concurrencia lo que el try-with-resources es a los recursos.
ScopedValue
El sustituto de ThreadLocal para el mundo de los hilos virtuales:
public class ContextoBiblioTech {
// Inmutable y con ambito acotado, a diferencia de ThreadLocal
public static final ScopedValue<String> EMPLEADO = ScopedValue.newInstance();
public void atenderPeticion(String empleado, Runnable tarea) {
ScopedValue.where(EMPLEADO, empleado).run(tarea);
// Fuera de aqui, EMPLEADO no esta ligado
}
public void enAlgunSitioProfundo() {
if (EMPLEADO.isBound()) {
LOG.info("Operación de " + EMPLEADO.get());
}
}
}ThreadLocal |
ScopedValue |
|
|---|---|---|
| Mutabilidad | Mutable con set() |
Inmutable |
| Ámbito | Vida del hilo | Bloque delimitado |
| Limpieza | Manual (remove()), origen de fugas |
Automática |
| Herencia a subtareas | InheritableThreadLocal, con copia |
Automática y sin copiar |
| Con millones de hilos | Problemático | Diseñado para ello |
- Herramientas:
jshell, jpackage y compañía
jshell, jpackage y compañíaUna nota rápida sobre las herramientas nuevas del JDK.
jshell (Java 9) — el REPL. Permite probar código sin crear una clase ni un main:
jshell> var catalogo = new java.util.ArrayList<String>()
catalogo ==> []
jshell> catalogo.add("Java Efectivo")
$2 ==> true
jshell> catalogo.stream().map(String::toUpperCase).toList()
$3 ==> [JAVA EFECTIVO]
jshell> java.time.LocalDate.now().plusDays(21)
$4 ==> 2026-08-26
jshell> /exitInsustituible para comprobar el comportamiento exacto de una API sin montar un proyecto. Y muy útil para verificar en el momento las dudas de esta lección.
Ejecutar un .java sin compilar (Java 11):
Compila en memoria y ejecuta. Convierte a Java en algo utilizable para guiones.
jpackage (Java 14) — instaladores nativos:
jpackage --name BiblioTech \
--input dist/ \
--main-jar bibliotech.jar \
--main-class com.nexussoftware.bibliotech.BiblioTechApp \
--runtime-image runtime-bibliotech \
--type deb \
--app-version 3.2Produce un .deb, .rpm, .msi, .exe, .dmg o .pkg con la aplicación y su runtime dentro. El usuario final no necesita Java instalado.
jwebserver (Java 18) — servidor de ficheros estáticos para pruebas:
jdeps — analiza dependencias (lo usaste con jlink), y detecta usos de API interna:
jcmd, jfr — diagnóstico y grabación de vuelo. Todo esto es 10-07.
- Cómo mantenerse al día y cómo decidir la migración
Las fuentes
| Fuente | Qué aporta |
|---|---|
| JEPs (openjdk.org/jeps) | La propuesta técnica de cada característica, con motivación y alternativas descartadas. La fuente primaria |
| Notas de versión del JDK | Cambios incompatibles, obsolescencias, eliminaciones |
| Inside.java | Blog oficial con artículos y vídeos del equipo |
| JDK Mission Control / JFR | Para el rendimiento (10-07) |
| Javadoc de la versión | Con @since en cada método |
Un JEP es la unidad de cambio de Java. Los que conviene conocer de esta lección: JEP 261 (módulos), JEP 286 (var), JEP 361 (switch de expresión), JEP 378 (bloques de texto), JEP 395 (records), JEP 409 (clases selladas), JEP 440/441 (patrones de registro y switch), JEP 444 (hilos virtuales).
Decidir a qué versión migrar
Regla base: usa la LTS más reciente que tus dependencias soporten. Hoy eso es Java 21, con Java 25 disponible.
El salto por versión de origen:
| Desde | Hasta | Dificultad | Qué esperar |
|---|---|---|---|
| 8 | 11 | Alta | Módulos, API interna bloqueada, eliminación de CORBA y JavaFX del JDK, javax.xml.bind fuera |
| 8 | 17 | Alta | Lo anterior más el Security Manager obsoleto y encapsulamiento fuerte |
| 11 | 17 | Media | Encapsulamiento fuerte por defecto es el principal escollo |
| 17 | 21 | Baja | Muy compatible; casi todo funciona sin cambios |
| 21 | 25 | Baja | Esperada como muy compatible |
El salto de Java 8 a 11 es el difícil, y por eso tantos proyectos siguen en 8. Los problemas típicos:
javax.xml.bind(JAXB) yjava.xml.wsse eliminaron del JDK. Hay que añadirlos como dependencia.- Bibliotecas que usan API interna (
sun.misc.Unsafe,sun.reflect) fallan conInaccessibleObjectException. - Herramientas antiguas (versiones viejas de Maven, Gradle, Lombok, ASM) no entienden el bytecode nuevo.
-XX:+UseParallelOldGCy otras opciones de JVM desaparecieron.
Un plan de migración razonable:
# 1. Detectar usos de API interna ANTES de tocar nada
jdeps --jdk-internals --multi-release 21 bibliotech.jar
# 2. Compilar con la version nueva pero destino antiguo
javac --release 11 ...
# 3. Ejecutar con la JVM nueva y el codigo antiguo, viendo los avisos
java --illegal-access=warn -jar bibliotech.jar # hasta Java 16
# 4. Actualizar dependencias UNA a UNA, no todas de golpe
# 5. Subir el destino de compilacion
javac --release 21 ...
# 6. Modernizar el codigo (opcional y gradual):
# var, switch de expresion, records, bloques de texto, streamsY una recomendación de método: separa la migración (que compile y funcione en la versión nueva) de la modernización (usar las características nuevas). Hacer las dos a la vez multiplica el riesgo y hace imposible saber qué rompió qué.
Errores Comunes y Consejos
1. Usar características de vista previa en producción. Requieren --enable-preview en compilación y ejecución, el bytecode solo funciona en esa versión exacta, y el diseño puede cambiar.
2. Modularizar una aplicación empresarial "porque es lo moderno". JPMS aporta poco a un JAR ejecutable y cuesta mucha fricción con las dependencias. Modulariza bibliotecas públicas, no aplicaciones.
3. Confundir exports con opens. Exportar permite el uso normal; solo opens permite setAccessible sobre privados. Un framework de persistencia necesita opens.
4. Abusar de --add-opens. Cada uso es deuda técnica: dependes de detalles internos que pueden cambiar. Úsalo como parche temporal mientras actualizas la biblioteca culpable.
5. Abusar de var. var resultado = servicio.procesar(datos); no dice nada. Úsalo cuando el tipo sea evidente en la propia línea.
6. var lista = new ArrayList<>(); infiere ArrayList<Object>, casi nunca lo que quieres. Con var, el tipo genérico va a la derecha.
7. Poner default en un switch sobre un tipo sellado. Destruye la exhaustividad: si mañana se añade un tipo, el default lo absorbe en silencio en lugar de romper la compilación. La ausencia de default es la característica, no un descuido.
8. Olvidar que las subclases de una sealed deben declararse. final, sealed o non-sealed: no hay cuarta opción, y el mensaje de error es claro pero se olvida.
9. Agrupar hilos virtuales en un pool. Contradice su propósito. Uno por tarea, y Semaphore para limitar el acceso a recursos externos.
10. Bloquear dentro de synchronized con hilos virtuales. Provoca pinning y bloquea un hilo portador. Usa ReentrantLock en Java 21 (en Java 24+ el problema está en gran parte resuelto).
11. Esperar que los hilos virtuales aceleren el cálculo. No lo hacen: solo evitan desperdiciar hilos del sistema operativo mientras se espera E/S.
12. Alinear mal el """ de cierre de un bloque de texto. Su posición participa en el cálculo de la sangría común. Alinéalo con el contenido salvo que quieras conservar margen.
13. Olvidar que List.of y Map.of no admiten null y que el orden de Set.of cambia entre ejecuciones a propósito.
Consejo 1: aprende las características por el problema que resuelven. sealed existe para que el compilador verifique la exhaustividad. Los hilos virtuales existen para que el código bloqueante escale. Memorizar sintaxis sin el porqué produce uso incorrecto.
Consejo 2: adopta gradualmente. Empieza por lo que no tiene riesgo: var donde ayude, switch de expresión, bloques de texto para SQL y JSON, record para DTO. Deja sealed y pattern matching para cuando refactorices una jerarquía.
Consejo 3: usa jshell para verificar. Cinco segundos en el REPL responden a "¿qué devuelve exactamente esto?" mejor que quince minutos leyendo documentación.
Consejo 4: separa migrar de modernizar. Primero que compile y pase las pruebas en la versión nueva; después, moderniza módulo a módulo.
Consejo 5: lee el JEP de lo que uses. Los JEP explican qué alternativas se descartaron y por qué, y eso enseña más sobre diseño de lenguajes que cualquier tutorial.
Ejercicios
Ejercicio 1: modelar el catálogo con tipos sellados
Rediseña el dominio de BiblioTech aprovechando todo lo de esta lección:
sealed interface ElementoCatalogo permits Material, Coleccion, dondeColeccionagrupa varios elementos (un elemento puede contener otros).sealed abstract class Material permits Libro, Revista, Dvd, AudioLibro, conLibroyDvdcomofinal,Revistasellada con dos subtipos, yAudioLibrocomonon-sealed.- Un
sealed interface ResultadoBusquedacon recordsEncontrado,NoEncontradoyAmbiguo(List<Material> candidatos). - Un servicio con
switchexhaustivos y sindefaultque calcule: plazo de préstamo, valor de reposición, etiqueta de estante y una ficha completa con bloques de texto. - Un método
int contarMateriales(ElementoCatalogo)recursivo que descienda por las colecciones anidadas usando patrones de registro. - Demuestra que añadir un quinto material rompe la compilación en todos los sitios correctos.
Ejercicio 2: EnriquecedorCatalogo con hilos virtuales
En 09-06 escribiste EnriquecedorCatalogo con CompletableFuture, allOf y un Semaphore. Reescríbelo con hilos virtuales y compara.
EnriquecedorVirtualque consulte metadatos y descargue portadas de N ISBN usandoExecutors.newVirtualThreadPerTaskExecutor().- Código secuencial y bloqueante dentro de cada tarea, sin
thenComposenithenCombine. - Limita a 50 peticiones simultáneas al servicio externo con
Semaphore, no con el tamaño del pool. - Maneja los fallos individuales sin que tumben el lote, con un
record ResultadoEnriquecimientosellado. - Mide y compara: pool de 200 plataforma, hilos virtuales, y
CompletableFuture, con 100, 1.000 y 10.000 ISBN. - Demuestra el pinning: una versión con
synchronizedalrededor de la llamada bloqueante frente a otra conReentrantLock, ejecutadas con-Djdk.tracePinnedThreads=full.
Ejercicio 3: informe de modernización
Escribe una herramienta AnalizadorDeModernizacion que examine ficheros .java y detecte oportunidades de modernización:
- Cadenas concatenadas con
+en varias líneas → bloque de texto. switchclásico conbreak→switchde expresión.instanceofseguido de cast al mismo tipo →instanceofcon patrón.- Clases con solo campos
final, constructor, getters,equals,hashCodeytoString→record. new SimpleDateFormat/java.util.Date→java.time(10-05).Executors.newFixedThreadPoolcon tareas bloqueantes → hilos virtuales.- Bucles con acumulador sobre colecciones → streams (10-04).
Para cada hallazgo, indica fichero, línea, la versión de Java que lo permite y una sugerencia. Genera un informe con groupingBy y bloques de texto. Aplícalo a las clases antiguas de BiblioTech.
Soluciones
Solución 1
package com.nexussoftware.bibliotech.dominio;
import java.util.List;
/**
* Raiz SELLADA del catalogo. Un elemento es, o un material concreto,
* o una coleccion que agrupa otros elementos.
*/
public sealed interface ElementoCatalogo permits Material, Coleccion {
String getIsbn();
String getTitulo();
}package com.nexussoftware.bibliotech.dominio;
import java.util.List;
import java.util.Objects;
/**
* Agrupacion de elementos. Puede contener colecciones anidadas:
* es una estructura recursiva (el patron Composite, formalizado en 12-02).
*/
public record Coleccion(String isbn, String titulo, List<ElementoCatalogo> elementos)
implements ElementoCatalogo {
public Coleccion {
Objects.requireNonNull(isbn, "isbn");
Objects.requireNonNull(titulo, "titulo");
elementos = List.copyOf(elementos); // copia defensiva inmutable
}
@Override public String getIsbn() { return isbn; }
@Override public String getTitulo() { return titulo; }
}package com.nexussoftware.bibliotech.dominio;
/**
* Jerarquia de materiales, CERRADA a cuatro tipos.
* Cada subclase declara explicitamente si se puede extender.
*/
public sealed abstract class Material
implements ElementoCatalogo
permits Libro, Revista, Dvd, AudioLibro {
private final String isbn;
private final String titulo;
private final double valoracion;
protected Material(String isbn, String titulo, double valoracion) {
this.isbn = isbn;
this.titulo = titulo;
this.valoracion = valoracion;
}
@Override public String getIsbn() { return isbn; }
@Override public String getTitulo() { return titulo; }
public double getValoracion() { return valoracion; }
}/** final: cerrada del todo. */
public final class Libro extends Material {
private final String autor;
private final int paginas;
public Libro(String isbn, String titulo, String autor, int paginas, double valoracion) {
super(isbn, titulo, valoracion);
this.autor = autor;
this.paginas = paginas;
}
public String getAutor() { return autor; }
public int getPaginas() { return paginas; }
}
/** sealed: su propia jerarquia cerrada de dos periodicidades. */
public sealed class Revista extends Material
permits RevistaMensual, RevistaTrimestral {
private final int numero;
protected Revista(String isbn, String titulo, int numero, double valoracion) {
super(isbn, titulo, valoracion);
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 titulo, int numero,
java.time.Month mes, double valoracion) {
super(isbn, titulo, numero, valoracion);
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 titulo, int numero,
int trimestre, double valoracion) {
super(isbn, titulo, numero, valoracion);
this.trimestre = trimestre;
}
public int getTrimestre() { return trimestre; }
}
/** final. */
public final class Dvd extends Material {
private final String director;
private final int minutos;
public Dvd(String isbn, String titulo, String director, int minutos, double valoracion) {
super(isbn, titulo, valoracion);
this.director = director;
this.minutos = minutos;
}
public String getDirector() { return director; }
public int getMinutos() { return minutos; }
}
/** non-sealed: DELIBERADAMENTE abierta a extension por terceros. */
public non-sealed class AudioLibro extends Material {
private final String narrador;
private final int minutos;
public AudioLibro(String isbn, String titulo, String narrador, int minutos, double valoracion) {
super(isbn, titulo, valoracion);
this.narrador = narrador;
this.minutos = minutos;
}
public String getNarrador() { return narrador; }
public int getMinutos() { return minutos; }
}package com.nexussoftware.bibliotech.dominio;
import java.util.List;
/** Tipo suma para el resultado de una busqueda. */
public sealed interface ResultadoBusqueda {
record Encontrado(Material material) implements ResultadoBusqueda { }
record NoEncontrado(String consulta, List<String> sugerencias) implements ResultadoBusqueda { }
record Ambiguo(String consulta, List<Material> candidatos) implements ResultadoBusqueda { }
}El servicio:
package com.nexussoftware.bibliotech.servicio;
import com.nexussoftware.bibliotech.dominio.*;
/**
* Todos los switch son EXHAUSTIVOS y sin default.
* Si se anade un quinto material, ninguno compila.
*/
public class ServicioCatalogo {
public int diasDePlazo(Material material) {
return switch (material) {
case Libro l when l.getPaginas() > 500 -> 30;
case Libro l when l.getPaginas() > 200 -> 21;
case Libro l -> 14;
case RevistaMensual r -> 7;
case RevistaTrimestral r -> 14;
case Dvd d when d.getMinutos() > 180 -> 5;
case Dvd d -> 3;
case AudioLibro a -> 21;
};
// Revista es sealed: el compilador acepta cubrir sus dos subtipos
// en lugar de Revista, y comprueba que estan los dos.
}
public double valorDeReposicion(Material material) {
return switch (material) {
case Libro l -> Math.max(15.0, l.getPaginas() * 0.08);
case RevistaMensual r -> 8.0;
case RevistaTrimestral r -> 14.0;
case Dvd d -> Math.max(10.0, d.getMinutos() * 0.15);
case AudioLibro a -> Math.max(20.0, a.getMinutos() * 0.10);
};
}
public String etiquetaDeEstante(ElementoCatalogo elemento) {
String prefijo = switch (elemento) {
case Libro l -> "LIB";
case RevistaMensual r -> "REM";
case RevistaTrimestral r -> "RET";
case Dvd d -> "DVD";
case AudioLibro a -> "AUD";
case Coleccion c -> "COL";
};
return prefijo + "-" + elemento.getIsbn().substring(4, 8);
}
/** Ficha con bloques de texto y formatted (Java 15). */
public String ficha(ElementoCatalogo elemento) {
return switch (elemento) {
case Libro l -> """
┌─ LIBRO ─────────────────────────────
│ %s
│ Autor: %s
│ Páginas: %d
│ Estante: %s
│ Plazo: %d días
│ Reposición: %.2f €
└─────────────────────────────────────"""
.formatted(l.getTitulo(), l.getAutor(), l.getPaginas(),
etiquetaDeEstante(l), diasDePlazo(l), valorDeReposicion(l));
case RevistaMensual r -> """
┌─ REVISTA MENSUAL ───────────────────
│ %s nº %d (%s)
│ Estante: %s
│ Plazo: %d días
└─────────────────────────────────────"""
.formatted(r.getTitulo(), r.getNumero(), r.getMes(),
etiquetaDeEstante(r), diasDePlazo(r));
case RevistaTrimestral r -> """
┌─ REVISTA TRIMESTRAL ────────────────
│ %s nº %d (T%d)
│ Estante: %s
│ Plazo: %d días
└─────────────────────────────────────"""
.formatted(r.getTitulo(), r.getNumero(), r.getTrimestre(),
etiquetaDeEstante(r), diasDePlazo(r));
case Dvd d -> """
┌─ DVD ───────────────────────────────
│ %s
│ Director: %s
│ Duración: %d min
│ Plazo: %d días
└─────────────────────────────────────"""
.formatted(d.getTitulo(), d.getDirector(), d.getMinutos(), diasDePlazo(d));
case AudioLibro a -> """
┌─ AUDIOLIBRO ────────────────────────
│ %s
│ Narrador: %s
│ Duración: %d min
│ Plazo: %d días
└─────────────────────────────────────"""
.formatted(a.getTitulo(), a.getNarrador(), a.getMinutos(), diasDePlazo(a));
case Coleccion(var isbn, var titulo, var elementos) -> """
┌─ COLECCIÓN ─────────────────────────
│ %s
│ Elementos: %d directos, %d en total
│ Estante: %s
└─────────────────────────────────────"""
.formatted(titulo, elementos.size(),
contarMateriales(elemento), etiquetaDeEstante(elemento));
};
}
/**
* RECURSIVO con patron de registro: la coleccion se descompone
* y se desciende por sus elementos.
*/
public int contarMateriales(ElementoCatalogo elemento) {
return switch (elemento) {
case Material m -> 1;
case Coleccion(var isbn, var titulo, var elementos) ->
elementos.stream()
.mapToInt(this::contarMateriales) // recursion
.sum();
};
}
/** Profundidad maxima de anidamiento. */
public int profundidad(ElementoCatalogo elemento) {
return switch (elemento) {
case Material m -> 0;
case Coleccion(var isbn, var titulo, var elementos) ->
1 + elementos.stream()
.mapToInt(this::profundidad)
.max().orElse(0);
};
}
/** El resultado de busqueda, tambien con switch exhaustivo. */
public String describirBusqueda(ResultadoBusqueda resultado) {
return switch (resultado) {
case ResultadoBusqueda.Encontrado(Material m) ->
"Encontrado: " + m.getTitulo();
case ResultadoBusqueda.NoEncontrado(var consulta, var sugerencias)
when sugerencias.isEmpty() ->
"No hay resultados para \"" + consulta + "\"";
case ResultadoBusqueda.NoEncontrado(var consulta, var sugerencias) ->
"No hay resultados para \"" + consulta + "\". ¿Quizás: "
+ String.join(", ", sugerencias) + "?";
case ResultadoBusqueda.Ambiguo(var consulta, var candidatos) ->
candidatos.size() + " coincidencias para \"" + consulta + "\": "
+ candidatos.stream().map(Material::getTitulo)
.collect(java.util.stream.Collectors.joining(", "));
};
}
}Prueba:
package com.nexussoftware.bibliotech;
import com.nexussoftware.bibliotech.dominio.*;
import com.nexussoftware.bibliotech.servicio.ServicioCatalogo;
import java.time.Month;
import java.util.List;
public class PruebaCatalogoSellado {
public static void main(String[] args) {
ServicioCatalogo servicio = new ServicioCatalogo();
Libro efectivo = new Libro("978-0000000001", "Java Efectivo", "Joshua Bloch", 412, 4.85);
Libro patrones = new Libro("978-0000000002", "Patrones de Diseño", "GoF", 395, 4.60);
Libro refactor = new Libro("978-0000000003", "Refactorización", "M. Fowler", 448, 4.72);
RevistaMensual javaMag = new RevistaMensual("978-0000000010", "Java Magazine",
142, Month.AUGUST, 3.90);
Dvd cursoSpring = new Dvd("978-0000000020", "Curso de Spring", "N. Vidal", 240, 4.10);
AudioLibro audio = new AudioLibro("978-0000000030", "Clean Code (audio)",
"D. Alonso", 480, 4.30);
// Coleccion ANIDADA
Coleccion esenciales = new Coleccion("978-1000000001", "Esenciales de Java",
List.of(efectivo, patrones, refactor));
Coleccion multimedia = new Coleccion("978-1000000002", "Formación multimedia",
List.of(cursoSpring, audio));
Coleccion completa = new Coleccion("978-1000000000", "Biblioteca técnica Nexus",
List.of(esenciales, multimedia, javaMag));
System.out.println(servicio.ficha(efectivo));
System.out.println(servicio.ficha(javaMag));
System.out.println(servicio.ficha(audio));
System.out.println(servicio.ficha(completa));
System.out.println();
System.out.println("Materiales totales: " + servicio.contarMateriales(completa));
System.out.println("Profundidad: " + servicio.profundidad(completa));
System.out.println();
System.out.println(servicio.describirBusqueda(
new ResultadoBusqueda.Encontrado(efectivo)));
System.out.println(servicio.describirBusqueda(
new ResultadoBusqueda.NoEncontrado("kotlin", List.of())));
System.out.println(servicio.describirBusqueda(
new ResultadoBusqueda.NoEncontrado("javaa", List.of("Java Efectivo"))));
System.out.println(servicio.describirBusqueda(
new ResultadoBusqueda.Ambiguo("java", List.of(efectivo, audio))));
}
}┌─ LIBRO ─────────────────────────────
│ Java Efectivo
│ Autor: Joshua Bloch
│ Páginas: 412
│ Estante: LIB-0000
│ Plazo: 21 días
│ Reposición: 32,96 €
└─────────────────────────────────────
┌─ REVISTA MENSUAL ───────────────────
│ Java Magazine nº 142 (AUGUST)
│ Estante: REM-0000
│ Plazo: 7 días
└─────────────────────────────────────
┌─ AUDIOLIBRO ────────────────────────
│ Clean Code (audio)
│ Narrador: D. Alonso
│ Duración: 480 min
│ Plazo: 21 días
└─────────────────────────────────────
┌─ COLECCIÓN ─────────────────────────
│ Biblioteca técnica Nexus
│ Elementos: 3 directos, 6 en total
│ Estante: COL-1000
└─────────────────────────────────────
Materiales totales: 6
Profundidad: 2
Encontrado: Java Efectivo
No hay resultados para "kotlin"
No hay resultados para "javaa". ¿Quizás: Java Efectivo?
2 coincidencias para "java": Java Efectivo, Clean Code (audio)Y la demostración de la exhaustividad. Al añadir Mapa a los permits:
public sealed abstract class Material
implements ElementoCatalogo
permits Libro, Revista, Dvd, AudioLibro, Mapa { }
public final class Mapa extends Material { /* ... */ }ServicioCatalogo.java:14: error: the switch expression does not cover all possible input values
return switch (material) {
^
ServicioCatalogo.java:27: error: the switch expression does not cover all possible input values
return switch (material) {
^
ServicioCatalogo.java:37: error: the switch expression does not cover all possible input values
String prefijo = switch (elemento) {
^
ServicioCatalogo.java:49: error: the switch expression does not cover all possible input values
return switch (elemento) {
^
4 errorsComentarios. Cuatro observaciones.
Cuatro errores de compilación son cuatro sitios que había que actualizar. Con la versión antigua de if-else con default, el Mapa habría caído en el default de cada método: plazo cero, reposición cero, etiqueta "X", y nadie se habría enterado hasta que un usuario reclamara. Ese es el valor entero de las clases selladas.
El compilador entiende las jerarquías anidadas. Revista es sealed permits RevistaMensual, RevistaTrimestral, y el switch cubre los dos subtipos en lugar de Revista. El compilador verifica que están los dos: quitar uno rompe la compilación.
AudioLibro es non-sealed a propósito, y demuestra que la apertura es una decisión por rama. Alguien puede escribir class Podcast extends AudioLibro, y el case AudioLibro a lo cubrirá.
El contarMateriales recursivo con patrón de registro es lo más expresivo del ejercicio. Dos casos, cuatro líneas, y recorre una estructura arbitrariamente anidada. Es el patrón Composite (12-02) expresado como tipo algebraico.
Solución 2
package com.nexussoftware.bibliotech.red;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Enriquecedor de catalogo con HILOS VIRTUALES.
* Sustituye a la version de 09-06 basada en CompletableFuture.
*/
public class EnriquecedorVirtual implements AutoCloseable {
private static final Logger LOG = Logger.getLogger(EnriquecedorVirtual.class.getName());
/** Tipo suma para el resultado de cada ISBN. */
public sealed interface ResultadoEnriquecimiento {
String isbn();
record Completo(String isbn, String metadatos, int bytesPortada, long ms)
implements ResultadoEnriquecimiento { }
record Parcial(String isbn, String metadatos, String motivoSinPortada, long ms)
implements ResultadoEnriquecimiento { }
record Fallido(String isbn, String motivo, long ms)
implements ResultadoEnriquecimiento { }
}
/**
* Semaforo, NO tamaño de pool: la restriccion es del servicio
* externo, no de nuestros hilos.
*/
private final Semaphore limiteExterno;
private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
private final ClienteMetadatos metadatos;
private final ClientePortadas portadas;
public EnriquecedorVirtual(ClienteMetadatos metadatos, ClientePortadas portadas,
int peticionesSimultaneas) {
this.metadatos = metadatos;
this.portadas = portadas;
this.limiteExterno = new Semaphore(peticionesSimultaneas);
}
public List<ResultadoEnriquecimiento> enriquecer(List<String> isbns) {
List<Future<ResultadoEnriquecimiento>> futuros = new ArrayList<>(isbns.size());
// Se lanzan TODAS: un hilo virtual por ISBN, sean 100 o 100.000
for (String isbn : isbns) {
futuros.add(executor.submit(() -> procesarUno(isbn)));
}
List<ResultadoEnriquecimiento> resultados = new ArrayList<>(isbns.size());
for (int i = 0; i < futuros.size(); i++) {
try {
resultados.add(futuros.get(i).get());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (ExecutionException e) {
resultados.add(new ResultadoEnriquecimiento.Fallido(
isbns.get(i), "excepción no controlada: " + e.getCause(), 0));
}
}
return resultados;
}
/**
* CODIGO SECUENCIAL Y BLOQUEANTE. Ni un thenCompose, ni un
* thenCombine, ni una retrollamada. Se lee de arriba abajo.
*/
private ResultadoEnriquecimiento procesarUno(String isbn) {
long inicio = System.nanoTime();
Thread.currentThread().setName("enriquecer-" + isbn);
try {
// 1. Metadatos: bloquea, y no pasa nada
String datos;
limiteExterno.acquire();
try {
datos = metadatos.consultar(isbn); // llamada BLOQUEANTE
} finally {
limiteExterno.release();
}
// 2. Portada: puede fallar sin invalidar el resultado
int bytes;
try {
limiteExterno.acquire();
try {
bytes = portadas.descargar(isbn).length;
} finally {
limiteExterno.release();
}
} catch (Exception e) {
return new ResultadoEnriquecimiento.Parcial(
isbn, datos, e.getClass().getSimpleName(), ms(inicio));
}
return new ResultadoEnriquecimiento.Completo(isbn, datos, bytes, ms(inicio));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return new ResultadoEnriquecimiento.Fallido(isbn, "interrumpido", ms(inicio));
} catch (Exception e) {
LOG.log(Level.FINE, e, () -> "Fallo al enriquecer " + isbn);
return new ResultadoEnriquecimiento.Fallido(
isbn, e.getClass().getSimpleName() + ": " + e.getMessage(), ms(inicio));
}
}
private long ms(long inicioNanos) {
return (System.nanoTime() - inicioNanos) / 1_000_000;
}
/** Informe con switch exhaustivo sobre el tipo sellado. */
public String informar(List<ResultadoEnriquecimiento> resultados) {
Map<String, Long> porTipo = resultados.stream()
.collect(java.util.stream.Collectors.groupingBy(
r -> switch (r) {
case ResultadoEnriquecimiento.Completo c -> "completos";
case ResultadoEnriquecimiento.Parcial p -> "parciales";
case ResultadoEnriquecimiento.Fallido f -> "fallidos";
},
java.util.TreeMap::new,
java.util.stream.Collectors.counting()));
double mediaMs = resultados.stream()
.mapToLong(r -> switch (r) {
case ResultadoEnriquecimiento.Completo c -> c.ms();
case ResultadoEnriquecimiento.Parcial p -> p.ms();
case ResultadoEnriquecimiento.Fallido f -> f.ms();
})
.average().orElse(0);
return """
Enriquecimiento de %d ISBN
%s
Duración media por ISBN: %.1f ms"""
.formatted(resultados.size(), porTipo, mediaMs);
}
@Override
public void close() {
executor.close(); // ExecutorService es AutoCloseable desde Java 19
}
}El banco de pruebas comparativo:
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 ComparativaEnriquecimiento {
/** Simula una llamada de red: 50 ms de espera. */
private static String consultarSimulado(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)", "VIRTUALES", "COMPL.FUTURE");
System.out.println("-".repeat(56));
for (int n : new int[] { 100, 1_000, 10_000 }) {
List<String> isbns = generar(n);
long plataforma = medir(() -> conPoolDePlataforma(isbns));
long virtuales = medir(() -> conHilosVirtuales(isbns));
long futuros = medir(() -> conCompletableFuture(isbns));
System.out.printf("%-10d %11d ms %11d ms %11d ms%n",
n, plataforma, virtuales, futuros);
}
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 medir(Runnable tarea) {
long inicio = System.currentTimeMillis();
tarea.run();
return System.currentTimeMillis() - inicio;
}
private static void conPoolDePlataforma(List<String> isbns) {
try (var executor = Executors.newFixedThreadPool(200)) {
List<Future<String>> futuros = isbns.stream()
.map(isbn -> executor.submit(() -> consultarSimulado(isbn)))
.toList();
futuros.forEach(ComparativaEnriquecimiento::obtener);
}
}
private static void conHilosVirtuales(List<String> isbns) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> futuros = isbns.stream()
.map(isbn -> executor.submit(() -> consultarSimulado(isbn)))
.toList();
futuros.forEach(ComparativaEnriquecimiento::obtener);
}
}
private static void conCompletableFuture(List<String> isbns) {
try (var executor = Executors.newFixedThreadPool(200)) {
List<CompletableFuture<String>> futuros = isbns.stream()
.map(isbn -> CompletableFuture.supplyAsync(() -> {
try {
return consultarSimulado(isbn);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}, executor))
.toList();
CompletableFuture.allOf(futuros.toArray(CompletableFuture[]::new)).join();
}
}
private static <T> T obtener(Future<T> f) {
try {
return f.get();
} catch (Exception e) {
return null;
}
}
// ------------------------------------------------------------------
// Demostracion del PINNING
// ------------------------------------------------------------------
static class ConSynchronized {
private final Map<String, String> cache = new HashMap<>();
/** synchronized + bloqueo -> PINNING del hilo portador. */
public synchronized String obtener(String isbn) throws InterruptedException {
String v = cache.get(isbn);
if (v == null) {
v = consultarSimulado(isbn);
cache.put(isbn, v);
}
return v;
}
}
static class ConReentrantLock {
private final Map<String, String> cache = new ConcurrentHashMap<>();
private final ReentrantLock cerrojo = new ReentrantLock();
/** ReentrantLock permite desmontar el hilo virtual. */
public String obtener(String isbn) throws InterruptedException {
String v = cache.get(isbn);
if (v != null) {
return v;
}
cerrojo.lock();
try {
v = cache.get(isbn);
if (v == null) {
v = consultarSimulado(isbn);
cache.put(isbn, v);
}
return v;
} finally {
cerrojo.unlock();
}
}
}
private static void demostrarPinning() {
System.out.println("--- Pinning (ejecutar con -Djdk.tracePinnedThreads=full) ---");
List<String> isbns = generar(500);
ConSynchronized conSync = new ConSynchronized();
long t1 = medir(() -> {
try (var e = Executors.newVirtualThreadPerTaskExecutor()) {
isbns.forEach(isbn -> e.submit(() -> conSync.obtener(isbn)));
}
});
ConReentrantLock conLock = new ConReentrantLock();
long t2 = medir(() -> {
try (var e = Executors.newVirtualThreadPerTaskExecutor()) {
isbns.forEach(isbn -> e.submit(() -> conLock.obtener(isbn)));
}
});
System.out.printf(" synchronized: %5d ms%n", t1);
System.out.printf(" ReentrantLock: %5d ms%n", t2);
}
}ISBN PLATAF. (200) VIRTUALES COMPL.FUTURE
--------------------------------------------------------
100 62 ms 58 ms 64 ms
1000 306 ms 71 ms 301 ms
10000 2571 ms 139 ms 2559 ms
--- Pinning (ejecutar con -Djdk.tracePinnedThreads=full) ---
synchronized: 25142 ms
ReentrantLock: 25089 msComentarios. Cinco observaciones.
Con 100 ISBN los tres son equivalentes, porque 100 caben en un pool de 200. La diferencia aparece cuando la concurrencia deseada supera al pool.
Con 10.000, los hilos virtuales son 18× más rápidos. El pool de 200 hace 50 rondas de 50 ms; los virtuales lo hacen todo a la vez. CompletableFuture sobre el mismo pool de 200 tiene exactamente el mismo límite: la asincronía no elimina el cuello de botella del pool, solo evita que los hilos esperen ociosos.
El código de procesarUno es secuencial. Compáralo con la versión de 09-06: allí había thenCompose, thenCombine, exceptionally y allOf, y las variables intermedias tenían que viajar dentro de las lambdas. Aquí hay un try, dos llamadas y un return. Y escala igual.
Los dos casos de pinning tardan lo mismo aquí, y eso es instructivo: el synchronized está en el método completo, así que serializa las 500 tareas con o sin pinning — 500 × 50 ms = 25 segundos en ambos casos. El pinning se manifiesta sobre todo cuando hay muchos portadores ocupados; con -Djdk.tracePinnedThreads=full en Java 21 verás las trazas <== monitors:1 en la versión con synchronized y ninguna en la del ReentrantLock. La lección real es que el problema aquí no es el pinning sino la sección crítica demasiado grande, que es un problema del módulo 8 y no de Loom.
El Semaphore en lugar del tamaño del pool es la pieza de diseño. El servicio de metadatos aguanta 50 peticiones simultáneas; eso es una propiedad del servicio, no del cliente. Expresarlo con un semáforo permite tener 10.000 hilos virtuales de los que solo 50 están dentro del servicio a la vez, y los 9.950 restantes esperando sin consumir ningún hilo del sistema operativo.
Solución 3
package com.nexussoftware.bibliotech.herramientas;
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 oportunidades de modernizacion en codigo Java.
*
* Es un analisis por expresiones regulares: rapido y aproximado.
* Un analizador real trabajaria sobre el AST (javax.lang.model, 10-02).
*/
public class AnalizadorDeModernizacion {
/** Tipo sellado para las categorias de hallazgo. */
public sealed interface Hallazgo {
Path fichero();
int linea();
int versionJava();
String sugerencia();
record ConcatenacionMultilinea(Path fichero, int linea) implements Hallazgo {
public int versionJava() { return 15; }
public String sugerencia() { return "Usa un bloque de texto \"\"\"...\"\"\""; }
}
record SwitchClasico(Path fichero, int linea) implements Hallazgo {
public int versionJava() { return 14; }
public String sugerencia() { return "Usa switch de expresión con ->"; }
}
record CastTrasInstanceof(Path fichero, int linea, String tipo) implements Hallazgo {
public int versionJava() { return 16; }
public String sugerencia() { return "Usa instanceof " + tipo + " variable"; }
}
record ApiDeFechasAntigua(Path fichero, int linea, String clase) implements Hallazgo {
public int versionJava() { return 8; }
public String sugerencia() {
return switch (clase) {
case "SimpleDateFormat" -> "Usa DateTimeFormatter (inmutable y seguro entre hilos)";
case "Calendar" -> "Usa LocalDate / LocalDateTime";
case "Date" -> "Usa Instant o LocalDate según el caso";
default -> "Migra a java.time";
};
}
}
record PoolDePlataforma(Path fichero, int linea) implements Hallazgo {
public int versionJava() { return 21; }
public String sugerencia() {
return "Si las tareas se bloquean, usa Executors.newVirtualThreadPerTaskExecutor()";
}
}
record BucleAcumulador(Path fichero, int linea) implements Hallazgo {
public int versionJava() { return 8; }
public String sugerencia() { return "Considera stream().filter(...).map(...).toList()"; }
}
record CandidatoARecord(Path fichero, int linea, String clase) implements Hallazgo {
public int versionJava() { return 16; }
public String sugerencia() { return "La clase " + clase + " podría ser un record"; }
}
}
// --- Patrones ---
private static final Pattern CONCATENACION = Pattern.compile("^\\s*\\+\\s*\"");
private static final Pattern SWITCH_CLASICO = Pattern.compile("\\bcase\\s+[^>]+:\\s*$");
private static final Pattern INSTANCEOF_SIN_PATRON =
Pattern.compile("instanceof\\s+(\\w+)\\s*\\)\\s*\\{?\\s*$");
private static final Pattern CAST = Pattern.compile("\\(\\s*(\\w+)\\s*\\)\\s*\\w+");
private static final Pattern FECHAS_ANTIGUAS =
Pattern.compile("\\bnew\\s+(SimpleDateFormat|Date|GregorianCalendar)\\b"
+ "|\\bCalendar\\.getInstance\\(\\)");
private static final Pattern POOL_FIJO =
Pattern.compile("Executors\\.newFixedThreadPool|Executors\\.newCachedThreadPool");
private static final Pattern DECLARACION_LISTA =
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<Hallazgo> analizar(Path raiz) throws IOException {
try (Stream<Path> ficheros = Files.walk(raiz)) {
return ficheros
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.filter(p -> !p.getFileName().toString().equals("module-info.java"))
.flatMap(this::analizarFichero)
.toList();
}
}
private Stream<Hallazgo> analizarFichero(Path fichero) {
try {
List<String> lineas = Files.readAllLines(fichero, StandardCharsets.UTF_8);
List<Hallazgo> hallazgos = new ArrayList<>();
String posibleTipoInstanceof = null;
int lineaInstanceof = 0;
int lineasDeDeclaracion = 0;
int inicioDeclaracion = 0;
for (int i = 0; i < lineas.size(); i++) {
String linea = lineas.get(i);
int numero = i + 1;
if (CONCATENACION.matcher(linea).find()) {
hallazgos.add(new Hallazgo.ConcatenacionMultilinea(fichero, numero));
}
if (SWITCH_CLASICO.matcher(linea).find()) {
hallazgos.add(new Hallazgo.SwitchClasico(fichero, numero));
}
if (FECHAS_ANTIGUAS.matcher(linea).find()) {
var m = FECHAS_ANTIGUAS.matcher(linea);
m.find();
String clase = m.group(1) != null ? m.group(1) : "Calendar";
hallazgos.add(new Hallazgo.ApiDeFechasAntigua(fichero, numero, clase));
}
if (POOL_FIJO.matcher(linea).find()) {
hallazgos.add(new Hallazgo.PoolDePlataforma(fichero, numero));
}
// instanceof en una linea, cast al mismo tipo en la siguiente
var mi = INSTANCEOF_SIN_PATRON.matcher(linea);
if (mi.find()) {
posibleTipoInstanceof = mi.group(1);
lineaInstanceof = numero;
} else if (posibleTipoInstanceof != null) {
var mc = CAST.matcher(linea);
if (mc.find() && posibleTipoInstanceof.equals(mc.group(1))) {
hallazgos.add(new Hallazgo.CastTrasInstanceof(
fichero, lineaInstanceof, posibleTipoInstanceof));
}
posibleTipoInstanceof = null;
}
// Declaracion de coleccion seguida de bucle for-each: candidato a stream
if (DECLARACION_LISTA.matcher(linea).find()) {
lineasDeDeclaracion = 1;
inicioDeclaracion = numero;
} else if (lineasDeDeclaracion > 0 && lineasDeDeclaracion < 4) {
if (FOR_EACH.matcher(linea).find()) {
hallazgos.add(new Hallazgo.BucleAcumulador(fichero, inicioDeclaracion));
lineasDeDeclaracion = 0;
} else {
lineasDeDeclaracion++;
}
} else {
lineasDeDeclaracion = 0;
}
}
detectarCandidatoARecord(fichero, lineas).ifPresent(hallazgos::add);
return hallazgos.stream();
} catch (IOException e) {
System.err.println("No se pudo leer " + fichero + ": " + e.getMessage());
return Stream.empty();
}
}
/** Heuristica: solo campos final, getters, equals, hashCode y toString. */
private Optional<Hallazgo> detectarCandidatoARecord(Path fichero, List<String> lineas) {
String contenido = String.join("\n", lineas);
boolean tieneSetters = contenido.contains("public void set");
boolean tieneCamposNoFinales = Pattern
.compile("private\\s+(?!final)\\w+\\s+\\w+\\s*;").matcher(contenido).find();
boolean tieneEquals = contenido.contains("public boolean equals(Object");
boolean tieneHashCode = contenido.contains("public int hashCode()");
boolean extiende = Pattern.compile("class\\s+\\w+\\s+extends").matcher(contenido).find();
if (!tieneSetters && !tieneCamposNoFinales && tieneEquals && tieneHashCode && !extiende) {
String nombre = fichero.getFileName().toString().replace(".java", "");
int linea = 1 + (int) lineas.stream()
.takeWhile(l -> !l.contains("class " + nombre))
.count();
return Optional.of(new Hallazgo.CandidatoARecord(fichero, linea, nombre));
}
return Optional.empty();
}
// ------------------------------------------------------------------
public String informar(List<Hallazgo> hallazgos, int versionObjetivo) {
Map<String, List<Hallazgo>> porTipo = hallazgos.stream()
.collect(Collectors.groupingBy(h -> h.getClass().getSimpleName(),
TreeMap::new, Collectors.toList()));
Map<Integer, Long> porVersion = hallazgos.stream()
.collect(Collectors.groupingBy(Hallazgo::versionJava,
TreeMap::new, Collectors.counting()));
long aplicables = hallazgos.stream()
.filter(h -> h.versionJava() <= versionObjetivo)
.count();
StringBuilder sb = new StringBuilder();
sb.append("""
════════════════════════════════════════════════════
INFORME DE MODERNIZACIÓN -- objetivo: Java %d
════════════════════════════════════════════════════
Hallazgos totales: %d
Aplicables ya: %d
Requieren migrar: %d
""".formatted(versionObjetivo, hallazgos.size(),
aplicables, hallazgos.size() - aplicables));
sb.append("\nPOR VERSIÓN MÍNIMA NECESARIA\n");
porVersion.forEach((v, n) -> sb.append(String.format(
" Java %-3d %4d %s%s%n", v, n, "▇".repeat((int) Math.min(n, 40)),
v <= versionObjetivo ? "" : " (requiere migrar)")));
sb.append("\nPOR TIPO\n");
porTipo.forEach((tipo, lista) -> {
sb.append(String.format(" %-26s %4d%n", tipo, lista.size()));
lista.stream().limit(3).forEach(h -> sb.append(String.format(
" %s:%d %s%n",
h.fichero().getFileName(), h.linea(), h.sugerencia())));
if (lista.size() > 3) {
sb.append(String.format(" ... y %d más%n", lista.size() - 3));
}
});
return sb.toString();
}
public static void main(String[] args) throws IOException {
Path raiz = Path.of(args.length > 0 ? args[0] : "src");
int objetivo = args.length > 1 ? Integer.parseInt(args[1]) : 21;
AnalizadorDeModernizacion analizador = new AnalizadorDeModernizacion();
List<Hallazgo> hallazgos = analizador.analizar(raiz);
System.out.println(analizador.informar(hallazgos, objetivo));
}
}════════════════════════════════════════════════════
INFORME DE MODERNIZACIÓN -- objetivo: Java 21
════════════════════════════════════════════════════
Hallazgos totales: 47
Aplicables ya: 47
Requieren migrar: 0
POR VERSIÓN MÍNIMA NECESARIA
Java 8 14 ▇▇▇▇▇▇▇▇▇▇▇▇▇▇
Java 14 9 ▇▇▇▇▇▇▇▇▇
Java 15 11 ▇▇▇▇▇▇▇▇▇▇▇
Java 16 8 ▇▇▇▇▇▇▇▇
Java 21 5 ▇▇▇▇▇
POR TIPO
ApiDeFechasAntigua 9
GestorSesion.java:34 Usa DateTimeFormatter (inmutable y seguro entre hilos)
AlmacenPrestamos.java:57 Usa LocalDate / LocalDateTime
RegistroOperaciones.java:22 Usa Instant o LocalDate según el caso
... y 6 más
BucleAcumulador 5
EstadisticasBiblioTech.java:19 Considera stream().filter(...).map(...).toList()
MenuBiblioTech.java:88 Considera stream().filter(...).map(...).toList()
... y 3 más
CandidatoARecord 3
Ficha.java:8 La clase Ficha podría ser un record
ResumenSesion.java:11 La clase ResumenSesion podría ser un record
... y 1 más
CastTrasInstanceof 8
CalculadoraPlazosAntigua.java:14 Usa instanceof Libro variable
ExportadorCatalogoCsv.java:41 Usa instanceof Material variable
... y 6 más
ConcatenacionMultilinea 11
ConsultasBiblioTech.java:12 Usa un bloque de texto """..."""
ClienteMetadatos.java:73 Usa un bloque de texto """..."""
... y 9 más
PoolDePlataforma 5
ServidorCatalogoConPool.java:24 Si las tareas se bloquean, usa Executors.newVirtualThreadPerTaskExecutor()
... y 4 más
SwitchClasico 6
MenuBiblioTech.java:52 Usa switch de expresión con ->
... y 5 másComentarios.
El analizador usa lo que analiza. Tipos sellados para las categorías de hallazgo, records para cada una, switch exhaustivo para las sugerencias, bloques de texto para el informe, Files.walk con streams para recorrer el árbol. Es una demostración por construcción.
El límite de las expresiones regulares es real y hay que declararlo. CastTrasInstanceof da falsos positivos con casts legítimos y falsos negativos cuando el cast está tres líneas más abajo. Un analizador serio trabaja sobre el AST, con javax.lang.model (que asomó en 10-02) o con una biblioteca como JavaParser. Este ejercicio es una aproximación pragmática, no una herramienta de producción.
La columna "requiere migrar" es lo más útil del informe. Ordenar los hallazgos por versión mínima convierte el informe en un argumento de negocio: "migrar a Java 17 desbloquea 28 mejoras; a Java 21, otras 5". Eso es más convincente que "deberíamos actualizar".
Y CandidatoARecord es deliberadamente conservador. Solo propone la conversión si no hay setters, ni campos mutables, ni herencia, y sí hay equals y hashCode. Proponer convertir a record una clase con estado mutable produciría una sugerencia incorrecta, y una herramienta que sugiere mal deja de usarse.
Conclusión
Ya sabes de dónde salió todo lo que llevabas usando, y bastante más.
Conoces el calendario de versiones: una cada seis meses desde 2018, con fecha fija, y LTS cada dos o tres años —8, 11, 17, 21, 25— que son las que se usan en producción. Y sabes que las novedades grandes pasan por vista previa con --enable-preview antes de ser definitivas, y que eso no se usa en producción porque el diseño puede cambiar.
Entiendes el sistema de módulos (JPMS) y los tres problemas que atacaba: un JDK monolítico de 60 MB, la ausencia total de encapsulamiento entre paquetes —public significaba "para todo el mundo"— y las dependencias sin comprobar hasta el NoClassDefFoundError. Sabes escribir un module-info.java con requires (y sus variantes transitive y static), exports (con exportación cualificada to) y opens, y tienes clara la distinción que causa la mitad de los problemas: exportar permite el uso normal; solo opens permite setAccessible sobre miembros privados. Por eso el ExportadorAnotado de 10-03 necesitaría opens si BiblioTech se modularizara, y por eso InaccessibleObjectException existe y --add-opens es la vía de escape —una tirita, no una solución—. Conoces el classpath frente al modulepath, los JAR automáticos como puente, y jlink, que reduce un runtime de 315 MB a 44 MB. Y tienes la valoración honesta: JPMS no se ha adoptado en aplicaciones empresariales, sí en el propio JDK, en bibliotecas serias y con jlink/jpackage; modulariza bibliotecas, no aplicaciones.
Repasaste las incorporaciones de API: colecciones inmutables List.of/Map.of —que rechazan null, rechazan duplicados y desordenan a propósito para que nadie dependa del orden—, métodos privados en interfaces, strip frente a trim, repeat, isBlank, lines que devuelve un Stream, Files.readString/writeString, las colecciones secuenciales de Java 21 que unifican getFirst/getLast/reversed, los mensajes útiles de NullPointerException que dicen exactamente qué devolvió null, y UTF-8 por defecto desde Java 18.
Y la sintaxis. var para inferencia local —que no es tipado dinámico, que brilla con tipos genéricos largos y que estorba cuando el tipo no se deduce de la línea—. El switch de expresión con flechas y yield, sin break, sin caída entre casos y con exhaustividad comprobada sobre enum. Los bloques de texto con su regla de sangría común —donde la posición del """ de cierre participa en el cálculo— y formatted(), ideales para el SQL y el JSON de los módulos siguientes. Los record con constructor compacto que valida y normaliza. El instanceof con patrón que elimina el cast redundante y cuyo ámbito se calcula por flujo, funcionando incluso tras una negación con salida temprana.
Y sobre todo, las dos características que cambian cómo se diseña. Las clases selladas con sealed/permits y sus tres opciones obligatorias para las subclases —final, sealed o non-sealed—, que dan lo que no existía: exhaustividad verificada por el compilador. Y el pattern matching para switch de Java 21 con case null, guardas when y patrones de registro que descomponen los componentes directamente, anidándose a varios niveles. Combinados, modelan tipos algebraicos donde los estados imposibles no se pueden construir.
BiblioTech lo demuestra: CalculadoraPlazos pasó de 45 líneas de if-else con casts y un default que ocultaba los casos nuevos, a 20 líneas de switch exhaustivos sin default. Y cuando el ejercicio añadió un quinto material, el compilador produjo cuatro errores en los cuatro sitios exactos que había que actualizar. La versión antigua habría compilado y habría fallado en producción.
Y tienes los hilos virtuales. Sabes qué son —hilos gestionados por la JVM sobre unos pocos portadores— y por qué cambian el cálculo: al bloquearse se desmontan del portador, guardan su pila en el heap y liberan el hilo del sistema operativo. De ahí las cifras: creación en microsegundos en lugar de milisegundos, cientos de bytes en lugar de un megabyte, millones viables en lugar de miles. Diez mil tareas bloqueadas un segundo pasaron de 50 segundos con un pool de 200 a 1,1 segundos; un millón de hilos virtuales arrancaron en menos de tres.
El ServidorCatalogo de 09-03 se ha refactorizado, y lo importante es lo que no cambió: atender() sigue siendo el mismo código secuencial y bloqueante, con try-with-resources, variables locales y trazas de pila legibles. Lo que desapareció es el pool acotado, el tamaño que ajustar, la cola, la política de rechazo y el límite de 50 clientes. Los hilos virtuales devuelven el código sencillo sin pagar en escalabilidad, que es exactamente lo que CompletableFuture no puede dar.
Conoces las cuatro reglas —no agruparlos en pool, uno por tarea, Semaphore para limitar recursos externos, y ReentrantLock en lugar de synchronized para evitar el pinning— y, tan importante, lo que no resuelven: no aceleran el cálculo, no eliminan ni una sola de las trampas del módulo 8 (que se aplican íntegras), no arreglan un pool de veinte conexiones de base de datos, desaconsejan ThreadLocal, y no sustituyen a CompletableFuture en la composición asíncrona. Y sabes lo que viene: concurrencia estructurada con StructuredTaskScope y ScopedValue como sustituto acotado e inmutable de ThreadLocal.
Sabes usar jshell para verificar en cinco segundos, jpackage para instaladores nativos, jdeps para analizar dependencias, y cómo mantenerte al día —los JEP como fuente primaria, las notas de versión para los cambios incompatibles— y cómo decidir la migración: la LTS más reciente que soporten tus dependencias, sabiendo que el salto de 8 a 11 es el difícil (JAXB fuera, API interna bloqueada, herramientas antiguas) y que de 17 a 21 casi todo funciona sin tocar nada. Con la regla de método que evita la mayoría de los desastres: separa migrar de modernizar.
BiblioTech, al terminar esta lección, tiene una jerarquía sellada donde el compilador verifica que has cubierto todos los materiales, estados de préstamo modelados como records donde las combinaciones imposibles no se pueden escribir, SQL y JSON en bloques de texto legibles, y un servidor que atiende decenas de miles de conexiones con el mismo código que atendía cincuenta.
Y queda una última caja negra. Todo lo que has escrito en este módulo se ejecuta sobre una JVM cuyo comportamiento sigues sin conocer. Cuando el ExportadorAnotado cachea la introspección, ¿dónde vive esa caché y quién la libera? Cuando un millón de hilos virtuales guardan su pila en el heap, ¿qué es exactamente el heap y qué pasa cuando se llena? Cuando la medición del apartado 14 de 10-04 dio números distintos antes y después del calentamiento, ¿qué estaba haciendo la JVM en ese rato? Cuando WeakHashMap apareció de pasada, ¿qué significa que una referencia sea "débil"? ¿Por qué la primera petición al servidor siempre es la más lenta? ¿Y por qué una aplicación Java que lleva tres días funcionando de repente empieza a pausarse cada pocos segundos?
En 10-07, Memoria, Recolección de Basura y Rendimiento, la caja se abre del todo y el módulo se cierra. Verás las regiones de memoria —pila por hilo con sus marcos y su StackOverflowError, heap compartido, metaspace, caché de código, memoria nativa— y qué significa cada mensaje distinto de OutOfMemoryError. Verás el modelo generacional, la hipótesis de que la mayoría de los objetos mueren jóvenes, y cómo el recolector decide qué borrar mediante alcanzabilidad desde las raíces —no contando referencias, por lo que los ciclos no importan—. Compararás los recolectores actuales —Serial, Parallel, G1, ZGC, Shenandoah, Epsilon— y sabrás qué tres parámetros se tocan de verdad. Verás que las fugas de memoria en Java existen, con sus cuatro patrones clásicos demostrados y cómo se diagnostican, y las referencias débiles, blandas y fantasma con WeakHashMap y su caso real en la caché de fichas. Aprenderás a medir antes de optimizar con jps, jstat, jmap, jcmd, volcados de heap y Java Flight Recorder, y por qué un microbenchmark casero miente —calentamiento, JIT, eliminación de código muerto— y qué es JMH. Y entenderás el compilador JIT: interpretación, C1, C2, puntos calientes, inlining, desoptimización, y por qué tu código se acelera solo con el tiempo.
Curso de Programación en Java
Módulo 1: Introducción a Java
- Introducción a Java
- Configuración del Entorno de Desarrollo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Operadores
- Entrada y Salida por Consola
- Tu Primer Programa Completo: BiblioTech
Módulo 2: Flujo de Control
- Sentencias Condicionales
- Bucles
- Sentencias Switch
- Break y Continue
- Depuración y Trazas de Ejecución
- Proyecto: Menú Interactivo de BiblioTech
Módulo 3: Programación Orientada a Objetos
- Introducción a la POO
- Clases y Objetos
- Métodos
- Constructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- La Clase Object: equals, hashCode y toString
Módulo 4: Programación Orientada a Objetos Avanzada
- Interfaces
- Clases Abstractas
- Clases Internas
- Clases Anónimas
- Expresiones Lambda
- Interfaces Funcionales y Referencias a Métodos
- Enumeraciones y Registros
Módulo 5: Estructuras de Datos y Colecciones
- Arreglos
- El Framework de Colecciones
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cola y Deque
- Pila
- Ordenación y Búsqueda en Colecciones
Módulo 6: Manejo de Excepciones
- Introducción a las Excepciones
- Bloque Try-Catch
- Throw y Throws
- Excepciones Personalizadas
- Bloque Finally
- Try-with-resources y AutoCloseable
- Estrategias de Manejo de Errores y Logging
Módulo 7: Entrada/Salida de Archivos
- Lectura de Archivos
- Escritura de Archivos
- Flujos de Archivos
- BufferedReader y BufferedWriter
- Serialización
- La API NIO.2: Path y Files
- Formatos de Intercambio: CSV y Properties
Módulo 8: Multihilo y Concurrencia
- Introducción al Multihilo
- Creación de Hilos
- Ciclo de Vida de un Hilo
- Sincronización
- Utilidades de Concurrencia
- Colecciones Concurrentes y Variables Atómicas
- Tareas Asíncronas con CompletableFuture
Módulo 9: Redes
- Introducción a las Redes
- Sockets
- ServerSocket
- DatagramSocket y DatagramPacket
- URL y HttpURLConnection
- El Cliente HTTP Moderno
Módulo 10: Temas Avanzados
- Genéricos
- Anotaciones
- Reflexión
- Características de Java 8: Streams y Optional
- Fechas y Horas con java.time
- Java 9 y Más Allá
- Memoria, Recolección de Basura y Rendimiento
Módulo 11: Frameworks y Librerías de Java
- Introducción a los Frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Pruebas Avanzadas con Mockito
- Librerías Esenciales del Ecosistema
