Java saca una nueva versión LTS: JDK 25.

Cabe destacar que, no hace mucho tiempo, uno de los lenguajes referencia en el sector ha cumplido 30 años y, aunque no es el que más crece en su uso, si nos fijamos en las estadísticas, se sigue manteniendo y augura un futuro prometedor. Quién sabe, quizás este lenguaje sea el que más maduro llegue a la edad de “jubilación”.

Esta nueva LTS llega tras la JDK 24 (que llegó el 18 de marzo) y tendrá un soporte Premier de al menos 5 años. La versión LTS más reciente era la JDK 21 (21 septiembre de 2023). Es bueno tener en el radar el soporte que recibe la JDK que estamos usando por si necesitamos realizar una migración (soporte oracle).

En este post me gustaría repasar funcionalidades ya confirmadas que irán en esta actualización y algunas que están en una fase de preview, pero que suenan interesantes y no hay que perderlas de vista por si en algún momento son incorporadas.

Comparto el detalle de las características incorporadas por si alguien tiene interés en echarle un vistazo.

Características confirmadas

1 Compact Source Files and Instance Main Methods Jeps-512

Java se vuelve más amigable y sencillo de utilizar.

void main() {
    IO.println("Hola mundo");
}

Nos generará implícitamente:

final class __ImplicitClass__ {
    void main() { IO.println("Hola mundo"); }
}

Aquí puedes consultar la info completa de Jeps-512.

2 Flexible Constructor Bodies Jeps-513

Esta característica ya fue introducida en modo preview anteriormente (JDK 22) pero ya ha pasado al estado de release. Esto nos facilita meter lógica entre el constructor por defecto cuando estamos construyendo un objeto.

public class A extends B {
    private static int multiplier = 10;
    private final int childValue;

    public A(int baseValue) {
        // Prologue
        int computedValue = baseValue * multiplier;

        super(baseValue); // Initialize B

        // Epilogue
        this.childValue = computedValue;
    }
}

Aquí puedes consultar la info completa de Jeps-513.

3 Ahead-of-Time Method Profiling Jeps-515

Java, al iniciar la JVM, siempre ha tenido que recopilar datos y estadísticas de los métodos para decidir qué optimizar, algo que causa lentitud durante el tiempo de arranque.

Ahora podemos crear un perfil de ejecución (métodos más usados, cuántas veces se ejecutan, tipos de objetos...) y usarlo en posteriores arranques. Esto acelera notablemente la fase de calentamiento de la aplicación permitiendo al compilador JIT generar código nativo óptimo sin esperar a recolectar perfiles en tiempo real.

Se apoya en un escaneo de las ejecuciones previas para ser más eficiente. Caché de AOT (Ahead-of-Time).

Lo mejor es que no tenemos que hacer cambios en nuestro código, mejora el uso de nuestros recursos (20% aprox en tiempos de ejecución inicial).

Es flexible y evolutivo si el comportamiento cambia. La JVM sigue recolectando perfiles en caliente, combinando la información AOT + JIT dinámico, lo que garantiza que el rendimiento no se degrade si el comportamiento de la app cambia en producción.

Aquí puedes consultar la info completa deJeps-515.

4 Module Import Declarations Jeps-511

Si echamos la vista muy atrás, cuando empezó Java 9 (Project JITSAW), se hizo un cambio importante al sistema de módulos. Lo que aporta esta característica es importar todos los paquetes exportados por un módulo en una línea de código.

import module <nombre-del-módulo>;

¿Cómo nos afecta? Podemos hacer un importación tipo:

import module java.base;

Y tener acceso a todos los paquetes exportados (List, Map, Path…). Además, si el módulo depende transitivamente de otro, también es importado (ejemplo java.sql.*, java.xml…). Es “más parecido’”a usar un API completo.

Aquí puedes consultar la info completa de Jeps-511

5 Scoped Values Jeps-506

Es una alternativa moderna a ThreadLocal para Virtual Threads (jeps-487). Al final, es una forma segura de compartir datos entre métodos e hilos pensada para virtual threads. Como sabemos, ThreadLocal es mutable, se puede olvidar limpiarlo (leaks) y no se lleva bien con virtual threads.

Ejemplo:

public class ScopedValueExample {
    static final ScopedValue<String> USER = ScopedValue.newInstance();

    public static void main(String[] args) {
        ScopedValue.where(USER, "Alice").run(() -> {
            greetUser();
        });
    }

    static void greetUser() {
        System.out.println("Hello, " + USER.get());
    }
}

En este enlace tienes la info completa de Jeps-506.

Flags y optimizaciones de JVM

1 Compact Object Headers Jeps-519

Fue introducida de forma experimental en la JDK 24 y ya forma parte como feature estable. Reduce las header de los objetos de la JVM de 12 bytes hasta 16 bytes con punteros sin comprimir o 8 bytes. ¿Y en qué se traduce?

