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, jcmd y 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).

El ecosistema Java por capas: sistema operativo, JVM, Java SE, lenguajes y frameworks, con Android aparte

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.util existen, 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 ves javax.persistence o javax.servlet, el tutorial es de antes de 2022 y tendrás que adaptarlo. En este curso todo es jakarta.* (Spring Boot 4.1 usa Jakarta EE 11).

1.3. ¿Por qué sigue siendo Java tan usado?

  • Compatibilidad hacia atrás: un .jar compilado 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).

Línea temporal de versiones de Java de la 8 a la 27 y diagrama de soporte de las LTS

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_HOME debe apuntar a la carpeta del JDK (no a bin). Maven, Gradle y muchas otras herramientas la usan para encontrar Java. Compruébala con echo $env:JAVA_HOME en PowerShell o echo $JAVA_HOME en 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: enum con 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(...) escribe 30,00 (con coma) y la concatenación "Total VIP: " + 200.0 escribe 200.0 (con punto). formatted, printf y String.format usan la configuración regional (locale) del sistema, que aquí es español; la conversión de un double a 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 clase public de 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.

Dos módulos de aplicación: com.empresa.app requiere com.empresa.modelo, que exporta un paquete y encapsula otro; ambos dependen de los módulos de la plataforma

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 como jlink (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 var cuando 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 con yield.
  • Cuando el switch es una expresión, debe ser exhaustivo: con un enum basta con cubrir todas las constantes; con int o String hace falta default.

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(), no getX()), equals, hashCode y toString. Sus atributos son private final y la clase es implícitamente final.

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 List mutable, 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 con permits. Cada subclase permitida debe ser final, sealed o non-sealed. Los records son implícitamente final, 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 switch de 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 case importa: se evalúan de arriba abajo. Si pones case Integer i antes de case 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 main de instancia (sin static, sin public y 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ódulo java.base y dispone de la clase java.lang.IO para 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.java se compilan en memoria y se ejecutan; desde Java 22 (JEP 458) el lanzador encuentra además otras clases .java del 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 module puede provocar ambigüedades: si importas dos módulos que tienen una clase con el mismo nombre (por ejemplo java.awt.List y java.util.List al importar java.desktop y java.base), tendrás que añadir un import explícito de la clase que quieres. En proyectos grandes, los import de 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

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-9 puedes 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: jpackage solo genera instaladores para el sistema operativo en el que se ejecuta (no puedes crear un .msi desde 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.print para 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

Mapa del curso con los cinco bloques y sus 25 módulos

Figura 4. Recorrido del Curso de Java Avanzado.

El curso sigue un orden deliberado:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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étodo conPrecio(double) que devuelva una copia con otro precio (los records son inmutables).
  • En main, crea tres productos de la tabla productos de 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 €:

  1. Muestra el saldo tras cada movimiento con Gatherers.scan.
  2. 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