Novedades de Java 19

Escrito por el .
java planeta-codigo
Enlace permanente Comentarios

Las versiones de Java más confiables por su soporte extendido son las LTS, las no LTS dan la oportunidad de probar e ir adaptándose a las novedades que se publicarán de forma definitiva en las LTS. Por ello las empresas seguramente prefieran ir cambiando de versiones de LTS a LTS, los usuarios y desarrolladores a nivel individual o las empresas que alguna novedad supone un cambio importante opten por usar una no LTS. La versión de Java 19 es una no LTS pero incorpora una novedad muy importante aún en vista previa, los virtual threads que permitirán a las aplicaciones pasar de usar miles a millones de threads y usar programación estructurada más sencilla en vez de programación asíncrona o concurrente para resolver problemas de concurrencia.

Java

Una nueva versión de Java que llega ya al número 19, no es una versión LTS con lo que carece de soporte extendido pero que junto con Java 18 incorpora varias novedades desde la última versión LTS alguna muy destacada y esperada que va a suponer un cambio de paradigma en la programación concurrente.

Las novedades no son muy numerosas ya que en el periodo de seis meses da tiempo a ir incorporando algunas que ya se publican aunque sea en modo de vista previa y aunque pueden cambiar cuando lleguen a la versión final permite a aquellos aventurados experimentar y adaptarse a ellas en futuras versiones e incluso utilizarlas si así lo desean en su versión de vista previa.

Introducción

En esta nueva versión de Java las novedades sin contar las incorporadas en vista previa son muy pocas la única es el port a la arquitectura de procesadores RISC-V que aunque es una prometedora arquitectura de procesadores con la interesante propiedad de no tener costes de licencias por implementarla que probablemente vaya ganando cuota de mercado a día de hoy es muy minoritaria, tanto en el escritorio como en los servidores.

Sin embargo, en las características en vista previa hay una muy destacada y relevante que son los threads virtuales que supone un cambio de paradigma en la programación concurrente que proporciona las ventajas de la programación estructurada sin la complejidad de la programación asíncrona o concurrente.

Como complemento a los threads virtuales hay otra ayuda con la concurrencia estructurada, dado que los threads virtuales van a permitir crear varios órdenes de magnitud de threads de lo que era posible hasta ahora, de pasar a únicamente utilizar miles de threads a poder utilizar millones de threads.

Otras novedades en vista previa son actualizaciones a características en vistas previas anteriores como los Record Patterns, Foreign Function & Memory API para integración de código nativo, el Vector API para aprovechar las instrucciones vectoriales de los procesadores, Pattern Matching for switch y Structured Concurrency que está relacionado con los threads virtuales y lo complementa.

Las mejoras incluidas en esta versión son:

Artículos relacionados:

Nuevas características

Linux/RISC-V Port

RISC-V es una es una arquitectura con un conjunto de instrucciones de código abierto, además de un diseño eficiente y moderno no requiere pagar licencias que lo hace interesante para muchas empresas y fabricantes al contrario de otras arquitecturas como ARM que requiere pagar licencias. RISC-V ya es soportado por varias herramientas de desarrollo importantes y hay una disponibilidad de hardware que va en aumento. Por estos motivos, una versión que permite usar Java en procesadores RISC-V es valiosa.

El port de Java para RISC-V incluye el intérprete el compilador JIT para el cliente y servidor y una implementación para todos los recolectores de basura soportados incluyendo ZGC y Shenandoah.

Nuevas características en vista previa

Record Patterns

El patern matching permite eliminar algunos cast explícitos, esto ya ha sido aplicado en expresiones como condiciones. Ahora el pattern matching se aplica a los records que permite una forma de desestructurar este tipo de clases para obtener los elementos de record en variables disponibles en el ámbito de uso de la expresión.

1
2
3
4
5
6
7
8
9
record Point(int x, int y) {}

static void printSum(Object o) {
    if (o instanceof Point p) {
        int x = p.x();
        int y = p.y();
        System.out.println(x + y);
    }
}
pattern-matching.java

Al escribir la expresión de pattern matching para un record permite al mismo tiempo desestructurar los elementos y extraerlos a variables. La expresión resultante para desestructurar los elementos es muy verbosa pero hace más simple el acceso posterior a las variables de record.

1
2
3
4
5
void printSum(Object o) {
    if (o instanceof Point(int x, int y)) {
        System.out.println(x + y);
    }
}
record-patterns.java

Foreign Function & Memory API

Esta característica facilita el uso del código nativo de forma eficiente y más sencilla que anteriormente con JNI, en esta nueva vista previa es un refinamiento de las versione anteriores incorporando los comentarios y sugerencias desde la última vista previa.

Para muchos desarrolladores esta característica es irrelevante o solo se benefician de forma indirecta pero para otros que trabajan con software embebido o que desarrollan librerías Java que hacen uso de código nativo implementado en otros lenguajes supone una gran mejora.

Virtual Threads

Un sistema operativo como Linux solo es capaz de ofrecer a las aplicaciones una cantidad limitada de threads ya que su consumo de recursos es elevado en el sistema y el cambio de contexto de unos a otros penaliza el rendimiento.

Para solventar esta limitación Java implementa dentro del JVM los threads virtuales que son más ligeros en recursos que los threads del sistema operativo y el cambio de contexto de uno a otro no supone tanta penalización en el rendimiento. Los threads virtuales permiten a las aplicaciones poder usar un mayor cantidad de hilos pasando de unos miles a varios millones sin ningún problema. Para las aplicaciones que usan gran cantidad de hilos como las de servidor de un hilo por petición permite escalar a un número significativamente mayor de peticiones y un mejor rendimiento.