Como dato curioso, Amazon reporta hasta un 30% menos de CPU en algunos servicios.

Para utilizarlo es tan sencillo como poner (no está por defecto):

java -XX:+UseCompactObjectHeaders ...

Aquí podrás ver toda la info de Jeps-519.

2 Generational Shenandoah Jeps-521

Al igual que el caso anterior fue introducida en la versión JDK 24 y confirmada en esta, y cambia el funcionamiento del recolector de basura. Permite manejar objetos jóvenes y viejos de forma separada mejorando la eficiencia del recolector de basura.

Recordemos que antes, cuando el recolector de basura se activaba, tenía que recorrer todo el heap. La mayoría de objetos mueren rápido, por lo que se centra en estos objetos jóvenes y pasando por los viejos de vez en cuando.

Igual que el caso anterior, no está por defecto y hay que activarlo:

java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational ...

Más info sobre Jeps-521 en este enlace.

Características en preview o experimental

Estas características no están confirmadas pero son muy interesantes para tenerlas en el radar.

Structured Concurrency (Quinta Preview) Jeps-505

La idea es mejorar la observabilidad del código concurrente y tener un estilo de concurrencia que mitigue los riesgos que pueden salir por la cancelación de hilos, retrasos…

Se trata de un API que permite tratar grupos de tareas relacionadas que se ejecutan en diferentes hilos como una única unidad de trabajo, simplificando el manejo de errores y cancelaciones, mejorando la fiabilidad y aumentando la observabilidad del código concurrente.

import java.util.concurrent.ExecutionException;
import java.util.concurrent.StructuredTaskScope;

public class StructuredConcurrencyExample {
    public static void main(String[] args) {
        try (var scope = new StructuredTaskScope.ShutdownOnFailure<Void>()) {

            // Fork: iniciamos tareas concurrentes
            var task1 = scope.fork(() -> fetchDataFromService1());
            var task2 = scope.fork(() -> fetchDataFromService2());

            // Esperamos que todas terminen
            scope.join();       
            scope.throwIfFailed(); // Propaga excepciones si alguna falló

            // Obtenemos resultados
            String result1 = task1.resultNow();
            String result2 = task2.resultNow();

            System.out.println("Resultados:");
            System.out.println("Service1 -> " + result1);
            System.out.println("Service2 -> " + result2);

        } catch (InterruptedException | ExecutionException e) {
            System.err.println("Ocurrió un error en las tareas: " + e.getMessage());
        }
    }

    // Simulación de tareas
    private static String fetchDataFromService1() throws InterruptedException {
        Thread.sleep(1000);
        return "Datos del servicio 1";
    }

    private static String fetchDataFromService2() throws InterruptedException {
        Thread.sleep(1500);
        return "Datos del servicio 2";
    }
}
  1. StructuredTaskScope.ShutdownOnFailure: crea un grupo de tareas que se cancelan automáticamente si alguna falla.
  2. fork: inicia tareas concurrentes y devuelve un Subtask.
  3. join(): espera a que todas las tareas terminen.
  4. throwIfFailed(): si alguna tarea lanzó una excepción, la propaga.
  5. resultNow(): obtiene el resultado de la tarea, ya que en este punto todas terminaron.

Ventajas: el código queda limpio, fácil de leer y la cancelación/propagación de errores se maneja de forma automática, evitando fugas de hilos.

Toda la información de Jeps-505 aquí

Primitive Types in Patterns (Tercera Preview) Jeps - 507

Esta feature busca eliminar las restricciones en el uso de tipos primitivos dentro de expresiones de pattern matching, instanceof y switch. Esto nos ayuda con tipos primitivos como int, long, boolean y otros directamente en estas estructuras, aportando mayor coherencia y expresividad al lenguaje. Además, extiende el soporte de switch para todos los tipos primitivos.

Ejemplo:

if (obj instanceof int i) {       // directamente con primitivo
    System.out.println("Es un int: " + i);
}
public static String identifyType(Object value) {
    return switch (value) {
            case int i       -> String.format("int: %d", i);   
            case Double d    -> String.format("Double: %f", d);
            case String s    -> String.format("String: %s", s);
            default          -> "Unknown type";
     };
}

Echa un vistazo a toda la información de Jeps - 507.

JFR CPU-Time Profiling (experimental) Jeps-509

Se añade una función experimental que mejora cómo se mide el uso de CPU de un programa Java. Hasta ahora solo se veía el tiempo que el programa pasaba en “código Java puro”, pero con esta mejora también se cuenta el tiempo gastado en partes externas (código nativo, librerías del sistema, etc.). Esto da una visión más real de dónde se consume la CPU.

Nota: de momento solo funciona en Linux.

Toda la información sobre Jeps-509.

Conclusión

Java sigue modernizándose y aportando valor en las nuevas versiones, optimizando recursos y aportando nuevas funcionalidades. E incluso haciendo más amigables algunas conocidas.

¡¡Larga vida a Java!!

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete