Java 21 vs Java 8: Por qué es otro lenguaje y qué cambió

DUGLAS MORENODUGLAS MORENO 👁 4

Introducción

Si sos desarrollador Java y todavía tenés en la cabeza los conceptos de Java 8, vas a notar que Java 21 es prácticamente otro lenguaje. A partir de 2021, la versión 21 introdujo una cantidad enorme de mejoras que modifican la forma en que escribimos código, diseñamos APIs y aprovechamos la JVM. La mayoría de los cursos todavía enseñan Java 8 porque es estable y tiene un ecosistema maduro, pero ignorar las novedades de Java 21 significa quedarse atrás en productividad y en capacidad de usar características modernas.

En este artículo vamos a desglosar, con ejemplos comparativos, por qué Java 21 es tan distinto y qué aprendizaje concreto podés obtener. La estructura está dividida en secciones que cubren los cambios sintácticos, las nuevas funcionalidades de la JVM, y las buenas prácticas que deberías aplicar en tus proyectos.

A quién está dirigido

Este texto está pensado para desarrolladores que ya manejan Java 8 y quieren actualizar su conocimiento, o para estudiantes que recién empiezan y quieren evitar aprender conceptos obsoletos. Si tenés un proyecto legado en Java 8 o estás empezando un nuevo servicio, podés esperar a que este artículo te muestre los beneficios de actualizar a Java 21.

Qué vas a aprender

  • Los conceptos clave que introdujeron los sellados, los registros y el pattern matching en los switches.
  • Cómo usar virtual threads para escribir código concurrente sin la complejidad de los hilos tradicionales.
  • Qué son las string templates y cómo simplifican la construcción de strings.
  • Mejoras en el API HTTP client y en la interacción con servicios externos.
  • Buenas prácticas y errores comunes que suelen aparecer al migrar de Java 8 a Java 21.

Con estos puntos, al terminar el artículo podrás decidir si actualizar tu código y, lo que es más importante, cómo hacerlo de forma segura y aprovechar al máximo lo que Java 21 tiene para ofrecer.

Cambios estructurales: sellados, registros y pattern matching en los switches

Sellos y registros: de las clases anónimas a los records

En Java 8, para representar una entidad simple utilizábamos clases anónimas o clases estáticas con constructores y métodos. Por ejemplo, un círculo y un cuadrado se definían así:

interface Shape {}

class Circle implements Shape {
    double radius;
    Circle(double r) { this.radius = r; }
    double area() { return Math.PI * radius * radius; }
}

class Square implements Shape {
    double side;
    Square(double s) { this.side = s; }
    double area() { return side * side; }
}

Con Java 21, los records permiten declarar una clase immutable y generar automáticamente constructor, getters, equals, hashCode y toString. Además, los sealed interfaces permiten limitar qué clases pueden implementar una interfaz, lo que mejora la exhaustividad del switch.

public sealed interface Shape permits Circle, Square {}

record Circle(double radius) implements Shape {}
record Square(double side) implements Shape {}

El código anterior es mucho más conciso y, al ser sealed, el compilador sabe que solo Circle y Square pueden implementar Shape. Esto permite que el switch sea exhaustivo sin necesidad de un default.

Pattern matching para switch y instanceof

Antes de Java 21, para distinguir el tipo de una variable en un switch tenías que usar instanceof y luego hacer cast manualmente:

if (shape instanceof Circle) {
    Circle c = (Circle) shape;
    System.out.println("Área: " + c.area());
}

En Java 21, el pattern matching para switch (preview) permite combinar la comprobación y el cast en una sola línea:

switch (shape) {
    case Circle c -> System.out.println("Área: " + c.area());
    case Square s -> System.out.println("Área: " + s.area());
    default -> System.out.println("Forma desconocida");
}

De forma similar, instanceof ahora acepta un patrón:

if (shape instanceof Circle c) {
    System.out.println("Radio: " + c.radius());
}

Esto elimina la verbosidad y reduce la probabilidad de errores de casting.

Ejemplo completo con salida esperada

A continuación, un programa completo que usa sellado, registros y pattern matching para calcular y mostrar el área de distintas formas. El programa está pensado para ejecutarse en Java 21.

public class AreaCalculator {
    public static void main(String[] args) {
        Shape s1 = new Circle(2.0);
        Shape s2 = new Square(3.0);
        Shape s3 = null; // para demostrar el default

        System.out.println(AreaCalculator.describe(s1));
        System.out.println(AreaCalculator.describe(s2));
        System.out.println(AreaCalculator.describe(s3));
    }