Adicionalmente, una de las mejoras cosas es que el uso de los threads virtuales no requiere una nueva API ya que la implementación la realiza usando la misma clase Thread que ha existido siempre en Java desde las versión 1.0 de Java.

La solución habitual a la escasez de threads y la limitación del sistema operativo para aprovechar el hardware ha sido la programación asíncrona, está sin embargo es más difícil de desarrollar, entender y depurar. Otra opción de algunos lenguajes han sido las coroutines como en Kotlin y otros lenguajes.

Los threads virtuales y la concurrencia estructurada promete la simplicidad de la programación estructurada con la ventaja de proporcionar un rendimiento similar que la programación asíncrona.

Structured Concurrency

Los threads virtuales permiten dedicar un thread a cada tarea con lo que se prevé que las aplicaciones aprovechen poder utilizar una mayor número de threads, pero gestionarlos sigue siendo un problema ya que los hilos siguen siendo independientes y su depuración complicada.

Si los threads virtuales permiten utilizar threads en abundancia la concurrencia estructurada asegura que los threads estén relacionados correctamente y de forma robusta además de mostrar los threads en herramientas de depuración de forma que sean entendidos por los desarrolladores.

La clase StructuredTaskScope permite estructurar una tarea ejecutada en varios hilos, tanto en los casos en que una tarea se divide en varias operaciones como en los casos de que por cada petición entrante se emplea un hilo como en un servidor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Response handle() throws ExecutionException, InterruptedException {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {

        Future<String> user = scope.fork(() -> findUser());
        Future<Integer> order = scope.fork(() -> fetchOrder());

        scope.join(); // Join both forks
        scope.throwIfFailed(); // ... and propagate errors

        // Here, both forks have succeeded, so compose their results

        return new Response(user.resultNow(), order.resultNow());
    }
}
structured-concurrency-1.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void serve(ServerSocket serverSocket) throws IOException, InterruptedException {
    try (var scope = new StructuredTaskScope<Void>()) {
        try {
            while (true) {
                var socket = serverSocket.accept();
                scope.fork(() -> handle(socket));

            }
        } finally {
            // If there's been an error or we're interrupted, we stop accepting
            scope.shutdown();  // Close all active connections
            scope.join();
        }
    }
}
structured-concurrency-2.java

Pattern Matching for switch

El pattern matching en las expresiones switch han sido mejorados en esta nueva vista previa.

  • Las guard patterns son reemplazados con cláusulas when en los bloques switch.
  • La semántica del switch en los casos en que es null la expresión se ajusta a la semántica existente que ha existido en los switch lanzando un NullPointerException.
1
2
3
4
5
6
7
8
static void testTriangle(Shape s) {
    switch (s) {
        case null -> { break; }
        case Triangle t
        when t.calculateArea() > 100 -> System.out.println("Large triangle");
        default -> System.out.println("A shape, possibly a small triangle");
    }
}
pattern-matching-switch.java

Por otro lado todo, ahora en los casos de un switch es posible usar el label null para seleccionar la rama cuando la expresión del switch toma un valor nulo, el comportamiento anterior era que si la expresión switch era null producía un NullPointerException lo que requería utilizar un guard clause antes del switch para evitar el NPE.

Desde Java 16 los bloques switch permiten dos estilos de grupos uno mediante etiquetas con : y otro de una única consecuencia con -> donde la continuación en cascada no es posible. En el primer caso los múltiples labels se escriben case l1: case l2: cuando en el estilo del segundo se escriben case l1, l2 ->.

Soportar la etiqueta null permite ahora escribir un switch de la siguiente forma.

1
2
3
4
5
6
7
8
Object o = ...;

switch (o) {
    case null:
    case String s: {
        System.out.println("String, including null");
    } break;
}
switch-null-label.java

Otras novedades

Interpolación de cadenas (String Templates)

En un futuro es posible que Java incorpore al lenguaje la interpolación de cadenas con la JEP 430: String Templates. Facilitará la construcción de cadenas con una mezcla de cadenas y valores de variables que produzcan la cadena resultante deseada. La construcción de cadenas es algo muy utilizado en todos los programas con lo que la interpolación de cadenas será una mejora en la la construcción de cadenas que hace el código más legible.

Un problema de la interpolación de cadenas es que en ciertas cadenas hay posibilidad de crear un problema de seguridad y por ello es peligrosa, una forma de este problema es el SQL injection. La interpolación de cadenas que se incorporará en Java solucionará al mismo tiempo que la legibilidad del código los problemas que pueden ocurrir con el SQL injection creando una clase TemplateString para las cadenas con interpolación y unos policies específicos para cada lenguaje como SQL o JSON según el formato resultante que garantizará una cadena resultante bien formada y evitará el problema de injection.

Estos son algunos ejemplos que se están evaluando para implementar la interpolación de cadenas. El STR sería el policy que determina como convertir el TemplateString en un String. Además será posible implementar nuevos policies haciendo una implementación de la interfaz funcional TemplateProcessor.

1
2
3
4
5
String name = "Joan";
TemplatedString ts = "My name is \{name}";

String name = "Joan";
String info = STR."My name is \{name}";
string-interpolation.java

Personalmente prefiero la sintaxis ${…} en vez de \{…} ya que la primera es la opción utilizada en otros lenguajes y no me gusta mucho ese syntactic sugar (o rat poison) para usar los TemplateProcessor que me parece nada coherente con el lenguaje. Utilizar el $ en vez de \ es una alternativa que han evaluado pero que de momento han descartado para evitar conflictos con código heredado. También han evaluado otro tipo de alternativas, en cualquier caso hasta la versión final y que esta sea incorporada en el lenguaje si lo es es muy posible que haya cambios.


Comparte el artículo: