¡Compártelo!

Jdk 21: mejoras en la última versión LTS de Java

Cada 6 meses Java lanza una nueva versión de nuestro lenguaje favorito. Da igual si la estábamos esperando con ganas o si nos pilla por sorpresa, es algo que celebrar dentro de la comunidad. Esta vez la versión 21 incluye diferentes características estables, otras en plan preview, y algunas que todavía están en modo beta. Y, sin darle muchas vueltas, comenzamos a repasar las más importantes dentro de este artículo. ¡Bienvenidas y bienvenidos a la nueva LTS JDK 21!

Sequenced Collections en JDK 21

En el mundo de Java, Collections no disponía de un tipo de colección diseñado para representar una secuencia de elementos con un orden de encuentro definido y operaciones uniformes entre ellos. 

Se trata de un conjunto de interfaces y clases que permiten representar colecciones con un orden de encuentro definido. Esto significa que los elementos de la colección se pueden acceder y recorrer en un orden específico, lo que puede ser útil en una variedad de escenarios.

Se han introducido tres nuevas interfaces para manejar colecciones secuenciadas (SequencedCollection, SequencedSet y SequencedMap), destinadas a representar colecciones con un orden específico. Estas interfaces ofrecen una API uniforme que facilita el acceso a los elementos de una colección en secuencia, ya sea en dirección directa o inversa.

Revisemos cómo ha evolucionado el código en comparación con la forma en que solíamos escribirlo antes de poder disfrutar de esta significativa mejora:

@Test

void sequenceCollection() {

 List<String> sequencedList = new ArrayList<>(List.of("Element1", "Element2", "Element3"));

 sequencedList.addLast("Element4");

 // get first and last element jdk 17

 assertThat( sequencedList.get(sequencedList.size() - 1)).isEqualTo("Element4");

 assertThat( sequencedList.get(0)).isEqualTo("Element1");

 // get first and last element jdk 21

 assertThat(sequencedList.getLast()).isEqualTo("Element4");

 assertThat(sequencedList.getFirst()).isEqualTo("Element1");

 LinkedHashSet<String> sequenceSet = new LinkedHashSet<>();

 sequenceSet.add("Element1");

 sequenceSet.add("Element2");

 sequenceSet.add("Element3");

 // get first  element set  jdk 17

 assertThat( sequenceSet.iterator().next()).isEqualTo("Element1");

 // get first  element set  jdk 21

 assertThat( sequenceSet.getFirst()).isEqualTo("Element1");

 // remove first  element set  jdk 21

 sequenceSet.removeFirst();

 assertThat( sequenceSet.getFirst()).isEqualTo("Element2");

 // reverse  set  jdk 21

 SequencedSet<String> reversed = sequenceSet.reversed();

 assertThat( reversed.getFirst()).isEqualTo("Element3");

}

Pattern Matching for switch expression

En Java 17 se introdujo la característica de «switch expression», transformando el switch de un simple «statement» a una expresión que puede devolver valores. Esta funcionalidad se ha perfeccionado en versiones más recientes, y en Java 21 se presenta una característica más refinada que incluye diversas mejoras que estaban en modo de vista previa desde Java 17. En tiempo de compilación se comprobará que dentro de nuestro código si usamos Enum o Sealed Classes evaluamos todos los posibles valores:

@Test

void switchTest() {

 enum Vehicle {

   CAR, BUS, TRAIN;

   int process() {

     return switch (this) {                         

       case CAR -> 0;

       case TRAIN -> 90;

     };

   }

 }

 assertThat(    Vehicle.BUS.process()).isEqualTo(180);

}
JDK 21

Virtual Threads

Los Hilos Virtuales fueron introducidos en Java a través del Proyecto Loom como una alternativa para el procesamiento paralelo. Son hilos livianos, gestionados por la Java Virtual Machine (JVM) en modo usuario. Como resultado, son especialmente adecuados para operaciones de entrada/salida (I/O), donde los hilos tradicionales pueden pasar un tiempo significativo esperando recursos externos.

A diferencia de las soluciones asíncronas o reactivas, los hilos virtuales nos permiten seguir utilizando el modelo de procesamiento hilo-por-solicitud. En otras palabras, podemos seguir escribiendo código de manera secuencial, sin mezclar la lógica de negocios y la API reactiva. Si has usado Spring Reactor te sonará el problema del “flaptmap hell” y lo difícil que se hace entender el código al mezclar lógica de negocio con operadores que permite la programación reactiva. 

Para entenderlo mostraremos un ejemplo reactivo donde se consulta una serie de productos y se les aplican unos descuentos para meterlos en el carro. Al final del se manda a una cola de Kafka un mensaje para notificar que el producto ha sido añadido correctamente al carro:

void addProductToCart(String productId, String cartId) {

 repository.findById(productId)

     .switchIfEmpty(Mono.error(() -> new IllegalArgumentException("not found!")))

     .flatMap(this::computePrice)

     .map(price -> new ProductAddedToCartEvent(productId, price.value(), price.currency(), cartId))

     .subscribe(event -> kafkaTemplate.send(PRODUCT_ADDED_TO_CART_TOPIC, cartId, event));

}

Mono<Price> computePrice(Product product) {

 if (product.category().isEligibleForDiscount()) {

   return discountService.discountForProduct(product.id())

       .map(product.basePrice()::applyDiscount);

 }

 return Mono.just(product.basePrice());

}

void addProductToCart(String productId, String cartId) {

 Thread.startVirtualThread(() -> computePriceAndPublishMessage(productId, cartId));

}

void computePriceAndPublishMessage(String productId, String cartId) {

 Product product = repository.findById(productId)

     .orElseThrow(() -> new IllegalArgumentException("not found!"));

 Price price = computePrice(productId, product);

 var event = new ProductAddedToCartEvent(productId, price.value(), price.currency(), cartId);

 kafkaTemplate.send(PRODUCT_ADDED_TO_CART_TOPIC, cartId, event);

}