    public static String describe(Shape s) {
        return switch (s) {
            case Circle c -> {"Círculo con radio " + c.radius() + \(
\)• Área: " + c.area();}
            case Square s -> {"Cuadrado con lado " + s.side() + \(
\)• Área: " + s.area();}
            case null -> "Forma nula\); 
            default -> {"Forma desconocida"};
        };
    }
}

Salida esperada (asumiendo que la JVM imprime los como saltos de línea):

Círculo con radio 2.0
• Área: 12.566370614359172
Cuadrado con lado 3.0
• Área: 9.0
Forma nula
default: Forma desconocida

Este ejemplo muestra cómo, con Java 21, el código es más legible, menos propenso a errores y, lo más importante, el compilador puede garantizar que el switch sea exhaustivo.

Virtual threads: concurrencia ligera sin el dolor de los hilos tradicionales

Qué son los virtual threads

Java 21 incluye los virtual threads (preview) como parte del proyecto Loom. Son hilos gestionados por la JVM que permiten crear miles de hilos con un coste de memoria mucho menor que los hilos nativos. En lugar de usar new Thread(...).start(), se usa Thread.startVirtualThread(Runnable).

Esto cambia radicalmente la forma de escribir código concurrente, porque ya no tenés que preocuparte por el límite de hilos del sistema operativo ni por la sobrecarga de crear y destruir hilos.

Ejemplo comparativo

A continuación, un programa que crea 10.000 hilos tradicionales y 10.000 virtual threads. El objetivo es demostrar la diferencia de consumo de recursos y la simplicidad del código.

Hilos tradicionales (Java 8)

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TraditionalThreads {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(200);
        for (int i = 0; i < 10_000; i++) {
            final int id = i;
            pool.submit(() -> {
                // Simulamos trabajo
                try { Thread.sleep(10); } catch (InterruptedException ignored) {}
                System.out.println("Tradicional " + id);
            });
        }
        pool.shutdown();
        pool.awaitTermination(1, java.util.concurrent.TimeUnit.MINUTES);
    }
}

Virtual threads (Java 21)

public class VirtualThreadsDemo {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10_000; i++) {
            final int id = i;
            Thread.startVirtualThread(() -> {
                try { Thread.sleep(10); } catch (InterruptedException ignored) {}
                System.out.println("Virtual " + id);
            });
        }
        // Esperamos a que terminen los hilos virtuales
        Thread.sleep(200);
        System.out.println("Fin del programa");
    }
}

Salida esperada (el orden puede variar, pero el mensaje final siempre aparece):

Virtual 0
Virtual 1
... (muchas líneas)
Virtual 9999
Fin del programa

En el caso de los hilos tradicionales, el programa puede colgarse o agotar los recursos del sistema si se intenta crear más de unos pocos miles de hilos. Con los virtual threads, el mismo número de hilos se ejecuta sin problemas, porque la JVM los multiplexa sobre un pequeño número de hilos nativos.

Ventajas concretas

  • Escalabilidad: podés crear cientos de miles de hilos sin que la JVM se quede sin memoria.
  • Simplicidad: el código es más parecido a la programación secuencial; no hay que gestionar pools de hilos.
  • Latencia reducida: los virtual threads pueden cambiar de contexto rápidamente, lo que mejora la respuesta de aplicaciones I/O‑bound.

Buenas prácticas al usar virtual threads

  1. No mezcles virtual threads con código síncrono que bloquee (por ejemplo, Thread.sleep o llamadas a I/O bloqueante) sin usar los blocking APIs de Loom.
  2. Usa Thread.join() y Thread.interrupt() como en los hilos tradicionales; la JVM los adapta automáticamente.
  3. Limita la cantidad de blocking operations simultáneas para evitar saturar el pool de hilos nativos que la JVM utiliza internamente.
  4. Prueba tu aplicación bajo carga real, ya que el comportamiento puede variar según la versión de la JVM y la configuración del sistema.

String templates: construcción de strings sin String.format

El problema con String.format

En Java 8, la forma habitual de construir strings era usar String.format, que requiere pasar parámetros en el orden correcto y es propenso a errores de formato.

String msg = String.format("Hola %s, tenés %d años", name, age);

Además, String.format es costoso en tiempo de ejecución porque necesita parsear el patrón cada vez.

String templates en Java 21

Java 21 introduce string templates (preview, JEP 374). Permiten escribir strings con placeholders y la JVM los sustituye en tiempo de compilación o en tiempo de ejecución de forma más eficiente.

String tmpl = "¡Hola $nombre, tu saldo es $saldo!\); 
String result = tmpl.text(); // "¡Hola Juan, tu saldo es 150.0!"

Los placeholders se definen con $ seguido del nombre de la variable. La API text() devuelve el string interpolado.

Comparación de salida

Con String.format

String name = "María\(<|message|>\| 30\);
String formatted = String.format("Bienvenido $name, tus puntos son $pts", "María", 30);
System.out.println(formatted);

