Módulo 00: Introducción
Objetivos del módulo
- Situar Java dentro de su ecosistema: Java SE, Jakarta EE, Spring, Android y los lenguajes de la JVM
- Entender el ciclo de lanzamiento semestral, qué es una versión LTS y por qué el curso usa Java 25 LTS
- Conocer las principales distribuciones del JDK y elegir una con criterio
- Repasar los conocimientos de Java y POO que se dan por sabidos
- Recorrer, con ejemplos ejecutados, las novedades del lenguaje desde Java 9 hasta Java 25
- Manejar las herramientas del JDK:
jshell,javadoc,jar,jdeps,jlink,jpackage,jcmdy JFR - Tener una visión de conjunto del curso y de cómo vas a trabajar en él
1. El ecosistema Java
Cuando alguien dice “programo en Java” puede estar hablando de cosas muy distintas: una aplicación de escritorio, una API REST con Spring Boot que atiende millones de peticiones, una app Android o un proceso de big data que corre en un clúster. Todas comparten el lenguaje, pero no necesariamente la misma plataforma.
📘 Concepto: Java es a la vez un lenguaje (la sintaxis que escribes), una plataforma (la máquina virtual y la biblioteca estándar que ejecutan tu código) y un ecosistema (miles de bibliotecas, frameworks y herramientas construidos encima).
Figura 1. El ecosistema Java por capas.
1.1. Las piezas del ecosistema
| Pieza | Qué es | Ejemplos | ¿Lo verás en el curso? |
|---|---|---|---|
| JVM | Máquina virtual que ejecuta bytecode (.class), con compilador JIT y recolector de basura | HotSpot, OpenJ9, GraalVM | Sí, de forma transversal |
| Java SE | Standard Edition: lenguaje + API estándar (java.util, java.io, java.net, java.sql…) + herramientas | JDK 25 | Sí, bloques 1 y 2 |
| Jakarta EE | Especificaciones para aplicaciones empresariales (antes Java EE, cedido por Oracle a la Fundación Eclipse en 2017) | Servlet, JPA, Bean Validation, CDI, JAX-RS | Sí: JPA (Hibernate) y Servlet bajo Spring |
| Spring | El framework empresarial más usado: inversión de control, web, datos, seguridad | Spring Framework 7, Spring Boot 4.1 | Sí, bloque 4 |
| Otros frameworks | Alternativas orientadas a la nube y al arranque rápido | Quarkus, Micronaut, Helidon | Solo se mencionan |
| Android | Plataforma móvil que usa Java o Kotlin como lenguaje, pero con su propio runtime (ART) y su propia API | Android SDK | No |
| Lenguajes de la JVM | Otros lenguajes que compilan a bytecode y conviven con Java | Kotlin, Scala, Groovy, Clojure | No (pero Gradle usa Kotlin) |
⚠️ Importante: Android no es Java SE. En Android escribes código Java o Kotlin, pero se compila a formato DEX y se ejecuta en el Android Runtime (ART), no en una JVM. Muchas clases de
java.utilexisten, pero las novedades de Java 21 o 25 llegan tarde o nunca. Lo que aprendas aquí de lenguaje te sirve; lo de la plataforma, no siempre.
1.2. Jakarta EE: el cambio de javax a jakarta
Hasta Java EE 8 los paquetes empresariales se llamaban javax.persistence, javax.servlet… Cuando Oracle cedió Java EE a la Fundación Eclipse, la marca “Java” no se pudo usar y la plataforma pasó a llamarse Jakarta EE. A partir de Jakarta EE 9 los paquetes se renombraron a jakarta.*:
// Código antiguo (Java EE, Spring Boot 2, Hibernate 5)
import javax.persistence.Entity;
// Código actual (Jakarta EE 9+, Spring Boot 3 y 4, Hibernate 6 y 7)
import jakarta.persistence.Entity;
💡 Consejo: Cuando busques ejemplos en Internet, fíjate en los
import. Si vesjavax.persistenceojavax.servlet, el tutorial es de antes de 2022 y tendrás que adaptarlo. En este curso todo esjakarta.*(Spring Boot 4.1 usa Jakarta EE 11).
1.3. ¿Por qué sigue siendo Java tan usado?
- Compatibilidad hacia atrás: un
.jarcompilado hace 15 años suele seguir funcionando en el JDK 25. - Rendimiento: el compilador JIT de HotSpot optimiza el código en caliente; los recolectores modernos (G1, ZGC) manejan montones de cientos de GB con pausas mínimas.
- Ecosistema: Maven Central aloja millones de versiones de bibliotecas.
- Evolución constante: desde 2017 hay versión nueva cada seis meses, y el lenguaje ha cambiado más en los últimos ocho años que en los quince anteriores. Este módulo lo demuestra.
2. Versiones y ciclo de lanzamiento
2.1. Del “cuando esté listo” al tren semestral
Hasta Java 9 las versiones salían cuando estaban terminadas: Java 6 (2006), Java 7 (2011), Java 8 (2014), Java 9 (2017). Las esperas eran larguísimas y cada versión traía un aluvión de cambios difíciles de adoptar.
Desde Java 10 (marzo de 2018) se sigue un tren de lanzamientos: sale una versión nueva cada seis meses, en marzo y en septiembre, con lo que esté terminado. Lo que no llega, espera al siguiente tren.
📘 Concepto: Una versión LTS (Long-Term Support) es una versión que los proveedores se comprometen a mantener con parches de seguridad durante años. Las demás versiones solo reciben actualizaciones durante seis meses, hasta que sale la siguiente. Desde Java 17, hay una LTS cada dos años (cada cuatro versiones).
Figura 2. Ciclo de lanzamientos y soporte de las versiones LTS.
2.2. Tabla de versiones LTS
| Versión | Publicación | Soporte Premier de Oracle | Soporte extendido de Oracle | Novedades estrella |
|---|---|---|---|---|
| Java 8 | marzo 2014 | marzo 2022 | diciembre 2030 | Lambdas, streams, Optional, java.time, métodos default |
| Java 11 | septiembre 2018 | septiembre 2023 | enero 2032 | var en lambdas, HttpClient, métodos nuevos de String, ejecutar .java directamente |
| Java 17 | septiembre 2021 | septiembre 2026 | septiembre 2029 | Records, clases selladas, text blocks, switch expressions, instanceof con patrón |
| Java 21 | septiembre 2023 | septiembre 2028 | septiembre 2031 | Hilos virtuales, pattern matching en switch, record patterns, colecciones secuenciadas |
| Java 25 | septiembre 2025 | septiembre 2030 | septiembre 2033 | Ficheros fuente compactos, IO, import module, constructores flexibles, scoped values |
Y las versiones no LTS posteriores:
| Versión | Publicación | Estado en octubre de 2026 |
|---|---|---|
| Java 26 | 17 de marzo de 2026 | Sin soporte desde septiembre de 2026 (sustituida por la 27) |
| Java 27 | 15 de septiembre de 2026 | Versión actual, no LTS: recibirá parches hasta marzo de 2027 |
| Java 29 | previsiblemente septiembre de 2027 | Será la siguiente LTS si se mantiene el ritmo de dos años |
⚠️ Importante: Las fechas de soporte de la tabla son las de Oracle. Otros proveedores (Adoptium, Amazon, Microsoft, Azul, Red Hat…) publican sus propios calendarios, normalmente con plazos parecidos para las LTS. Lo que no cambia es la idea: en producción se usan versiones LTS, y las versiones intermedias sirven para probar lo que viene.
2.3. ¿Por qué el curso usa Java 25 y no Java 27?
- Java 25 es la LTS más reciente: es la que vas a encontrar en empresas que actualicen en los próximos años.
- Spring Boot 4.1, Hibernate 7 y las herramientas del curso están probadas con ella.
- Todo lo que aprendas funciona igual en Java 27: el lenguaje es compatible hacia adelante.
Java 27 (la actual) no trae cambios del lenguaje definitivos que afecten al curso: sus novedades son sobre todo de rendimiento, seguridad y características en preview (en la sección 9 tienes la lista).
2.4. JEP, preview e incubator
Cada cambio en Java se describe en un JEP (JDK Enhancement Proposal), un documento numerado y público en openjdk.org/jeps. Cuando en este curso leas “JEP 444”, es la propuesta que introdujo los hilos virtuales.
Antes de ser definitivas, muchas características pasan por fases:
| Fase | Qué significa | Cómo se usa |
|---|---|---|
| Incubator | API experimental en un módulo jdk.incubator.* | --add-modules jdk.incubator.vector |
| Preview | Característica del lenguaje o de la API completa pero que aún puede cambiar | javac --release 25 --enable-preview y java --enable-preview |
| Final (estándar) | Parte de la especificación, sin opciones especiales | Directamente |
# Compilar y ejecutar con características preview (por ejemplo, concurrencia estructurada en Java 25)
javac --release 25 --enable-preview Tareas.java
java --enable-preview Tareas
# Con el lanzador de código fuente también vale:
java --enable-preview --source 25 Tareas.java
💡 Consejo: No uses características preview en código de producción: pueden cambiar o desaparecer en la siguiente versión. Son para probarlas y dar opinión. En este curso solo aparecen cuando se dice expresamente (por ejemplo, la concurrencia estructurada del módulo 07, que sigue en preview en Java 25, 26 y 27).
2.5. Saber qué versión tienes
java -version
javac -version
Salida real en el contenedor que se usa para probar todo el código de este curso:
openjdk version "25.0.4.1" 2026-08-18 LTS
OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS)
OpenJDK 64-Bit Server VM Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS, mixed mode, sharing)
El número sigue el formato FEATURE.INTERIM.UPDATE.PATCH: 25 es la versión, 0 el interino (siempre 0 en la práctica), 4 la cuarta actualización trimestral de seguridad y 1 un parche de emergencia sobre ella. Las actualizaciones de seguridad salen en enero, abril, julio y octubre.
Desde el propio código también puedes consultarlo:
System.out.println(Runtime.version()); // 25.0.4.1+1-LTS
System.out.println(Runtime.version().feature()); // 25
3. Distribuciones del JDK
Java es OpenJDK: un proyecto de código abierto (licencia GPL con Classpath Exception). Varias empresas compilan ese código, lo prueban con el TCK (Technology Compatibility Kit) y publican su distribución. Funcionalmente son casi idénticas; cambian el soporte, la licencia de los binarios y algún extra.
| Distribución | Proveedor | Características |
|---|---|---|
| Eclipse Temurin | Fundación Eclipse (Adoptium) | Gratuita, muy popular, imágenes Docker oficiales eclipse-temurin. Recomendada para el curso |
| Oracle OpenJDK | Oracle (jdk.java.net) | Gratuita, pero solo se actualiza durante seis meses por versión |
| Oracle JDK | Oracle | Binarios de Oracle; gratuitos bajo condiciones (licencia NFTC) durante un periodo limitado en cada LTS; revisa la licencia antes de usarla en producción |
| Amazon Corretto | Amazon | Gratuita, con soporte largo; habitual en AWS |
| Microsoft Build of OpenJDK | Microsoft | Gratuita; habitual en Azure |
| Azul Zulu | Azul | Gratuita con soporte de pago; muchas plataformas |
| BellSoft Liberica | BellSoft | Gratuita; variantes con JavaFX incluido |
| Red Hat build of OpenJDK | Red Hat | Integrada en RHEL y OpenShift |
| GraalVM | Oracle / comunidad | JDK con compilador Graal y generación de ejecutables nativos (native-image) |
💡 Consejo: Para estudiar, cualquier distribución sirve. Elige Temurin 25 y no le des más vueltas. En una empresa, la distribución la decide normalmente el equipo de sistemas en función del soporte contratado.
3.1. Instalación
Windows (con el gestor de paquetes winget):
winget install EclipseAdoptium.Temurin.25.JDK
# Cierra y abre la terminal y comprueba
java -version
También puedes descargar el instalador .msi de adoptium.net: marca la opción de configurar JAVA_HOME y añadir Java al PATH.
Linux y macOS con SDKMAN!, que permite tener varias versiones instaladas y cambiar entre ellas:
sdk list java # muestra los identificadores disponibles
sdk install java 25.0.4-tem # usa el identificador exacto que aparezca en la lista
sdk use java 25.0.4-tem # cambia la versión en la terminal actual
Desde IntelliJ IDEA: en el asistente de nuevo proyecto, en el desplegable JDK eliges Download JDK…, la versión 25 y el proveedor Eclipse Temurin. El IDE lo descarga y lo configura.
⚠️ Importante: La variable de entorno
JAVA_HOMEdebe apuntar a la carpeta del JDK (no abin). Maven, Gradle y muchas otras herramientas la usan para encontrar Java. Compruébala conecho $env:JAVA_HOMEen PowerShell oecho $JAVA_HOMEen Bash.
3.2. Java con Docker (sin instalar nada)
Si tienes Docker, puedes ejecutar cualquier versión de Java sin instalarla. Así se han comprobado todos los ejemplos del curso:
# Ejecuta Hola.java de la carpeta actual con el JDK 25
docker run --rm -v "$PWD:/w" -w /w eclipse-temurin:25 java Hola.java
# Imagen con Maven y JDK 25 (la usarás en el módulo 01)
docker run --rm -v "$PWD:/w" -w /w maven:3.9-eclipse-temurin-25 mvn -v
En PowerShell, sustituye $PWD por ${PWD}.
4. Repaso rápido de lo que se da por sabido
Este curso continúa el de Fundamentos. Antes de seguir, comprueba que dominas estos conceptos; si alguno te suena raro, repásalo en el Curso de Java: Fundamentos de Programación.
| Tema | Lo que debes saber |
|---|---|
| Sintaxis básica | Tipos primitivos y envoltorios, operadores, if, switch, bucles, String y StringBuilder |
| Métodos | Parámetros, valor de retorno, sobrecarga, recursividad, paso por valor de referencias |
| POO | Clases, objetos, constructores, this, encapsulación, static, final |
| Herencia y polimorfismo | extends, super, sobrescritura con @Override, clases abstractas, interfaces, instanceof |
| Clases especiales | enum con atributos y métodos, clases internas y anónimas, Object (equals, hashCode, toString) |
| Excepciones | try/catch/finally, try-with-resources, comprobadas y no comprobadas, crear excepciones propias |
| Colecciones | List, Set, Map y sus implementaciones, Iterator, Comparable y Comparator básicos, genéricos básicos (List<String>) |
| Ficheros | Lectura y escritura de texto con Files o BufferedReader/BufferedWriter |
| Herramientas | Un IDE, depuración, JUnit 5 básico, Git, diagramas UML de clases |
💻 Ejemplo: Este programa resume en 90 líneas casi todo lo anterior:
enumcon atributo, interfaz, clase abstracta, herencia, polimorfismo, colecciones genéricas y excepciones. Si lo entiendes entero, estás preparado.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// Repaso rápido de lo que ya sabes del curso de Fundamentos
public class Repaso {
// Enumerado con atributo y constructor
enum Categoria {
BASICO(0.0), PREMIUM(0.10), VIP(0.20);
private final double descuento;
Categoria(double descuento) { this.descuento = descuento; }
double getDescuento() { return descuento; }
}
// Interfaz: un contrato
interface Facturable {
double importe();
}
// Clase abstracta con estado común
static abstract class Cliente implements Facturable {
private final String nombre;
protected final Categoria categoria;
Cliente(String nombre, Categoria categoria) {
if (nombre == null || nombre.isBlank()) {
throw new IllegalArgumentException("El nombre es obligatorio");
}
this.nombre = nombre;
this.categoria = categoria;
}
String getNombre() { return nombre; }
@Override
public String toString() {
return "%s (%s) -> %.2f €".formatted(nombre, categoria, importe());
}
}
// Herencia y polimorfismo
static class ClienteParticular extends Cliente {
private final double cuota;
ClienteParticular(String nombre, Categoria cat, double cuota) {
super(nombre, cat);
this.cuota = cuota;
}
@Override
public double importe() { return cuota * (1 - categoria.getDescuento()); }
}
static class ClienteEmpresa extends Cliente {
private final int licencias;
ClienteEmpresa(String nombre, Categoria cat, int licencias) {
super(nombre, cat);
this.licencias = licencias;
}
@Override
public double importe() { return licencias * 25.0 * (1 - categoria.getDescuento()); }
}
public static void main(String[] args) {
List<Cliente> clientes = new ArrayList<>();
clientes.add(new ClienteParticular("Ana", Categoria.BASICO, 30));
clientes.add(new ClienteEmpresa("Talleres Martín", Categoria.VIP, 10));
clientes.add(new ClienteParticular("Luis", Categoria.PREMIUM, 30));
// Colecciones con genéricos
Map<Categoria, Double> totalPorCategoria = new HashMap<>();
for (Cliente c : clientes) {
System.out.println(c); // polimorfismo
totalPorCategoria.merge(c.categoria, c.importe(), Double::sum);
}
System.out.println("Total VIP: " + totalPorCategoria.get(Categoria.VIP));
// Excepciones
try {
new ClienteParticular(" ", Categoria.BASICO, 10);
} catch (IllegalArgumentException e) {
System.out.println("Error controlado: " + e.getMessage());
}
}
}
Salida:
Ana (BASICO) -> 30,00 €
Talleres Martín (VIP) -> 200,00 €
Luis (PREMIUM) -> 27,00 €
Total VIP: 200.0
Error controlado: El nombre es obligatorio
📘 Concepto: Fíjate en que
"%.2f".formatted(...)escribe30,00(con coma) y la concatenación"Total VIP: " + 200.0escribe200.0(con punto).formatted,printfyString.formatusan la configuración regional (locale) del sistema, que aquí es español; la conversión de undoublea texto con+siempre usa punto. Lo verás muchas veces en las salidas del curso.
Aparecen dos detalles que quizá no viste en Fundamentos y que se explican en los módulos 02 y 03: Map.merge y la referencia a método Double::sum.
5. Novedades de Java 9 a Java 11
A partir de aquí recorremos lo que ha cambiado en el lenguaje y en la API desde Java 8. No hace falta que lo memorices todo ahora: la mayoría de estas características se usan a lo largo del curso y se profundiza en ellas en su módulo correspondiente.
5.1. El sistema de módulos (JPMS, Java 9)
📘 Concepto: Un módulo es un conjunto de paquetes con un nombre y un descriptor,
module-info.java, que declara qué necesita (requires) y qué ofrece al exterior (exports). Es un nivel de encapsulación por encima del paquete: una clasepublicde un paquete no exportado no es accesible desde otros módulos.
Antes de Java 9, cualquier clase public de cualquier .jar del classpath era accesible para todo el mundo, y el propio JDK era un bloque monolítico. El proyecto Jigsaw (JEP 261 y relacionados) dividió el JDK en unos 70 módulos y permitió a las aplicaciones hacer lo mismo.
Figura 3. Módulos, requires y exports.
Vamos a construir el ejemplo de la figura. La estructura de carpetas sigue la convención de “un directorio por módulo”:
modular/
└── src/
├── com.empresa.modelo/
│ ├── module-info.java
│ └── com/empresa/modelo/
│ ├── Empleado.java
│ └── interno/
│ └── Validador.java
└── com.empresa.app/
├── module-info.java
└── com/empresa/app/
└── Main.java
Los dos descriptores:
/**
* Módulo con el modelo de datos de la empresa.
*/
module com.empresa.modelo {
exports com.empresa.modelo; // paquete público: otros módulos pueden usarlo
// com.empresa.modelo.interno NO se exporta: queda encapsulado
}
module com.empresa.app {
requires com.empresa.modelo; // dependencia explícita
requires java.logging; // módulo de la plataforma (java.base es implícito)
}
El código de las clases:
package com.empresa.modelo;
import com.empresa.modelo.interno.Validador;
/** Empleado de la empresa. */
public record Empleado(int id, String nombre, double salario) {
public Empleado {
Validador.comprobarSalario(salario);
}
}
package com.empresa.modelo.interno;
/** Clase pública, pero en un paquete no exportado: solo visible dentro del módulo. */
public final class Validador {
private Validador() {}
public static void comprobarSalario(double salario) {
if (salario < 0) throw new IllegalArgumentException("Salario negativo");
}
}
package com.empresa.app;
import com.empresa.modelo.Empleado;
import java.util.logging.Logger;
public class Main {
private static final Logger LOG = Logger.getLogger(Main.class.getName());
public static void main(String[] args) {
Empleado e = new Empleado(100, "Ana", 6500);
System.out.println(e);
System.out.println("Módulo de Main: " + Main.class.getModule().getName());
System.out.println("Módulo de Empleado: " + Empleado.class.getModule().getName());
LOG.info("Aplicación modular arrancada");
}
}
Compilamos todos los módulos de una vez con --module-source-path y ejecutamos indicando el module path (-p o --module-path) y el módulo principal (-m módulo/clase):
javac -d out --module-source-path src -m com.empresa.app
java --module-path out -m com.empresa.app/com.empresa.app.Main
Empleado[id=100, nombre=Ana, salario=6500.0]
Módulo de Main: com.empresa.app
Módulo de Empleado: com.empresa.modelo
Oct 04, 2026 11:25:20 AM com.empresa.app.Main main
INFO: Aplicación modular arrancada
javac ha dejado cada módulo en su carpeta:
out/com.empresa.app/com/empresa/app/Main.class
out/com.empresa.app/module-info.class
out/com.empresa.modelo/com/empresa/modelo/Empleado.class
out/com.empresa.modelo/com/empresa/modelo/interno/Validador.class
out/com.empresa.modelo/module-info.class
Puedes inspeccionar un módulo con --describe-module:
java --module-path out --describe-module com.empresa.modelo
com.empresa.modelo file:///w/out/com.empresa.modelo/
exports com.empresa.modelo
requires java.base mandated
contains com.empresa.modelo.interno
contains indica un paquete que está dentro pero no se exporta. Si otro módulo intenta usar Validador, el compilador lo impide aunque la clase sea public:
ilegal/src/com.empresa.malo/com/empresa/malo/X.java:2: error: package com.empresa.modelo.interno is not visible
import com.empresa.modelo.interno.Validador;
^
(package com.empresa.modelo.interno is declared in module com.empresa.modelo, which does not export it)
1 error
Las directivas de module-info.java más habituales:
| Directiva | Significado |
|---|---|
requires M; | Este módulo necesita el módulo M |
requires transitive M; | Lo necesito y, además, quien me requiera a mí también podrá usar M |
requires static M; | Necesario al compilar, opcional al ejecutar |
exports p; | Los tipos públicos del paquete p son accesibles desde otros módulos |
exports p to M; | Exportación calificada: solo para el módulo M |
opens p; | Permite la reflexión profunda sobre p en ejecución (la necesitan Hibernate, Jackson o Spring) |
uses S; / provides S with Impl; | Consumo y provisión de servicios con ServiceLoader |
⚠️ Importante: ¿Hay que modularizar todas las aplicaciones? No. Si no escribes
module-info.java, tu código va al classpath como siempre (en el llamado “módulo sin nombre”) y funciona perfectamente. La mayoría de aplicaciones Spring Boot no son modulares. Lo importante es entender el concepto, porque el JDK sí es modular y eso permite herramientas comojlink(sección 10) y explica errores del tipo “module X does not open package Y” que verás con bibliotecas que usan reflexión.
java --list-modules | head -8
java.base@25.0.4.1
java.compiler@25.0.4.1
java.datatransfer@25.0.4.1
java.desktop@25.0.4.1
java.instrument@25.0.4.1
java.logging@25.0.4.1
java.management@25.0.4.1
java.management.rmi@25.0.4.1
El JDK 25 de Temurin trae 69 módulos (java.* son estándar; jdk.*, específicos del JDK).
5.2. Colecciones inmutables, var y otras mejoras (Java 9-11)
💻 Ejemplo: Un recorrido por las mejoras más útiles de estas versiones.
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public class Java9a11 {
public static void main(String[] args) {
// Java 9: métodos de fábrica para colecciones inmutables
List<String> lenguajes = List.of("Java", "Kotlin", "Scala");
Set<Integer> primos = Set.of(2, 3, 5, 7);
Map<String, Integer> edades = Map.of("Ana", 34, "Luis", 41);
System.out.println(lenguajes + " " + primos.size() + " " + edades.get("Ana"));
try {
lenguajes.add("Groovy");
} catch (UnsupportedOperationException e) {
System.out.println("List.of es inmutable: " + e.getClass().getSimpleName());
}
// Java 9: Optional.ifPresentOrElse, Optional.or, Optional.stream
Optional<String> vacio = Optional.empty();
vacio.ifPresentOrElse(System.out::println, () -> System.out.println("Sin valor"));
// Java 10: inferencia de tipos de variables locales
var mapa = Map.of("uno", 1, "dos", 2); // Map<String, Integer>
for (var entrada : mapa.entrySet()) {
if (entrada.getValue() == 1) System.out.println("var: " + entrada.getKey());
}
// Java 10: copias inmutables
var copia = List.copyOf(lenguajes);
System.out.println("copyOf: " + copia);
// Java 11: nuevos métodos de String
String texto = " Hola, Java ";
System.out.println("[" + texto.strip() + "] isBlank=" + " ".isBlank());
System.out.println("ab".repeat(3));
"línea 1\nlínea 2\nlínea 3".lines().map(String::toUpperCase).forEach(System.out::println);
// Java 11: var en parámetros de lambdas (útil para anotarlos)
java.util.function.BinaryOperator<Integer> suma = (var a, var b) -> a + b;
System.out.println("suma = " + suma.apply(2, 3));
}
}
[Java, Kotlin, Scala] 4 34
List.of es inmutable: UnsupportedOperationException
Sin valor
var: uno
copyOf: [Java, Kotlin, Scala]
[Hola, Java] isBlank=true
ababab
LÍNEA 1
LÍNEA 2
LÍNEA 3
suma = 5
var: cuándo sí y cuándo no
var (Java 10, JEP 286) le pide al compilador que deduzca el tipo de una variable local a partir de su inicialización. Java sigue siendo un lenguaje de tipado estático: la variable tiene un tipo fijo, simplemente no lo escribes.
var lista = new ArrayList<String>(); // ArrayList<String>
lista.add("hola");
// lista = 42; // ERROR: el tipo es ArrayList<String>
| Uso | ¿Se puede? |
|---|---|
Variable local con inicialización: var x = 10; | Sí |
Variable del for y del for-each: for (var e : lista) | Sí |
Recurso de try-with-resources: try (var in = Files.newBufferedReader(p)) | Sí |
Parámetros de una lambda: (var a, var b) -> a + b (Java 11) | Sí |
| Atributo de clase, parámetro de método, tipo de retorno | No |
Sin inicializar (var x;) o inicializada a null | No |
💡 Consejo: Usa
varcuando el tipo es evidente al leer la línea (var clientes = new HashMap<String, List<Cliente>>();) y evítalo cuando oculta información (var r = servicio.procesar();¿qué devuelve?). La legibilidad manda.
Otras novedades de estas versiones
| Versión | Novedad | Dónde la verás |
|---|---|---|
| 9 | jshell, la consola interactiva de Java (JEP 222) | Sección 10 |
| 9 | Métodos private en interfaces | Módulo 03 |
| 9 | Stream.takeWhile, dropWhile, iterate con condición, Optional.stream | Módulo 04 |
| 10 | List.copyOf, Collectors.toUnmodifiableList | Módulo 02 |
| 11 | HttpClient estándar (JEP 321) | Módulo 09 |
| 11 | Ejecutar un fichero fuente sin compilar: java Hola.java (JEP 330) | Todo el curso |
| 11 | Files.readString y Files.writeString | Módulo 08 |
6. Novedades de Java 12 a Java 17
6.1. Switch expressions (Java 14)
El switch clásico tenía dos problemas: el fall-through (olvidar un break es un error clásico) y que no devolvía valores. Las expresiones switch (JEP 361) resuelven ambos con la sintaxis de flecha ->.
import java.time.DayOfWeek;
public class SwitchYTexto {
// Java 14: switch como expresión, con flechas y sin "break"
static int horasLaborables(DayOfWeek dia) {
return switch (dia) {
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY -> 8;
case FRIDAY -> 7;
case SATURDAY, SUNDAY -> 0;
}; // exhaustivo: si falta un caso, no compila
}
static String clasificar(int nota) {
return switch (nota) {
case 10, 9 -> "Sobresaliente";
case 8, 7 -> "Notable";
case 6 -> "Bien";
case 5 -> "Suficiente";
default -> {
// un bloque necesita "yield" para devolver el valor
String r = nota >= 0 && nota < 5 ? "Insuficiente" : "Nota no válida";
yield r;
}
};
}
public static void main(String[] args) {
int total = 0;
for (DayOfWeek d : DayOfWeek.values()) total += horasLaborables(d);
System.out.println("Horas semanales: " + total);
System.out.println(clasificar(9) + ", " + clasificar(3) + ", " + clasificar(12));
// Java 15: bloques de texto
String json = """
{
"nombre": "Ana",
"puesto": "DIRECTOR GENERAL",
"salario": 6500
}
""";
System.out.print(json);
String sql = """
SELECT nombre, salario \
FROM empleados \
WHERE salario > %d""".formatted(3000); // "\" une líneas
System.out.println(sql);
}
}
Horas semanales: 39
Sobresaliente, Insuficiente, Nota no válida
{
"nombre": "Ana",
"puesto": "DIRECTOR GENERAL",
"salario": 6500
}
SELECT nombre, salario FROM empleados WHERE salario > 3000
Claves de las expresiones switch:
- Varias etiquetas separadas por comas:
case 10, 9 ->. - Con
->no hay fall-through: solo se ejecuta la rama elegida. - Si una rama necesita varias sentencias, usa un bloque
{ ... }y devuelve el valor conyield. - Cuando el
switches una expresión, debe ser exhaustivo: con unenumbasta con cubrir todas las constantes; conintoStringhace faltadefault.
6.2. Bloques de texto (Java 15)
Un bloque de texto (JEP 378) es un literal de cadena multilínea que empieza con """ seguido de salto de línea. El compilador elimina la sangría común (tomando como referencia la línea menos sangrada, incluida la de las """ de cierre), así que puedes indentarlo con el código.
| Secuencia | Efecto |
|---|---|
\ al final de línea | Une la línea con la siguiente (no inserta salto) |
\s | Un espacio que no se recorta |
""" de cierre en su propia línea | El texto termina con \n |
""" de cierre pegado al texto | El texto no termina con \n |
💡 Consejo: Los bloques de texto brillan con SQL, JSON, HTML y plantillas. Combinados con
formatted(...)sustituyen a muchas concatenaciones ilegibles. En el módulo 12 (JDBC) escribirás así las consultas.
6.3. Records (Java 16)
📘 Concepto: Un record (JEP 395) es una clase inmutable pensada para transportar datos. Declaras sus componentes y el compilador genera el constructor, los métodos de acceso (
x(), nogetX()),equals,hashCodeytoString. Sus atributos sonprivate finaly la clase es implícitamentefinal.
record Punto(double x, double y) {
// constructor compacto: validación
Punto {
if (Double.isNaN(x) || Double.isNaN(y)) throw new IllegalArgumentException("NaN");
}
double distanciaAlOrigen() { return Math.hypot(x, y); }
}
Un record puede: tener métodos, métodos y atributos static, constructores adicionales (que deleguen en el canónico), implementar interfaces y declararse dentro de otra clase o incluso dentro de un método. No puede: heredar de otra clase (ya extiende java.lang.Record), declarar atributos de instancia adicionales ni modificar sus componentes.
| Clase tradicional (Java 8) | Record (Java 16) |
|---|---|
~40 líneas: atributos, constructor, getters, equals, hashCode, toString | 1 línea |
| Mutable salvo que lo cuides | Inmutable (superficialmente) |
p.getX() | p.x() |
⚠️ Importante: La inmutabilidad de un record es superficial: si un componente es una
Listmutable, el record no puede cambiar a qué lista apunta, pero alguien podría modificar la lista. Haz una copia defensiva en el constructor compacto:lineas = List.copyOf(lineas);.
Los records se usan muchísimo en el resto del curso: como DTO en las APIs REST, como resultados de consultas, como claves de mapas y en todos los ejemplos de streams.
6.4. Pattern matching con instanceof (Java 16)
// Antes
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.toUpperCase());
}
// Java 16 (JEP 394): el patrón declara la variable y hace el cast
if (obj instanceof String s && s.length() > 3) {
System.out.println(s.toUpperCase());
}
La variable s solo existe donde el compilador sabe que el patrón ha encajado (por eso puede usarse tras &&, pero no tras ||).
6.5. Clases e interfaces selladas (Java 17)
📘 Concepto: Una clase o interfaz sellada (
sealed, JEP 409) declara exactamente qué clases pueden heredar de ella conpermits. Cada subclase permitida debe serfinal,sealedonon-sealed. Los records son implícitamentefinal, así que encajan perfectamente.
sealed interface Forma permits Circulo, Rectangulo, Triangulo {}
record Circulo(Punto centro, double radio) implements Forma {}
record Rectangulo(Punto esquina, double ancho, double alto) implements Forma {}
record Triangulo(double base, double altura) implements Forma {}
¿Para qué sirve limitar la herencia? Para que el compilador conozca todas las variantes posibles. Combinado con el pattern matching de Java 21 (siguiente sección), el compilador comprueba que tu switch trata todos los casos y te avisa si mañana añades Hexagono y olvidas actualizarlo. Es la forma moderna de modelar “un dato puede ser A, B o C” (lo que otros lenguajes llaman tipos suma o tipos algebraicos).
6.6. Otras novedades de estas versiones
| Versión | Novedad |
|---|---|
| 12 | String.indent, String.transform, Collectors.teeing (módulo 04) |
| 14 | Helpful NullPointerExceptions (JEP 358): el mensaje indica qué era null, p. ej. Cannot invoke "String.length()" because "nombre" is null (si se compiló sin información de depuración, como con java Fichero.java, verás "<local1>" en lugar del nombre) |
| 15 | String.formatted |
| 16 | Stream.toList() (módulo 04), jpackage (sección 10) |
| 17 | Nuevos generadores de números aleatorios (RandomGenerator), se refuerza la encapsulación de los internos del JDK |
7. Novedades de Java 18 a Java 21
7.1. Pattern matching en switch y record patterns (Java 21)
Java 21 completó una idea que llevaba varias versiones gestándose: un switch que no solo compara valores, sino que comprueba tipos y desestructura objetos (JEP 441 y JEP 440).
💻 Ejemplo: Las formas selladas de la sección anterior, ahora con
switchde patrones.
import java.util.List;
public class RecordsYSellados {
// Java 16: record = clase de datos inmutable (constructor, getters, equals, hashCode, toString)
record Punto(double x, double y) {
// constructor compacto: validación
Punto {
if (Double.isNaN(x) || Double.isNaN(y)) throw new IllegalArgumentException("NaN");
}
double distanciaAlOrigen() { return Math.hypot(x, y); }
}
// Java 17: jerarquía sellada: solo estas clases pueden implementar Forma
sealed interface Forma permits Circulo, Rectangulo, Triangulo {}
record Circulo(Punto centro, double radio) implements Forma {}
record Rectangulo(Punto esquina, double ancho, double alto) implements Forma {}
record Triangulo(double base, double altura) implements Forma {}
// Java 21: pattern matching en switch + record patterns. Exhaustivo sin default
static double area(Forma f) {
return switch (f) {
case Circulo c -> Math.PI * c.radio() * c.radio();
case Rectangulo(Punto p, double an, double al) -> an * al;
case Triangulo(double b, double h) -> b * h / 2;
};
}
static String describir(Object o) {
return switch (o) {
case null -> "es null";
case Integer i when i > 100 -> "entero grande " + i; // guarda "when"
case Integer i -> "entero " + i;
case String s when s.isBlank() -> "cadena vacía";
case String s -> "cadena de " + s.length() + " caracteres";
case Circulo(Punto(var x, var y), var r) -> "círculo en (" + x + "," + y + ") r=" + r;
default -> "otro: " + o.getClass().getSimpleName();
};
}
public static void main(String[] args) {
Punto p = new Punto(3, 4);
System.out.println(p + " distancia=" + p.distanciaAlOrigen());
System.out.println("equals: " + p.equals(new Punto(3, 4)));
// Java 16: pattern matching con instanceof (sin cast manual)
Object obj = "Java 25";
if (obj instanceof String s && s.length() > 3) {
System.out.println("instanceof: " + s.toUpperCase());
}
List<Forma> formas = List.of(new Circulo(p, 1), new Rectangulo(p, 2, 3), new Triangulo(4, 5));
for (Forma f : formas) System.out.printf("%s -> %.2f%n", f.getClass().getSimpleName(), area(f));
for (Object o : new Object[]{null, 500, 7, " ", "hola", new Circulo(new Punto(1, 2), 3), 2.5}) {
System.out.println(describir(o));
}
}
}
Punto[x=3.0, y=4.0] distancia=5.0
equals: true
instanceof: JAVA 25
Circulo -> 3,14
Rectangulo -> 6,00
Triangulo -> 10,00
es null
entero grande 500
entero 7
cadena vacía
cadena de 4 caracteres
círculo en (1.0,2.0) r=3.0
otro: Double
Lo que debes extraer de este ejemplo:
| Elemento | Ejemplo | Explicación |
|---|---|---|
| Patrón de tipo | case Circulo c -> | Si f es un Circulo, lo tienes en c ya convertido |
| Record pattern | case Triangulo(double b, double h) -> | Extrae los componentes directamente |
| Patrones anidados | case Circulo(Punto(var x, var y), var r) | Desestructura a varios niveles; var infiere el tipo |
| Guarda | case Integer i when i > 100 -> | Condición adicional para que el caso encaje |
case null | case null -> | Sin él, un switch sobre null lanza NullPointerException |
| Exhaustividad | area(Forma f) sin default | Como Forma es sellada, el compilador sabe que no hay más casos |
⚠️ Importante: El orden de los
caseimporta: se evalúan de arriba abajo. Si ponescase Integer iantes decase Integer i when i > 100, el segundo nunca se alcanzaría y el compilador da un error de “dominancia”.
7.2. Hilos virtuales (Java 21)
📘 Concepto: Un hilo virtual (JEP 444) es un hilo ligero gestionado por la JVM, no por el sistema operativo. Cuando se bloquea esperando (E/S,
sleep, una consulta a la base de datos), la JVM lo “aparca” y reutiliza el hilo del sistema operativo para otro hilo virtual. Puedes crear millones; con hilos tradicionales (“de plataforma”) el límite práctico son unos pocos miles.
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.SequencedMap;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class Java21 {
public static void main(String[] args) throws Exception {
// ---------- Hilos virtuales (Java 21, JEP 444) ----------
AtomicInteger completadas = new AtomicInteger();
Instant inicio = Instant.now();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1)); // simula una espera de E/S
completadas.incrementAndGet();
return null;
});
}
} // close() espera a que terminen todas las tareas
long ms = Duration.between(inicio, Instant.now()).toMillis();
System.out.println("Tareas completadas: " + completadas.get());
System.out.println("¿Menos de 3 segundos? " + (ms < 3000));
Thread t = Thread.ofVirtual().name("virtual-1").start(
() -> System.out.println("Soy " + Thread.currentThread().getName()
+ ", virtual=" + Thread.currentThread().isVirtual()));
t.join();
// ---------- Colecciones secuenciadas (Java 21, JEP 431) ----------
List<String> lista = new ArrayList<>(List.of("b", "c"));
lista.addFirst("a");
lista.addLast("d");
System.out.println(lista + " primero=" + lista.getFirst() + " último=" + lista.getLast());
System.out.println("al revés: " + lista.reversed());
SequencedMap<String, Integer> pedidos = new LinkedHashMap<>();
pedidos.put("5001", 3);
pedidos.put("5002", 1);
pedidos.putFirst("5000", 7);
System.out.println(pedidos + " primera entrada=" + pedidos.firstEntry());
}
}
Tareas completadas: 10000
¿Menos de 3 segundos? true
Soy virtual-1, virtual=true
[a, b, c, d] primero=a último=d
al revés: [d, c, b, a]
{5000=7, 5001=3, 5002=1} primera entrada=5000=7
10 000 tareas que esperan un segundo cada una terminan en algo más de un segundo, porque se ejecutan todas a la vez en hilos virtuales. Con un pool de 200 hilos de plataforma tardaría unos 50 segundos. Los hilos virtuales son la gran revolución de la concurrencia en Java para aplicaciones que pasan la mayor parte del tiempo esperando (servidores web, microservicios); los estudiarás a fondo en el módulo 07.
💡 Consejo: Los hilos virtuales no hacen más rápido el cálculo puro: si una tarea consume CPU todo el tiempo, sigues limitado por el número de núcleos. Su ventaja es la espera barata.
7.3. Colecciones secuenciadas (Java 21)
Antes de Java 21, obtener el último elemento dependía de la colección: lista.get(lista.size() - 1), deque.getLast(), sortedSet.last(), y en un LinkedHashSet ni siquiera había forma directa. Las nuevas interfaces SequencedCollection, SequencedSet y SequencedMap (JEP 431) unifican estas operaciones: getFirst(), getLast(), addFirst(), addLast(), removeFirst(), removeLast() y reversed(). Las verás con detalle (y con su diagrama de jerarquía) en el módulo 02.
7.4. Otras novedades de estas versiones
| Versión | Novedad |
|---|---|
| 18 | UTF-8 por defecto en toda la API (JEP 400): se acabaron los problemas de tildes al leer ficheros en Windows |
| 18 | jwebserver: servidor web estático mínimo para pruebas (JEP 408) |
| 18 | @snippet en Javadoc para incluir código de ejemplo (JEP 413) |
| 19-20 | Vistas previas de hilos virtuales y patrones |
| 21 | Generational ZGC (JEP 439), StringBuilder.repeat, String.indexOf con rango, Character.isEmoji |
8. Novedades de Java 22 a Java 25
8.1. Variables y patrones sin nombre _ (Java 22)
A veces el lenguaje te obliga a declarar una variable que no vas a usar: la excepción de un catch, el elemento de un bucle que solo cuenta, un componente de un record pattern que no te interesa. Desde Java 22 (JEP 456) puedes llamarla _ para dejar claro que se ignora.
import java.util.List;
import java.util.Map;
import java.util.stream.Gatherers;
import java.util.stream.Stream;
public class Java22a24 {
sealed interface Evento permits Alta, Baja, Cambio {}
record Alta(String usuario, String fecha) implements Evento {}
record Baja(String usuario, String motivo) implements Evento {}
record Cambio(String usuario, String campo, String valor) implements Evento {}
public static void main(String[] args) {
// ---------- Variables y patrones sin nombre "_" (Java 22, JEP 456) ----------
List<Evento> eventos = List.of(new Alta("ana", "2026-01-10"),
new Cambio("luis", "email", "l@x.es"), new Baja("eva", "traslado"));
for (Evento e : eventos) {
String texto = switch (e) {
case Alta(String u, _) -> "Alta de " + u; // no me interesa la fecha
case Baja(var u, _) -> "Baja de " + u;
case Cambio(var u, var campo, _) -> u + " cambia " + campo;
};
System.out.println(texto);
}
int contador = 0;
for (Evento _ : eventos) contador++; // variable que no se usa
System.out.println("Eventos: " + contador);
try {
Integer.parseInt("doce");
} catch (NumberFormatException _) { // excepción que no se usa
System.out.println("No es un número");
}
Map<String, Integer> mapa = Map.of("a", 1);
mapa.forEach((_, v) -> System.out.println("valor " + v)); // parámetro que no se usa
// ---------- Stream gatherers (Java 24, JEP 485) ----------
List<List<Integer>> grupos = Stream.of(1, 2, 3, 4, 5, 6, 7)
.gather(Gatherers.windowFixed(3))
.toList();
System.out.println("windowFixed(3): " + grupos);
List<List<Integer>> ventanas = Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.windowSliding(2))
.toList();
System.out.println("windowSliding(2): " + ventanas);
List<Integer> acumulados = Stream.of(10, 20, 30, 40)
.gather(Gatherers.scan(() -> 0, Integer::sum))
.toList();
System.out.println("scan (sumas parciales): " + acumulados);
}
}
Alta de ana
luis cambia email
Baja de eva
Eventos: 3
No es un número
valor 1
windowFixed(3): [[1, 2, 3], [4, 5, 6], [7]]
windowSliding(2): [[1, 2], [2, 3], [3, 4], [4, 5]]
scan (sumas parciales): [10, 30, 60, 100]
8.2. Stream gatherers (Java 24)
Los gatherers (JEP 485) son operaciones intermedias de streams definidas por el usuario, mediante el método gather(...). La clase Gatherers trae algunas ya hechas: agrupar en ventanas fijas (windowFixed) o deslizantes (windowSliding), acumular (scan, fold) o aplicar una función de forma concurrente (mapConcurrent). Antes había que recurrir a bucles o a trucos con colectores. Los estudiarás en el módulo 04, incluido cómo escribir uno propio.
8.3. Ficheros fuente compactos y métodos main de instancia (Java 25)
📘 Concepto: Desde Java 25 (JEP 512) un programa puede ser un fichero fuente compacto: sin declarar la clase, con un método
mainde instancia (sinstatic, sinpublicy sin parámetros) y con métodos y campos sueltos. El compilador crea una clase implícita alrededor. Además, un fichero compacto importa automáticamente todo el módulojava.basey dispone de la clasejava.lang.IOpara leer y escribir en la consola.
// Java 25 (JEP 512): fichero fuente compacto y método main de instancia.
// No hay declaración de clase: el compilador crea una clase implícita.
// Los ficheros compactos importan automáticamente el módulo java.base.
String saludo = "Hola desde un fichero compacto";
void main() {
IO.println(saludo);
List<String> cursos = List.of("Fundamentos", "Avanzado"); // sin import: java.base ya está importado
for (String c : cursos) {
IO.println("Curso de Java " + c);
}
IO.println("Doble de 21 = " + doble(21));
}
int doble(int n) {
return n * 2;
}
java Compacto.java
Hola desde un fichero compacto
Curso de Java Fundamentos
Curso de Java Avanzado
Doble de 21 = 42
La clase IO tiene métodos estáticos print, println y readln (que muestra un mensaje y lee una línea). Es ideal para scripts, pruebas rápidas y para empezar a programar. En aplicaciones reales con paquetes, Maven y Spring seguirás usando clases normales con public static void main(String[] args), que es lo que verás en el resto del curso.
💡 Consejo: Los ficheros compactos son perfectos para pequeñas utilidades que antes escribías en Python o Bash. Con
java Script.javase compilan en memoria y se ejecutan; desde Java 22 (JEP 458) el lanzador encuentra además otras clases.javadel mismo directorio, así que un “programa de varios ficheros” también funciona sin compilar.
8.4. Importar módulos completos (Java 25)
import module java.base; (JEP 511) importa de golpe todos los paquetes exportados por un módulo. Para scripts y prototipos ahorra decenas de líneas de import:
import module java.base; // Java 25 (JEP 511): importa todos los paquetes exportados por java.base
public class Java25 {
// ---------- Cuerpos de constructor flexibles (Java 25, JEP 513) ----------
static class Persona {
protected final String nombre;
Persona(String nombre) {
this.nombre = nombre;
System.out.println("Constructor de Persona: " + nombre);
}
}
static class Empleado extends Persona {
private final double salario;
Empleado(String nombre, double salario) {
// Antes de Java 25 esto no compilaba: había que llamar a super() en la primera línea
if (salario < 0) {
throw new IllegalArgumentException("Salario negativo: " + salario);
}
String normalizado = nombre.strip().toUpperCase();
this.salario = salario; // se pueden inicializar campos propios antes de super
super(normalizado);
}
}
// ---------- Scoped values (Java 25, JEP 506) ----------
private static final ScopedValue<String> USUARIO = ScopedValue.newInstance();
static void procesarPeticion() {
// cualquier método llamado dentro del ámbito puede leer el valor sin pasarlo como parámetro
System.out.println("Procesando para " + USUARIO.get());
}
public static void main(String[] args) {
// Gracias a "import module java.base" no hay que importar List, Map, LocalDate...
List<LocalDate> fechas = List.of(LocalDate.of(2025, 9, 16), LocalDate.of(2026, 9, 15));
Map<String, LocalDate> lanzamientos = Map.of("JDK 25", fechas.get(0), "JDK 27", fechas.get(1));
System.out.println(new TreeMap<>(lanzamientos));
Empleado e = new Empleado(" ana garcía ", 6500);
System.out.println("Empleado creado: " + e.nombre);
try {
new Empleado("Luis", -1);
} catch (IllegalArgumentException ex) {
System.out.println("Rechazado antes de llamar a super: " + ex.getMessage());
}
ScopedValue.where(USUARIO, "ana").run(Java25::procesarPeticion);
System.out.println("¿Ligado fuera del ámbito? " + USUARIO.isBound());
}
}
{JDK 25=2025-09-16, JDK 27=2026-09-15}
Constructor de Persona: ANA GARCÍA
Empleado creado: ANA GARCÍA
Rechazado antes de llamar a super: Salario negativo: -1.0
Procesando para ana
¿Ligado fuera del ámbito? false
⚠️ Importante:
import modulepuede provocar ambigüedades: si importas dos módulos que tienen una clase con el mismo nombre (por ejemplojava.awt.Listyjava.util.Listal importarjava.desktopyjava.base), tendrás que añadir unimportexplícito de la clase que quieres. En proyectos grandes, losimportde clase concretos (los que gestiona el IDE) siguen siendo la opción más clara.
8.5. Cuerpos de constructor flexibles (Java 25)
Hasta Java 24, la llamada a super(...) o this(...) tenía que ser la primera sentencia del constructor. Eso obligaba a trucos (métodos estáticos auxiliares) para validar o preparar los argumentos. Desde Java 25 (JEP 513) puedes escribir sentencias antes de super(...), con una restricción: en ese “prólogo” no puedes usar this (ni llamar a métodos de instancia ni leer campos), aunque sí puedes asignar campos propios. Lo has visto en el ejemplo anterior: se valida el salario y se normaliza el nombre antes de construir la parte Persona.
💡 Consejo: Asignar los campos propios antes de
super(...)evita un error sutil clásico: que el constructor de la superclase llame a un método sobrescrito que lee un campo de la subclase todavía sin inicializar.
8.6. Scoped values (Java 25)
Un ScopedValue (JEP 506) permite compartir un dato inmutable con todos los métodos llamados dentro de un ámbito (y con los hilos hijos de la concurrencia estructurada), sin pasarlo como parámetro. Es la alternativa moderna, más segura y eficiente, a ThreadLocal, pensada para hilos virtuales: el usuario autenticado de una petición, el identificador de una transacción… Se ve en el módulo 07.
8.7. Otras novedades de estas versiones y lo que sigue en preview
| Versión | Novedad | Estado |
|---|---|---|
| 22 | API de funciones y memoria externa (FFM, JEP 454): llamar a código C sin JNI | Final |
| 22 | Lanzar programas de varios ficheros fuente (JEP 458) | Final |
| 23 | Comentarios de documentación en Markdown con /// (JEP 467) | Final |
| 24 | API para leer y generar ficheros .class (Class-File API, JEP 484) | Final |
| 24 | Los hilos virtuales ya no se “anclan” al usar synchronized (JEP 491) | Final |
| 25 | Cabeceras de objeto compactas (JEP 519), API de derivación de claves (JEP 510) | Final |
| 25 | Concurrencia estructurada (JEP 505) | Preview (quinta) |
| 25 | Tipos primitivos en patrones, instanceof y switch (JEP 507) | Preview |
9. Después de Java 25: JDK 26 y JDK 27
No forman parte del curso, pero conviene saber qué ha llegado después de la LTS:
| Versión | JEP destacados |
|---|---|
| JDK 26 (marzo 2026) | JEP 517: HTTP/3 en HttpClient; JEP 500: preparar que final signifique realmente final (avisos al modificar campos final por reflexión); JEP 504: se elimina la API de applets; JEP 516: caché de objetos AOT con cualquier GC; concurrencia estructurada (sexta preview), lazy constants (segunda preview) |
| JDK 27 (septiembre 2026) | JEP 523: G1 como recolector por defecto en todos los entornos; JEP 534: cabeceras de objeto compactas por defecto; JEP 527: intercambio de claves híbrido poscuántico en TLS 1.3; JEP 536: redacción de datos en JFR; concurrencia estructurada (séptima preview), patrones con tipos primitivos (quinta preview), lazy constants (tercera preview) |
📘 Concepto: Fíjate en que ninguna característica nueva del lenguaje se ha vuelto definitiva en 26 ni en 27: las novedades son de rendimiento, seguridad y plataforma. Por eso el código que escribes con Java 25 está perfectamente al día.
10. Herramientas del JDK
El JDK no es solo javac y java. Su carpeta bin trae más de treinta herramientas; estas son las que todo programador profesional debe conocer:
| Herramienta | Para qué sirve | Ejemplo |
|---|---|---|
javac | Compilar | javac -d out Hola.java |
java | Ejecutar clases, .jar, módulos o directamente ficheros fuente | java Hola.java |
jshell | Consola interactiva (REPL) para probar código | jshell |
javadoc | Generar documentación HTML a partir de los comentarios | javadoc -d docs ... |
jar | Empaquetar clases y recursos en un .jar | jar --create --file app.jar ... |
jdeps | Analizar dependencias entre paquetes y módulos | jdeps -s app.jar |
jlink | Crear un runtime de Java a medida con solo los módulos necesarios | jlink --add-modules ... |
jpackage | Crear instaladores nativos (.msi, .exe, .deb, .rpm, .dmg) | jpackage --input target ... |
jcmd | Enviar órdenes de diagnóstico a una JVM en marcha | jcmd <pid> GC.heap_info |
jconsole | Monitorizar una JVM con interfaz gráfica (memoria, hilos, CPU) | jconsole |
jfr | Leer grabaciones de Java Flight Recorder | jfr summary grabacion.jfr |
jwebserver | Servidor HTTP estático mínimo (Java 18) | jwebserver -p 8000 |
javap | Desensamblar un .class | javap -c Hola.class |
10.1. jshell: experimentar sin crear proyectos
jshell evalúa expresiones y declaraciones de una en una y muestra el resultado. Perfecto para comprobar cómo se comporta un método de la API sin crear una clase:
$ jshell
| Welcome to JShell -- Version 25.0.4.1
| For an introduction type: /help intro
jshell> int x = 21
x ==> 21
jshell> x * 2
$2 ==> 42
jshell> var nombres = List.of("Ana", "Luis", "Marta")
nombres ==> [Ana, Luis, Marta]
jshell> nombres.stream().map(String::toUpperCase).toList()
$4 ==> [ANA, LUIS, MARTA]
jshell> record Punto(int x, int y) {}
| created record Punto
jshell> new Punto(3, 4)
$6 ==> Punto[x=3, y=4]
jshell> "Java".repeat(3)
$7 ==> "JavaJavaJava"
jshell> /vars
| int x = 21
| int $2 = 42
| List<String> nombres = [Ana, Luis, Marta]
| List<String> $4 = [ANA, LUIS, MARTA]
| Punto $6 = Punto[x=3, y=4]
| String $7 = "JavaJavaJava"
jshell> /exit
| Goodbye
Observa que no hace falta punto y coma, que las expresiones sin variable se guardan en $2, $4… y que java.util ya está importado. Comandos útiles: /vars, /methods, /list, /imports, /open Fichero.java, /save sesion.jsh, /help y la tecla Tab para autocompletar.
10.2. jar: empaquetar
Un .jar es un fichero ZIP con las clases compiladas, los recursos y un fichero META-INF/MANIFEST.MF con metadatos (por ejemplo, la clase principal). Con el ejemplo modular:
mkdir mods
jar --create --file mods/modelo.jar -C out/com.empresa.modelo .
jar --create --file mods/app.jar --main-class com.empresa.app.Main -C out/com.empresa.app .
jar --describe-module --file mods/app.jar
com.empresa.app jar:file:///w/mods/app.jar!/module-info.class
requires com.empresa.modelo
requires java.base mandated
requires java.logging
contains com.empresa.app
main-class com.empresa.app.Main
Como el jar conoce su clase principal, basta con indicar el módulo:
java -p mods -m com.empresa.app
Para un proyecto no modular el equivalente es jar --create --file app.jar --main-class paquete.Main -C out . y se ejecuta con java -jar app.jar. En el módulo 01 verás que Maven hace todo esto por ti.
jdeps muestra de qué depende un jar:
jdeps --module-path mods -s mods/app.jar
com.empresa.app -> com.empresa.modelo
com.empresa.app -> java.base
com.empresa.app -> java.logging
10.3. jlink: un Java a medida
Como el JDK es modular, jlink puede generar un runtime que contenga solo los módulos que tu aplicación necesita (más tu propia aplicación) y un lanzador:
jlink --module-path mods --add-modules com.empresa.app \
--launcher empresa=com.empresa.app/com.empresa.app.Main \
--strip-debug --no-header-files --no-man-pages --output runtime
du -sh runtime /opt/java/openjdk
runtime/bin/java --list-modules
runtime/bin/empresa
56M runtime
252M /opt/java/openjdk
com.empresa.app
com.empresa.modelo
java.base@25.0.4.1
java.logging@25.0.4.1
Empleado[id=100, nombre=Ana, salario=6500.0]
Módulo de Main: com.empresa.app
Módulo de Empleado: com.empresa.modelo
Oct 04, 2026 11:28:34 AM com.empresa.app.Main main
INFO: Aplicación modular arrancada
El runtime ocupa 56 MB frente a los 252 MB del JDK completo y no necesita que el usuario tenga Java instalado. Es muy útil para imágenes Docker pequeñas.
💡 Consejo: Con la opción
--compress zip-9puedes reducir todavía más el tamaño a cambio de un arranque algo más lento.
10.4. jpackage: instaladores nativos
jpackage (Java 16) parte de tu aplicación (y de un runtime creado con jlink, que genera automáticamente) y produce un instalador nativo del sistema donde se ejecuta: .msi o .exe en Windows, .deb o .rpm en Linux, .dmg o .pkg en macOS.
# Desde un proyecto no modular empaquetado en target/app.jar
jpackage --name Empresa --input target --main-jar app.jar \
--main-class com.empresa.app.Main --type msi --win-menu --win-shortcut
⚠️ Importante:
jpackagesolo genera instaladores para el sistema operativo en el que se ejecuta (no puedes crear un.msidesde Linux) y, según el tipo, necesita herramientas externas (en Windows, WiX Toolset para.msi). Por eso no se muestra aquí su salida: el contenedor del curso es Linux.
10.5. javadoc: documentar
javadoc convierte los comentarios /** ... */ (y desde Java 23 también los comentarios Markdown ///) en un sitio web de documentación como el de la API oficial:
javadoc -quiet -d docs --module-source-path src --module com.empresa.modelo,com.empresa.app
Genera docs/index.html con el índice, la búsqueda y una página por clase. Además avisa de lo que falta por documentar:
src/com.empresa.app/module-info.java:1: warning: no comment
module com.empresa.app {
^
5 warnings
Las etiquetas básicas son @param, @return, @throws, @see, @since y {@code ...}:
/**
* Calcula el salario neto aplicando la retención indicada.
*
* @param bruto salario bruto mensual en euros; no puede ser negativo
* @param retencion porcentaje de retención entre 0 y 100
* @return el salario neto
* @throws IllegalArgumentException si algún argumento está fuera de rango
* @since 1.2
*/
public static double neto(double bruto, double retencion) { ... }
10.6. Diagnóstico: jcmd, jconsole y JFR
Cuando una aplicación en producción va lenta o consume demasiada memoria, estas herramientas permiten mirar dentro de la JVM sin pararla. Lanzamos un programa que se queda esperando un minuto y lo inspeccionamos:
java Servidor.java &
jcmd -l # lista las JVM en marcha (PID y clase principal)
jcmd 102 VM.version
jcmd 102 GC.heap_info
102 jdk.compiler/com.sun.tools.javac.launcher.SourceLauncher Servidor.java
124 jdk.jcmd/sun.tools.jcmd.JCmd -l
102:
OpenJDK 64-Bit Server VM version 25.0.4.1+1-LTS
JDK 25.0.4.1
102:
garbage-first heap total reserved 12337152K, committed 786432K, used 26725K [0x000000050f000000, 0x0000000800000000)
region size 8192K, 4 young (32768K), 0 survivors (0K)
(El programa aparece como SourceLauncher Servidor.java porque lo lanzamos directamente desde el código fuente.) Otras órdenes de jcmd: Thread.print (volcado de todos los hilos, para encontrar bloqueos), GC.class_histogram (qué clases ocupan más memoria), VM.flags y help para ver la lista completa.
Java Flight Recorder (JFR) es un registrador de eventos integrado en la JVM con un impacto mínimo en el rendimiento: graba uso de CPU, asignación de memoria, pausas del GC, bloqueos, E/S… Puedes activarlo al arrancar (java -XX:StartFlightRecording=duration=60s,filename=app.jfr -jar app.jar) o en caliente con jcmd:
jcmd 102 JFR.start duration=5s filename=/tmp/grab.jfr
jfr summary /tmp/grab.jfr
Version: 2.1
Chunks: 1
Start: 2026-10-04 11:34:27 (UTC)
Duration: 5 s
Event Type Count Size (bytes)
=============================================================
jdk.BooleanFlag 496 15709
jdk.ActiveSetting 376 10097
jdk.ModuleRequire 156 1716
jdk.LongFlag 137 4537
Las grabaciones .jfr se analizan cómodamente con JDK Mission Control (JMC, aplicación gráfica que se descarga aparte) o con el profiler de IntelliJ IDEA. jconsole es una herramienta gráfica más sencilla que se conecta a una JVM local o remota (por JMX) y muestra gráficas de memoria, hilos y clases cargadas; para visualizarla necesitas un entorno gráfico, así que pruébala en tu equipo con jconsole mientras se ejecuta cualquier programa.
💡 Consejo: En el módulo 07 usarás
jcmd <pid> Thread.printpara diagnosticar un interbloqueo, y en el 16, JFR para ver el impacto de las consultas de Hibernate.
11. Mapa del curso y forma de trabajo
Figura 4. Recorrido del Curso de Java Avanzado.
El curso sigue un orden deliberado:
- Bloque 1 · Java moderno (00-05). Herramientas de construcción y el Java “de hoy”: genéricos a fondo, programación funcional, streams y fechas. Es la base de todo lo demás: Spring y Hibernate están llenos de lambdas, genéricos y
Optional. - Bloque 2 · Concurrencia, E/S y diseño (06-11). Hilos, executors, hilos virtuales, ficheros y JSON, red y HTTP, patrones de diseño y pruebas profesionales.
- Bloque 3 · Acceso a datos (12-16). JDBC, transacciones, pools, el patrón DAO, JPA e Hibernate sobre la base de datos EMPRESA en MySQL que ya conoces del Curso de Bases de Datos.
- Bloque 4 · Spring (17-23). El núcleo de Spring, Spring Boot, Spring Data JPA, APIs REST, aplicaciones web con Thymeleaf, seguridad, pruebas y despliegue con Docker.
- Bloque 5 · Proyecto final (24). Una aplicación completa que lo integra todo.
11.1. Entorno de trabajo recomendado
| Herramienta | Versión | Para qué |
|---|---|---|
| JDK | Temurin 25 LTS | Compilar y ejecutar |
| IDE | IntelliJ IDEA (Community o Ultimate; gratuita con licencia educativa) | Editar, depurar, Maven/Gradle integrados |
| Maven | 3.9.x (o el wrapper mvnw de cada proyecto) | Construir proyectos (módulo 01) |
| Docker Desktop | Actual | MySQL, Testcontainers y despliegue |
| Git | Actual | Control de versiones |
💡 Consejo: A partir del módulo 01 todos los ejemplos son proyectos Maven. Hasta entonces puedes ejecutar los ejemplos de este módulo directamente con
java Fichero.java, sin IDE ni compilación previa.
12. Ejercicios
Ejercicio 1: De clase tradicional a record
Convierte esta clase en un record Producto(int id, String nombre, double precio, int stock) que:
- Valide en el constructor compacto que el nombre no está vacío y que el precio y el stock no son negativos, y elimine los espacios sobrantes del nombre.
- Tenga un método
agotado()y un métodoconPrecio(double)que devuelva una copia con otro precio (los records son inmutables). - En
main, crea tres productos de la tablaproductosde la base de datos EMPRESA (6, 8 y 9), muéstralos ordenados por precio descendente, aplica una rebaja al monitor y prueba un precio negativo.
Ver solución
import java.util.Comparator;
import java.util.List;
public class Ej1Producto {
record Producto(int id, String nombre, double precio, int stock) {
Producto {
if (nombre == null || nombre.isBlank()) throw new IllegalArgumentException("Nombre vacío");
if (precio < 0) throw new IllegalArgumentException("Precio negativo: " + precio);
if (stock < 0) throw new IllegalArgumentException("Stock negativo: " + stock);
nombre = nombre.strip(); // se puede normalizar un parámetro
}
boolean agotado() { return stock == 0; }
Producto conPrecio(double nuevoPrecio) { // "wither": devuelve una copia modificada
return new Producto(id, nombre, nuevoPrecio, stock);
}
}
public static void main(String[] args) {
List<Producto> productos = List.of(
new Producto(6, "Ratón inalámbrico", 19.90, 200),
new Producto(8, " Pack teclado y ratón ", 34.50, 0),
new Producto(9, "Monitor 24 FHD", 139.00, 35));
productos.stream()
.sorted(Comparator.comparingDouble(Producto::precio).reversed())
.forEach(p -> System.out.printf("%-22s %8.2f € %s%n",
p.nombre(), p.precio(), p.agotado() ? "AGOTADO" : "stock " + p.stock()));
Producto rebajado = productos.get(2).conPrecio(119.00);
System.out.println(rebajado);
System.out.println("¿Iguales? " + rebajado.equals(productos.get(2)));
try {
new Producto(99, "Erróneo", -5, 1);
} catch (IllegalArgumentException e) {
System.out.println("Error: " + e.getMessage());
}
}
}
Monitor 24 FHD 139,00 € stock 35
Pack teclado y ratón 34,50 € AGOTADO
Ratón inalámbrico 19,90 € stock 200
Producto[id=9, nombre=Monitor 24 FHD, precio=119.0, stock=35]
¿Iguales? false
Error: Precio negativo: -5.0
En el constructor compacto los parámetros (nombre, precio…) son variables que se asignan automáticamente a los campos al final, por eso puedes reasignar nombre = nombre.strip(). Los métodos “wither” como conPrecio son el patrón habitual para “modificar” objetos inmutables. El Comparator.comparingDouble(...).reversed() se explica en el módulo 02.
Ejercicio 2: Medios de pago sellados
Modela con una interfaz sellada Pago tres medios: Tarjeta(titular, importe, extranjera), Transferencia(iban, importe) y Bizum(telefono, importe). Escribe double comision(Pago) con un switch de patrones sin default:
- Tarjeta nacional: 1,5 %; extranjera: 3 %.
- Transferencia con IBAN español (empieza por
ES): gratis; internacional: 2,50 €. - Bizum: gratis, pero si supera 1000 € se lanza
IllegalArgumentException.
Ver solución
import java.util.List;
public class Ej2Pagos {
sealed interface Pago permits Tarjeta, Transferencia, Bizum {}
record Tarjeta(String titular, double importe, boolean extranjera) implements Pago {}
record Transferencia(String iban, double importe) implements Pago {}
record Bizum(String telefono, double importe) implements Pago {}
static double comision(Pago pago) {
return switch (pago) {
case Tarjeta(var _, var imp, var ext) when ext -> imp * 0.03; // extranjera: 3 %
case Tarjeta(var _, var imp, var _) -> imp * 0.015; // nacional: 1,5 %
case Transferencia(var iban, var _) when iban.startsWith("ES") -> 0;
case Transferencia t -> 2.50; // internacional
case Bizum b when b.importe() > 1000 ->
throw new IllegalArgumentException("Bizum máximo 1000 €");
case Bizum _ -> 0;
};
}
public static void main(String[] args) {
List<Pago> pagos = List.of(
new Tarjeta("Ana", 200, false),
new Tarjeta("John", 200, true),
new Transferencia("ES9121000418450200051332", 1500),
new Transferencia("FR7630006000011234567890189", 1500),
new Bizum("600100100", 50),
new Bizum("600100101", 2000));
for (Pago p : pagos) {
try {
System.out.printf("%-14s comisión %6.2f €%n", p.getClass().getSimpleName(), comision(p));
} catch (IllegalArgumentException e) {
System.out.println(p.getClass().getSimpleName() + " rechazado: " + e.getMessage());
}
}
}
}
Tarjeta comisión 3,00 €
Tarjeta comisión 6,00 €
Transferencia comisión 0,00 €
Transferencia comisión 2,50 €
Bizum comisión 0,00 €
Bizum rechazado: Bizum máximo 1000 €
Como Pago es sellada y cada tipo tiene un caso sin guarda al final (Tarjeta(var _, var imp, var _), Transferencia t, Bizum _), el switch es exhaustivo sin default. Si añades un cuarto medio de pago a permits, el método deja de compilar hasta que lo trates: ese es el valor de las jerarquías selladas. Observa el uso de _ (Java 22) para los componentes que no interesan y que una rama puede lanzar una excepción en lugar de devolver un valor.
Ejercicio 3: Informe HTML con bloques de texto
Dada una lista de empleados (record Empleado(id, nombre, puesto, salario)), genera una tabla HTML usando bloques de texto y formatted. Usa los empleados 100, 120 y 121 de la base de datos EMPRESA.
Ver solución
import java.util.List;
import java.util.stream.Collectors;
public class Ej3Informe {
record Empleado(int id, String nombre, String puesto, double salario) {}
static String aHtml(List<Empleado> empleados) {
String filas = empleados.stream()
.map(e -> """
<tr><td>%d</td><td>%s</td><td>%s</td><td>%.2f</td></tr>"""
.formatted(e.id(), e.nombre(), e.puesto(), e.salario()))
.collect(Collectors.joining("\n"));
return """
<table>
<tr><th>Id</th><th>Nombre</th><th>Puesto</th><th>Salario</th></tr>
%s
</table>""".formatted(filas);
}
public static void main(String[] args) {
List<Empleado> empleados = List.of(
new Empleado(100, "Ana", "DIRECTOR GENERAL", 6500),
new Empleado(120, "Pablo", "ANALISTA", 3500),
new Empleado(121, "Raquel", "PROGRAMADOR", 2800));
System.out.println(aHtml(empleados));
}
}
<table>
<tr><th>Id</th><th>Nombre</th><th>Puesto</th><th>Salario</th></tr>
<tr><td>100</td><td>Ana</td><td>DIRECTOR GENERAL</td><td>6500,00</td></tr>
<tr><td>120</td><td>Pablo</td><td>ANALISTA</td><td>3500,00</td></tr>
<tr><td>121</td><td>Raquel</td><td>PROGRAMADOR</td><td>2800,00</td></tr>
</table>
Fíjate en la sangría: en el bloque de la fila, las """ de cierre están pegadas al texto y no hay ninguna línea menos sangrada, así que toda la sangría se elimina y las filas salen pegadas al margen. Para que salieran con dos espacios, el texto debería tener dos espacios más que la línea de referencia. Controlar la sangría de los bloques de texto requiere práctica: ante la duda, imprime el resultado. El salario sale con coma porque formatted usa la configuración regional española.
Ejercicio 4: Hilos virtuales frente a un pool clásico
Simula 1000 peticiones a un servicio remoto que tarda 200 ms (Thread.sleep(200)). Mide cuánto se tarda en atenderlas con un Executors.newFixedThreadPool(50) y con Executors.newVirtualThreadPerTaskExecutor(). Usa try-with-resources para cerrar el executor (y esperar a las tareas).
Ver solución
import java.time.Duration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Ej4Virtuales {
// Simula una llamada a un servicio remoto que tarda 200 ms
static String consultar(int id) throws InterruptedException {
Thread.sleep(200);
return "respuesta " + id;
}
static long medir(ExecutorService executor, int peticiones) {
long inicio = System.nanoTime();
try (executor) { // Java 19+: ExecutorService es AutoCloseable
for (int i = 0; i < peticiones; i++) {
int id = i;
executor.submit(() -> consultar(id));
}
}
return Duration.ofNanos(System.nanoTime() - inicio).toMillis();
}
public static void main(String[] args) {
int n = 1_000;
long plataforma = medir(Executors.newFixedThreadPool(50), n);
long virtuales = medir(Executors.newVirtualThreadPerTaskExecutor(), n);
System.out.println("Pool de 50 hilos de plataforma: ~" + Math.round(plataforma / 100.0) / 10.0 + " s");
System.out.println("Hilos virtuales: ~" + Math.round(virtuales / 100.0) / 10.0 + " s");
System.out.println("Los virtuales son más rápidos: " + (virtuales < plataforma));
}
}
Salida (los tiempos varían según el equipo):
Pool de 50 hilos de plataforma: ~5.1 s
Hilos virtuales: ~1.3 s
Los virtuales son más rápidos: true
Con 50 hilos de plataforma, las 1000 tareas se atienden en tandas de 50: 1000 / 50 × 0,2 s = 4 s como mínimo. Con hilos virtuales se lanzan las 1000 a la vez y el total se acerca a los 200 ms de una sola espera más el coste de crear las tareas (aquí algo más de un segundo porque el contenedor arranca en frío y la JVM aún no ha optimizado el código). Desde Java 19, ExecutorService implementa AutoCloseable y close() espera a que terminen las tareas enviadas. La variable id es necesaria porque una lambda solo puede capturar variables effectively final (módulo 03).
Ejercicio 5: Saldo y lotes con gatherers
Dada una lista de movimientos bancarios (record Movimiento(concepto, importe)) y un saldo inicial de 500 €:
- Muestra el saldo tras cada movimiento con
Gatherers.scan. - Muestra los movimientos agrupados en lotes de 3 con
Gatherers.windowFixed.
Ver solución
import java.util.List;
import java.util.stream.Gatherers;
public class Ej5Gatherers {
record Movimiento(String concepto, double importe) {}
public static void main(String[] args) {
List<Movimiento> movimientos = List.of(
new Movimiento("Nómina", 2200), new Movimiento("Alquiler", -850),
new Movimiento("Luz", -64.30), new Movimiento("Supermercado", -112.45),
new Movimiento("Bizum recibido", 25), new Movimiento("Gasolina", -60),
new Movimiento("Seguro", -38.90));
// a) saldo después de cada movimiento (partiendo de 500 €)
List<Double> saldos = movimientos.stream()
.map(Movimiento::importe)
.gather(Gatherers.scan(() -> 500.0, Double::sum))
.toList();
for (int i = 0; i < movimientos.size(); i++) {
System.out.printf("%-15s %9.2f saldo %9.2f%n",
movimientos.get(i).concepto(), movimientos.get(i).importe(), saldos.get(i));
}
// b) procesar en lotes de 3 (por ejemplo, para enviarlos a un servidor)
movimientos.stream()
.gather(Gatherers.windowFixed(3))
.forEach(lote -> System.out.println("Lote de " + lote.size() + ": "
+ lote.stream().map(Movimiento::concepto).toList()));
}
}
Nómina 2200,00 saldo 2700,00
Alquiler -850,00 saldo 1850,00
Luz -64,30 saldo 1785,70
Supermercado -112,45 saldo 1673,25
Bizum recibido 25,00 saldo 1698,25
Gasolina -60,00 saldo 1638,25
Seguro -38,90 saldo 1599,35
Lote de 3: [Nómina, Alquiler, Luz]
Lote de 3: [Supermercado, Bizum recibido, Gasolina]
Lote de 1: [Seguro]
scan recibe un valor inicial (como Supplier) y una función que combina el acumulado con cada elemento, y emite cada resultado intermedio (a diferencia de reduce, que solo da el final). windowFixed(3) agrupa en listas de 3 y la última puede quedar incompleta. En un programa real, para dinero usarías BigDecimal en lugar de double.
Ejercicio 6: Calculadora de IMC en un fichero compacto
Escribe un fichero compacto de Java 25 (sin declarar clase) que pida con IO.readln el nombre, el peso y la altura, calcule el índice de masa corporal (peso / altura²) mediante un record Medida y lo clasifique: bajo peso (< 18,5), normal (< 25), sobrepeso (< 30) u obesidad.
Ver solución
// Fichero compacto (Java 25): se ejecuta con "java Ej6Imc.java"
record Medida(double pesoKg, double alturaM) {
double imc() { return pesoKg / (alturaM * alturaM); }
}
String clasificar(double imc) {
if (imc < 18.5) return "Bajo peso";
if (imc < 25) return "Normal";
if (imc < 30) return "Sobrepeso";
return "Obesidad";
}
void main() {
String nombre = IO.readln("Nombre: ");
double peso = Double.parseDouble(IO.readln("Peso (kg): "));
double altura = Double.parseDouble(IO.readln("Altura (m): "));
var m = new Medida(peso, altura);
IO.println("%s, tu IMC es %.1f: %s".formatted(nombre, m.imc(), clasificar(m.imc())));
}
Ejecución:
$ java Ej6Imc.java
Nombre: Marta
Peso (kg): 62
Altura (m): 1.68
Marta, tu IMC es 22,0: Normal
En un fichero compacto puedes declarar records, clases, métodos y campos al mismo nivel que main. IO.readln(mensaje) muestra el mensaje y devuelve la línea leída. Observa que la altura se escribe con punto (1.68) porque Double.parseDouble no depende de la configuración regional, mientras que la salida con %.1f usa coma.
Ejercicio 7 (propuesto): Biblioteca modular
Crea dos módulos: com.biblioteca.dominio (con un record Libro exportado y una clase GeneradorIsbn en un paquete interno no exportado) y com.biblioteca.app (con un main que cree varios libros y los muestre). Compílalos con --module-source-path, ejecútalos con -m, empaquétalos en dos jars modulares y genera con jlink un runtime con un lanzador llamado biblioteca. Comprueba con --list-modules qué módulos contiene y compara su tamaño con el del JDK.
Ejercicio 8 (propuesto): Expresiones switch sobre enum
Define un enum Mes con los doce meses y escribe, con expresiones switch, un método int dias(Mes m, boolean bisiesto) y otro String estacion(Mes m) (simplifica: invierno = diciembre a febrero, etc.). Comprueba que si borras un case el compilador da error porque el switch deja de ser exhaustivo.
Ejercicio 9 (propuesto): Expresiones aritméticas con tipos sellados
Modela expresiones aritméticas con una interfaz sellada Expr y los records Num(double v), Suma(Expr izq, Expr der), Mul(Expr izq, Expr der) y Neg(Expr e). Escribe con record patterns un método recursivo double evaluar(Expr e) y otro String mostrar(Expr e) que genere la expresión con paréntesis, por ejemplo ((2.0 + 3.0) * -(4.0)). Añade una simplificación: Mul(Num(0), _) vale 0 sin evaluar la otra parte.
Ejercicio 10 (propuesto): Constructores flexibles
Crea una clase CuentaBancaria(String iban, double saldoInicial) y una subclase CuentaJoven cuyo constructor reciba la fecha de nacimiento del titular y, antes de llamar a super(...), compruebe que es menor de 26 años (lanza IllegalArgumentException si no) y normalice el IBAN quitando espacios y pasándolo a mayúsculas. Explica en un comentario por qué antes de Java 25 habría hecho falta un método estático auxiliar.
Ejercicio 11 (propuesto): Explorando con jshell
Abre jshell y, sin crear ningún fichero, averigua: (a) qué devuelve "Hola".chars().sum(), (b) el resultado de List.of(3, 1, 2).reversed(), (c) qué excepción lanza Map.of("a", 1, "a", 2) y (d) qué imprime Runtime.version().feature(). Guarda la sesión con /save explorar.jsh y vuelve a cargarla con /open explorar.jsh en una sesión nueva.
Ejercicio 12 (propuesto): Diagnóstico con jcmd y JFR
Escribe un programa que cree una lista y le añada un millón de objetos String en un bucle que se repite cada segundo durante un minuto. Mientras se ejecuta: localiza su PID con jcmd -l, muestra el uso del heap con GC.heap_info, obtén el histograma de clases con GC.class_histogram (¿qué clase ocupa más memoria?) y graba 20 segundos de JFR. Analiza la grabación con jfr print --events jdk.GarbageCollection o ábrela con JDK Mission Control.
Resumen
| Concepto | Versión | Sintaxis o idea clave |
|---|---|---|
| LTS | 8, 11, 17, 21, 25 | Versión con soporte largo; una cada dos años. JDK 27 es la actual (no LTS) |
| JEP / preview | — | Propuesta de mejora numerada; preview requiere --enable-preview |
| Módulos (JPMS) | 9 | module m { requires x; exports p; } en module-info.java |
| Colecciones inmutables | 9 / 10 | List.of(...), Map.of(...), List.copyOf(...) |
var | 10 / 11 | var lista = new ArrayList<String>(); solo en variables locales |
| Switch expressions | 14 | int h = switch (d) { case LUNES -> 8; default -> { yield 0; } }; |
| Bloques de texto | 15 | """ ... """ con sangría automática |
| Records | 16 | record Punto(int x, int y) { Punto { /* validar */ } } |
instanceof con patrón | 16 | if (o instanceof String s) ... |
| Clases selladas | 17 | sealed interface Forma permits A, B {} |
Pattern matching en switch | 21 | case Circulo(Punto p, var r) when r > 0 -> ... |
| Hilos virtuales | 21 | Executors.newVirtualThreadPerTaskExecutor(), Thread.ofVirtual() |
| Colecciones secuenciadas | 21 | getFirst(), getLast(), reversed() |
| Sin nombre | 22 | catch (Exception _), case Alta(var u, _) |
| Gatherers | 24 | stream.gather(Gatherers.windowFixed(3)) |
| Ficheros compactos | 25 | void main() { IO.println("Hola"); } |
import module | 25 | import module java.base; |
| Constructores flexibles | 25 | Sentencias antes de super(...) sin usar this |
| Scoped values | 25 | ScopedValue.where(CLAVE, valor).run(...) |
| Herramientas | — | jshell, jar, javadoc, jdeps, jlink, jpackage, jcmd, jconsole, jfr |