Price computePrice(String productId, Product product) {

 if (product.category().isEligibleForDiscount()) {

   BigDecimal discount = discountService.discountForProduct(productId);

   return product.basePrice().applyDiscount(discount);

 }

 return product.basePrice();

}

Records Pattern  en JDK 21

Los patrones de registros en Java 21 es una nueva característica que permite desestructurar instancias de clases de un Record. 

@Test

void destructRecordTest() {

  record Person(String name, Integer age) {}

 Person person = new Person("Juan", 30);

  // jdk 17 instance of

 if (person instanceof Person p) {

   String name = p.name();

   int age = p.age();

   assertThat(name+age).isEqualTo("Juan30");

 }

 // jdk 21 instance of

 if (person instanceof Person(String name, Integer age)) {

   assertThat(name+age).isEqualTo("Juan30");

 }

 // jdk 21 instance of applies to switch patterns too

 switch (person) {

   case Person(String name, Integer age) -> {

     assertThat(name+age).isEqualTo("Juan30");

   }

 }

}

Funcionalidades en modo preview

String Template 

Muchos lenguajes de programación ya ofrecen interpolación de strings como alternativa a la concatenación de strings y esta nueva versión podremos ver una versión inicial de cómo poder usarlo.

// jdk 21 preview mode --> it must be preview mode ON in Jdk

@Test

void stringTemplateTest() {

 String name = "Cristian";

 // JDK 21 String template

 String cad = STR."My name is \{name}";

 assertThat(cad).isEqualTo("My name is Cristian");

 String cad2 = STR."My name is \{name.toUpperCase()}";

 assertThat(cad2).isEqualTo("My name is CRISTIAN");

}

Unnamed pattern y variables:

Con las nuevas funciones de «pattern matching» en Java, incluyendo expresiones lambda, surge la falta de variables sin nombre. Cuando se utiliza una expresión lambda y algunas de sus variables de entrada no se utilizan, el desarrollador aún está obligado a definirlas.

Ahora en jdk 21 podemos usar un “_”.

Se puede utilizar las variables sin nombre en los siguientes lugares de nuestro código:

  • Declaración de variable local dentro de un bloque.
  • Especificación de recurso en una declaración ‘try-with-resources’.
  • Encabezado de una declaración ‘for’ básica.
  • Encabezado de un bucle ‘enhanced for’.
  • Parámetro de excepción en un bloque ‘catch’.
  • Parámetro para una expresión lambda.
Person person = new Person("Juan", 30);

// local variable declaration statement in a block.

int _ = person.remove();

// Resource specification of a 'try-with-resources' statement.

try {

 int _ = person.remove();

} catch (NullPointerException _) {

 System.out.println("Null pointer: ");

}

List<String> sequencedList = new ArrayList<>(List.of("Element1", "Element2", "Element3"));

// A formal parameter of a lambda expression.

sequencedList.forEach(_-> System.out.println("sequencedList = " + sequencedList));

// jdk 17 instance of

if (person instanceof Person _) {

 System.out.println(STR."person = \{person}");

}

if (person instanceof Person(String name, _)) {

 System.out.println("Department Name is : " + name); 

}

Clases anónimas y main sin clase: 

Buscando lograr el mismo propósito que otros lenguajes donde la sintaxis inicial para ponerse en marcha es mínima, sin la necesidad de crear una clase o incluso un método main en JDK 21 se ha añadido la característica nos libera al menos de la primera parte: ya no necesitamos crear una clase, aunque la segunda parte sigue siendo necesaria:

Una clase sin nombre reside en el paquete sin nombre, y el paquete sin nombre reside en el módulo sin nombre. Mientras que puede haber solo un paquete sin nombre (salvo múltiples cargadores de clases) y solo un módulo sin nombre, puede haber múltiples clases sin nombre en el módulo sin nombre. Cada clase sin nombre contiene un método main y representa un programa. Varias clases de este tipo en el paquete sin nombre representan varios programas.

Una clase sin nombre es casi exactamente como una clase declarada explícitamente. Sus miembros pueden tener los mismos modificadores (como private y static) y los modificadores tienen los mismos valores por defecto (como acceso de paquete y membresía de instancia). La clase puede tener inicializadores estáticos y también inicializadores de instancia. Una diferencia clave es que, si bien una clase sin nombre tiene un constructor predeterminado sin parámetros, no puede tener ningún otro constructor.

Ahora, para iniciar un programa, solo necesitamos escribir:

void main() {

 System.out.println("Hello, World!");

}

En IntelliJ nos va a permitir ejecutar este código sin necesidad de indicar ni siquiera un paquete base:

JDK 21
String greeting() { return "Hello, World!"; }

void main() {

 System.out.println(greeting());

}

En el fascinante viaje del desarrollo de software, cada nueva versión de JDK no solo representa avances tecnológicos, sino también oportunidades de mejorar nuestra manera de generar código. Con la llegada de JDK 21 hemos sido testigos de la incorporación de características innovadoras que no sólo simplifican nuestras tareas diarias, sino que también expanden el horizonte de posibilidades dentro de nuestra querida java virtual machine. 

Recuerda que cada línea de código es una oportunidad para explorar, aprender y crear nuevas aplicaciones. ¡Hasta la próxima versión, donde seguramente nos esperan nuevas sorpresas y desafíos! Happy coding.

¿Quieres ver la formación detallada sobre JDK 21? ¡No te pierdas este vídeo!

Repositorio github código de JDK 21:

https://github.com/cristianprofile/jdk21_test_features

Artículos ​ relacionados