Salida: Bienvenido María, tus puntos son 30

Con string templates

String name = "María\(<|message|>\| 30\);
String tmpl = "Bienvenido $name, tus puntos son $pts\);
String result = tmpl.text();
System.out.println(result);

Salida: Bienvenido María, tus puntos son 30

En ambos casos la salida es la misma, pero la sintaxis de string templates es más legible y evita errores de orden de parámetros.

Mejoras en el API HTTP client

HTTP/2 y HTTP/3 soportados de forma nativa

A partir de Java 21, el cliente HTTP (java.net.http.HttpClient) soporta HTTP/2 y, mediante la dependencia java.net.http.HttpClient.Version.HTTP_3, también HTTP/3. Esto permite conexiones más eficientes, compresión de encabezados y multiplexado sin necesidad de bibliotecas externas.

HttpClient client = HttpClient.newBuilder()
    .version(HttpClient.Version.HTTP_2)
    .build();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/resource\)))
    .GET()
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());

Uso de string templates para construir URLs

Con string templates puedes crear URLs de forma más clara:

String base = "https://api.ejemplo.com\);
String endpoint = ’/users/$id\);
String url = base + endpoint; // "https://api.ejemplo.com/users/42"

Esto reduce la probabilidad de errores de concatenación y mejora la legibilidad.

Buenas prácticas y errores comunes al migrar a Java 21

1. Actualizar el JDK y las dependencias

  • Actualiza tu JDK a una versión 21 o superior. Las versiones anteriores no reconocerán las nuevas características como records o virtual threads.
  • Revisa tus dependencias de terceros (Maven, Gradle). Algunas bibliotecas pueden no ser compatibles con Java 21 y requerir versiones actualizadas.

2. Habilitar preview features si es necesario

Algunas funcionalidades (p.ej., sealed interfaces, pattern matching para switch, string templates) están marcadas como preview. Si tu compilador no las reconoce por defecto, necesitas habilitar la opción --enable-preview tanto en la compilación como en la ejecución.

javac --enable-preview -d out src/main/java/Main.java
java --enable-preview -cp out Main

3. Evitar el uso de finalize y migrar a records o sealed classes

El método finalize está deprecado desde Java 9. En Java 21, la tendencia es usar records o sealed classes para definir tipos inmutables y evitar la necesidad de finalize.

4. Concurrency: no bloquear virtual threads con I/O síncrono

Los virtual threads son eficientes, pero si llamas a métodos bloqueantes (p.ej., InputStream.read()) sin configurar el blocking mode, podés crear cuellos de botella. Usa Thread.startVirtualThread con java.nio.channels.FileChannel en modo no bloqueante o configura el blocking flag.

5. Pruebas de exhaustividad del switch

Al usar pattern matching en switch, el compilador te avisa si no cubriste todos los casos de una sealed interface. Aprovechá esa ventaja y mantén el default solo cuando sea realmente necesario.

6. Manejo de strings: prefiere string templates sobre concatenación

La concatenación de strings con + en bucles es costosa. Las string templates son más eficientes y hacen que el código sea más claro.

Conclusión

Java 21 no es solo una versión menor; es una reinvención del lenguaje que incorpora características modernas, reduce la verbosidad y abre la puerta a modelos de concurrencia más ligeros. Comparado con Java 8, los records, el pattern matching para switch y instanceof, los virtual threads y las string templates cambian radicalmente la forma en que escribimos código.

  • Records y sealed interfaces permiten modelar datos y APIs de forma más clara y segura.
  • Pattern matching elimina casts explícitos y mejora la exhaustividad del switch.
  • Virtual threads hacen que la concurrencia sea tan simple como crear una función lambda, sin preocuparse por límites de hilos.
  • String templates simplifican la construcción de strings y evitan errores de formato.
  • El API HTTP client ahora soporta HTTP/2 y HTTP/3, lo que simplifica la integración con servicios externos.

Al migrar un proyecto de Java 8 a Java 21, el primer paso es actualizar el JDK, revisar dependencias y habilitar preview features si es necesario. Luego, reemplaza clases estáticas por records, usa pattern matching en switch y instanceof, y considera adoptar virtual threads para mejorar la escalabilidad de tus aplicaciones concurrentes.

Con estas prácticas, no solo estarás alineado con las últimas tendencias del ecosistema Java, sino que también ganarás en productividad, mantenibilidad y rendimiento. Si todavía tenés dudas sobre alguna característica, experimentá con un proyecto de prueba y aprovechá la documentación oficial de Java 21, que incluye ejemplos y guías paso a paso.

¡Podés empezar hoy mismo a modernizar tu código y descubrir por qué Java 21 es, sin duda, otro lenguaje!

Comentarios (0)

Sé el primero en comentar.

Dejá tu comentario