Translate

Mostrando las entradas para la consulta pattern matching ordenadas por fecha. Ordenar por relevancia Mostrar todas las entradas
Mostrando las entradas para la consulta pattern matching ordenadas por fecha. Ordenar por relevancia Mostrar todas las entradas

sábado, 11 de julio de 2026

Polimorfismo vs Pattern Matching


Durante los últimos años aparecieron innumerables artículos anunciando la muerte del polimorfismo. La llegada de sealed classes, union types y pattern matching hizo que muchos comenzaran a tratar la orientación a objetos como una técnica del pasado.

Sin embargo, las críticas al polimorfismo no son nuevas. Tampoco las críticas al pattern matching. De hecho, muchas de ellas fueron formuladas por las personas que diseñaron estos paradigmas mucho antes de que existieran Java, Kotlin o Rust.

Vale la pena volver a esas discusiones:


Agregar operaciones es difícil

Philip Wadler formuló en 1998 el famoso Expression Problem.

Su observación sigue siendo una de las críticas más fuertes al polimorfismo: los sistemas orientados a objetos hacen muy sencillo incorporar nuevos tipos, pero extremadamente costoso agregar nuevas operaciones.

Cada nueva operación obliga a modificar toda la jerarquía de clases.

Desde esta perspectiva, el pattern matching invierte completamente el problema: agregar una operación suele consistir simplemente en escribir una nueva función.

No es casualidad que muchos desarrolladores funcionales vean al patrón Visitor como una consecuencia directa de esta limitación.


El comportamiento queda disperso

Martin Odersky ha defendido repetidamente el pattern matching como una herramienta para expresar algoritmos completos en un único lugar.

Cuando la lógica está distribuida entre decenas de implementaciones distintas, comprender una operación completa implica recorrer buena parte del proyecto.

Con pattern matching, el algoritmo aparece concentrado y resulta mucho más sencillo razonar sobre él.

La pregunta implícita es incómoda para la orientación a objetos: ¿Queremos organizar el código alrededor de los datos o alrededor de las operaciones?


El despacho dinámico oculta demasiado

Otra crítica frecuente desde el mundo funcional es que el polimorfismo sacrifica legibilidad en favor de la extensibilidad.

Una llamada aparentemente inocente puede terminar ejecutando cualquiera de decenas de implementaciones.

El flujo del programa deja de ser evidente y pasa a depender de la jerarquía de tipos.

Para muchos, el pattern matching hace exactamente lo contrario: vuelve explícitas todas las alternativas posibles.


Pattern Matching rompe el encapsulamiento

Si hubiera que elegir una crítica histórica, probablemente sea la de Alan Kay.

Kay insistió durante décadas en que los objetos nunca fueron concebidos como estructuras de datos con comportamiento, sino como entidades que colaboran mediante el envío de mensajes.

Desde esa mirada, inspeccionar explícitamente el estado de un objeto para decidir qué hacer significa abandonar el modelo de objetos y volver a programar sobre representaciones internas.

No es una cuestión sintáctica.

Es un cambio de filosofía.


Cada nuevo tipo obliga a revisar todos los matches

Bertrand Meyer, creador de Eiffel y autor del principio Open/Closed, probablemente encontraría aquí su principal objeción.

Cuando aparece un nuevo subtipo, todos los matches relevantes deben ser revisados.

La lógica que antes estaba distribuida vuelve a concentrarse en múltiples lugares.

El costo del cambio deja de estar en agregar operaciones y pasa a estar en agregar variantes del modelo.

Exactamente el problema inverso al planteado por Wadler.


Un match de 500 líneas sigue siendo un switch de 500 líneas

Quizás sea la crítica más práctica.

El pattern matching no garantiza buen diseño.

Nada impide escribir enormes bloques de decisiones difíciles de mantener.

Cambiar la sintaxis de switch por match no elimina automáticamente el acoplamiento ni mejora la modularidad.

Muchas veces simplemente produce un switch más elegante.


Entonces... ¿quién tiene razón?

Probablemente ambos.

Philip Wadler demostró que la orientación a objetos favorece la extensión por tipos.

Bertrand Meyer mostró que esa misma decisión permite mantener el sistema cerrado frente a modificaciones cuando aparecen nuevos comportamientos internos.

Alan Kay defendió durante toda su carrera que los objetos deberían ocultar completamente su representación.

Martin Odersky respondió que muchas veces entender un algoritmo completo es más importante que esconder la representación de los datos.


No son respuestas contradictorias.

Responden preguntas diferentes.


Cada cierto tiempo la industria intenta declarar un ganador.

Hace veinte años parecía que todo debía resolverse con herencia y polimorfismo.

Hoy pareciera que todo debería resolverse con pattern matching.

La realidad es bastante menos interesante para quienes buscan una respuesta definitiva: ninguno reemplaza al otro.


Uno optimiza la evolución del sistema cuando aparecen nuevos tipos.

El otro optimiza la evolución cuando aparecen nuevas operaciones.


No existe una técnica superior.


Existe una pregunta mucho más importante:

¿Qué es lo que cambia con mayor frecuencia en tu dominio?


Porque esa respuesta, mucho más que el paradigma elegido, suele determinar cuál de las dos herramientas encaja mejor.

viernes, 3 de julio de 2026

Diseñando MateScript: un JavaScript de tipado estático para la JVM


Hace tiempo que me interesa el diseño de lenguajes de programación.

No solamente cómo construir compiladores, sino también cómo tomar decisiones sobre la sintaxis, el sistema de tipos y la experiencia de desarrollo.

Por eso decidí comenzar un proyecto experimental: MateScript.


¿Qué es MateScript?

MateScript es un lenguaje inspirado en JavaScript, pero con tipado estático y compilación a bytecode JVM.

JavaScript es uno de los lenguajes más populares del mundo, pero también arrastra varios problemas derivados de sus decisiones históricas.


Por ejemplo:

let age = 42;

age = "hola";


Esto es perfectamente válido.

MateScript buscará detectar estos problemas durante la compilación.


let age = 42;

age = "hola";


Resultado:

Type mismatch: expected Int found String


MateScript intentará mantener las características más atractivas de JavaScript:

  • Sintaxis sencilla
  • Funciones de primera clase
  • Lambdas
  • Inferencia de tipos
  • Programación funcional
  • Objetos


Pero agregando:

  • Tipado estático
  • Null Safety
  • Pattern Matching
  • Compilación a bytecode JVM
  • Interoperabilidad con Java


¿Por qué no usar TypeScript?

TypeScript es una excelente herramienta.

Sin embargo, sigue dependiendo del ecosistema JavaScript.


El objetivo de MateScript es diferente:

  • Compilar directamente a bytecode JVM
  • Ejecutarse sin Node.js
  • Integrarse con bibliotecas Java
  • Aprovechar el rendimiento y madurez de la plataforma JVM


Primeras decisiones de diseño

Variables inmutables por defecto

let name = "Emanuel";


Una variable declarada con let no podrá modificarse.


Para variables mutables utilizaremos:

var counter = 0;


Inferencia de tipos

No será necesario declarar tipos explícitamente.


let age = 42;


El compilador inferirá:

Int


Funciones

La sintaxis será muy similar a JavaScript.


function greet(name: String): String {

    return "Hola " + name;

}


Lambdas

let square = x => x * x;


Null Safety

Los tipos no aceptarán valores nulos por defecto.

let name: String = null;

Error de compilación.


Para permitir nulos:

let name: String?;


Y acá podemos hacer algo bastante elegante, podemos hacer que los tipos tengan alias por lo tanto : 

String? va a ser -> Optional<String> 


Ahora bien, como hago Optinal<T> , acá podemos utilizar el camino que usa Typescript union de tipos : T|Null y podemos hacer que Null sea un object como Scala. Y no vamos a tener nulos en nuestro lenguaje. 


¿Por qué la JVM?

La JVM es una de las plataformas más maduras de la industria.

  • Proporciona:
  • Garbage Collector
  • JIT Compiler
  • Ecosistema enorme
  • Bibliotecas maduras
  • Herramientas de monitoreo


En lugar de construir una máquina virtual propia, MateScript aprovechará toda esa infraestructura.


¿Qué sigue?

En los próximos artículos comenzaremos la implementación de MateScript.

El primer paso será construir una gramática utilizando ANTLR capaz de reconocer programas simples como:


let name = "Emanuel";

print(name);


A partir de allí iremos avanzando gradualmente hacia un compilador completo capaz de generar bytecode JVM.


La meta no es crear el próximo Kotlin. La meta es aprender cómo se construyen realmente los lenguajes de programación.


miércoles, 17 de junio de 2026

¿Por qué Clojure compila diferente? Ventajas y desventajas de ser un "Hosted Language" en la JVM


Cuando pensamos en lenguajes para la JVM, solemos meter en la misma bolsa a Java, Scala, Kotlin y Clojure. Después de todo, todos terminan ejecutándose sobre la misma máquina virtual.


Pero hay una diferencia fundamental en cómo están construidos.


Scala y Kotlin generan bytecode JVM de manera bastante directa. Clojure, en cambio, adopta una filosofía distinta: es un hosted language, es decir, un lenguaje "hospedado" sobre la plataforma Java.


¿Y qué significa eso? ¿Tiene ventajas? ¿Tiene costos?

Lenguajes como Java, Scala o Kotlin siguen aproximadamente este esquema:


Código fuente

      ↓

Compilador

      ↓

Bytecode JVM (.class)

      ↓

JVM


Muchas características del lenguaje se traducen directamente en clases, métodos y bytecode especializado.


Por ejemplo, una case class de Scala:

case class Person(name: String)

genera automáticamente métodos como:

  • equals()
  • hashCode()
  • copy()
  • toString()


Todo esto queda representado directamente en bytecode.


Clojure también produce bytecode JVM, pero gran parte del comportamiento del lenguaje vive en su runtime:


Código Clojure

      ↓

Compilador

      ↓

Bytecode + Runtime de Clojure

      ↓

JVM


Por eso Rich Hickey describe a Clojure como un hosted language.


La plataforma Java no es solamente un destino de compilación: es el ecosistema sobre el cual está construido el lenguaje.


Ventaja: Aprovecha toda la plataforma Java


Desde Clojure podemos usar directamente cualquier clase Java:


(import java.time.LocalDate)

(LocalDate/now)


No hay puentes especiales ni adaptadores.

Clojure reutiliza:

  • la JVM;
  • el recolector de basura;
  • las bibliotecas Java;
  • los hilos;
  • las excepciones;
  • las herramientas de profiling;
  • todo el ecosistema existente.


En vez de reinventar la rueda, se apoya en ella.


Ventaja: Un compilador relativamente pequeño


Muchas características importantes de Clojure viven en bibliotecas y no en el compilador:

  • secuencias perezosas;
  • colecciones persistentes;
  • STM;
  • transducers.


Esto hace que el compilador sea considerablemente más sencillo que los de Scala o Kotlin.


Ventaja: Desarrollo interactivo y REPL


Una de las mayores fortalezas de Clojure es su modelo de desarrollo interactivo.

Podemos definir una función:


(defn cuadrado [x]

  (* x x))


y cargarla inmediatamente en la REPL, sin recompilar todo el proyecto.


Este enfoque favorece:

  • feedback rápido;
  • hot reloading;
  • experimentación;
  • desarrollo incremental.


Ventaja: Evolución mediante librerías


Muchas características avanzadas de Clojure se agregaron sin modificar demasiado el lenguaje:

  • transducers;
  • core.async;
  • spec;
  • STM.


Al depender más del runtime y menos del compilador, la evolución del ecosistema suele ser más flexible.


Las desventajas

Menos optimizaciones

Scala y Kotlin generan bytecode más especializado.

En Clojure, muchas operaciones pasan por componentes del runtime como:

  • IFn;
  • PersistentVector;
  • PersistentMap;
  • RT.


Esto introduce niveles adicionales de indirección y, en ciertos casos, puede afectar el rendimiento.


Más reflexión

Si escribimos:

(defn largo [s]

  (.length s))


el compilador puede recurrir a reflexión.

Podemos ayudarlo con type hints:


(defn largo [^String s]

  (.length s))


pero esto requiere intervención del programador.


Menos comprobaciones en tiempo de compilación


Scala y Kotlin verifican muchas cosas antes de ejecutar:

  • tipos;
  • nulabilidad;
  • exhaustividad del pattern matching;
  • restricciones genéricas.


Clojure, al ser dinámico, detecta muchos errores recién en tiempo de ejecución.


Interoperabilidad asimétrica

Desde Clojure usar Java es muy fácil.

Pero desde Java consumir código Clojure no es tan natural:


IFn plus = Clojure.var("myns", "plus");


Mientras que una clase escrita en Scala o Kotlin suele verse como una clase Java convencional.


Dos filosofías distintas

No es que una aproximación sea mejor que la otra.

Scala y Kotlin intentan enriquecer la JVM mediante compiladores sofisticados y bytecode especializado.

Clojure adopta otra filosofía: construir un lenguaje pequeño y dinámico que reutilice al máximo la plataforma existente.

Al final, la diferencia más interesante no es a qué compilan, porque todos terminan ejecutándose en la JVM.

La verdadera diferencia es: ¿Cuánto del lenguaje vive en el compilador y cuánto vive en el runtime?

Y en esa decisión de diseño está gran parte de la personalidad de Clojure.


martes, 21 de abril de 2026

Records vs Clases vs Lombok vs Kotlin vs Scala


¿Cuál es la mejor forma de modelar datos? Desde los Struct de c++ nos venimos preguntando esto. Vamos a ver algunas opciones modernas que nos provee la plataforma java. 

Cuando trabajamos con objetos que representan datos (DTOs, Value Objects, etc.), distintos lenguajes ofrecen soluciones para evitar el boilerplate.


En este post comparamos:

  • Records en Java
  • Clases tradicionales
  • Lombok
  • Data classes en Kotlin
  • Case classes en Scala


1. Clase tradicional (Java)


public class Persona {

    private final String nombre;

    private final int edad;


    public Persona(String nombre, int edad) {

        this.nombre = nombre;

        this.edad = edad;

    }


    public String getNombre() { return nombre; }

    public int getEdad() { return edad; }


    @Override public boolean equals(Object o) { ... }

    @Override public int hashCode() { ... }

    @Override public String toString() { ... }

}

Ventajas

  • Total control
  • Compatible con todo (JPA, frameworks)


Desventajas

  • Mucho boilerplate
  • Propenso a errores


2. Records (Java)


public record Persona(String nombre, int edad) {}


Ventajas

  • Ultra conciso
  • Inmutabilidad garantizada
  • Sin dependencias


Desventajas

  • Menos flexible
  • No sirve bien con JPA
  • No herencia


3. Lombok (@Data)


import lombok.Data;


@Data

public class Persona {

    private String nombre;

    private int edad;

}


Ventajas

  • Reduce mucho código
  • Mutable o inmutable (configurable)


Desventajas

  • Dependencia externa
  • "Magia" en compilación (puede confundir)
  •  Problemas en tooling/debug


4. Data Classes (Kotlin)


data class Persona(val nombre: String, val edad: Int)


Ventajas

  • Muy conciso
  • Inmutable por defecto
  • copy() incluido
  • Destructuring


val (nombre, edad) = persona


Desventajas

  • Requiere usar Kotlin
  • Interoperabilidad Java no siempre perfecta


5. Case Classes (Scala)


case class Persona(nombre: String, edad: Int)


Ventajas

  • Inmutables
  • Pattern matching nativo
  • copy() automático
  • Muy expresivas


persona match {

  case Persona(nombre, edad) => println(nombre)

}


Desventajas

  • Curva de aprendizaje
  • Ecosistema más complejo


Y entonces? Y ninguno es super mejor, pero podemos tener estas reglas: 


Java Records

Ideal para:

  • DTOs simples
  • APIs REST
  • Código moderno sin dependencias


Son el "mínimo viable elegante" en Java.


Lombok

Ideal para:

  • Proyectos legacy
  • Equipos que ya lo usan


 Soluciona el problema… pero no es parte del lenguaje.


Kotlin

La mejor experiencia general para modelado de datos.

  • copy()
  • destructuring
  • null-safety


Es claramente superior en ergonomía.


Scala

El más poderoso conceptualmente.

  • Pattern matching real
  • Inmutabilidad fuerte
  • Integración con FP


Pero más complejo.


 Clases Java

 Siguen siendo necesarias cuando:

  • Usás JPA
  • Necesitás mutabilidad
  • Requerís control total


Si estás en Java moderno → Records

Si querés máxima productividad → Kotlin

Si buscás poder expresivo → Scala

Si estás en legacy → Lombok o clases



lunes, 20 de abril de 2026

Records en Java: Datos Inmutables de Forma Elegante


Desde Java 14, el lenguaje incorporó una nueva forma de definir clases de datos: los records.

Su objetivo es simple: reducir el boilerplate cuando solo queremos representar datos inmutables.


¿Qué problema vienen a resolver?

Antes de records, una clase típica para representar datos se veía así:


public class Persona {

    private final String nombre;

    private final int edad;


    public Persona(String nombre, int edad) {

        this.nombre = nombre;

        this.edad = edad;

    }


    public String getNombre() {

        return nombre;

    }


    public int getEdad() {

        return edad;

    }


    @Override

    public boolean equals(Object o) { ... }


    @Override

    public int hashCode() { ... }


    @Override

    public String toString() { ... }

}


Mucho código repetitivo, ¿no?

Con records, lo mismo se define así:


public record Persona(String nombre, int edad) {}


Y listo.


Java automáticamente genera:

  • Constructor
  • Getters (sin prefijo get)
  • equals()
  • hashCode()
  • toString()


Un record es una clase especial que:

  • Es inmutable
  • Es final
  • Extiende implícitamente de java.lang.Record


Persona p = new Persona("Juan", 30);

System.out.println(p.nombre()); // Juan

System.out.println(p.edad());   // 30


Podemos hacer un constructor personalizado por ejemplo para validar datos:


public record Persona(String nombre, int edad) {

    public Persona {

        if (edad < 0) {

            throw new IllegalArgumentException("Edad inválida");

        }

    }

}


Este es un constructor compacto.

Los records no son solo datos, también pueden tener lógica:


public record Persona(String nombre, int edad) {

    public boolean esMayorDeEdad() {

        return edad >= 18;

    }

}


Restricciones importantes:

  • No podés extender otras clases
  • Los atributos son implícitamente final
  • No hay setters
  • No están pensados para entidades mutables (ej: JPA)


¿Cuándo usar Records?

Usalos cuando:

✔ Tenés objetos de solo datos (DTOs)

✔ Querés inmutabilidad

✔ Buscás claridad y menos boilerplate


Evitarlos cuando:

❌ Necesitás mutabilidad

❌ Estás modelando entidades complejas

❌ Usás frameworks que requieren setters (como JPA tradicional)


Con features modernas (Java 21+), los records se integran muy bien con pattern matching:


if (obj instanceof Persona(String nombre, int edad)) {

    System.out.println(nombre);

}


Esto hace el código más declarativo y expresivo.


Los records son una de las mejoras más importantes en Java moderno:

  • Reducen código repetitivo
  • Fomentan inmutabilidad
  • Mejoran la legibilidad


Son ideales para modelar datos de forma clara y segura.

sábado, 18 de abril de 2026

Java 26: Hacia dónde evoluciona la plataforma


Java 26 representa una versión en evolución dentro del ciclo de releases de Java, donde se profundizan cambios importantes en concurrencia, rendimiento y modelo de datos.

Tipo de release: No LTS

Estado: 🧪 En desarrollo / Early Access

Enfoque: innovación + maduración de features clave


Concurrencia (Project Loom)

Java continúa consolidando un nuevo modelo de concurrencia.


Structured Concurrency (posible estabilización)

Permite agrupar tareas relacionadas como una unidad lógica

Simplifica:

  • manejo de errores
  • cancelación
  • sincronización


Hace que el código concurrente sea más legible y seguro


Virtual Threads (optimización continua)

  • Mejor integración con APIs existentes
  • Ajustes de rendimiento
  • Mayor adopción en frameworks


Lenguaje: menos boilerplate


Pattern Matching (más evolución)

  • Código más declarativo
  • Reducción de casts explícitos
  • Mejor integración con `switch`


Java sigue acercándose a un estilo más expresivo sin perder claridad.

Interoperabilidad nativa

Foreign Function & Memory API (FFM)

  • Más cerca de ser estándar definitivo
  • Alternativa real a JNI


Permite:

  • acceso eficiente a memoria off-heap
  • llamadas a librerías nativas


Clave para aplicaciones de alto rendimiento


Proyecto Valhalla (avance gradual)

Uno de los cambios más importantes a largo plazo.


Value Objects (en progreso)

  • Objetos sin identidad
  • Mejor uso de memoria
  • Mayor eficiencia en estructuras de datos


Impacto esperado:

  • colecciones más rápidas
  • menos overhead de objetos


JVM y rendimiento


  • Mejoras continuas en:
    • G1
    • ZGC
  • Optimización del JIT
  • Mejor uso de CPU moderna


Java 26 no es una versión de adopción masiva, sino una versión que:

  • empuja nuevas ideas
  • estabiliza features incubadas
  • prepara cambios grandes a futuro


viernes, 10 de abril de 2026

Java 25: Evolución y consolidación de la plataforma


Java SE 25 continúa el camino de modernización de Java, con foco en concurrencia, rendimiento y maduración de features introducidas en versiones anteriores.

Ojo es No LTS


Concurrencia (Project Loom)


Java sigue apostando fuerte por la concurrencia moderna.

Virtual Threads (madurez)

  • Totalmente integrados en el ecosistema
  • Mejoras en estabilidad y rendimiento
  • Transparencia casi total respecto a threads tradicionales


Beneficio clave: Escalar a miles de tareas concurrentes sin complejidad extra


Scoped Values (evolución)

Alternativa moderna a ThreadLocal:


ScopedValue<String> user = ScopedValue.newInstance();


ScopedValue.where(user, "Emanuel").run(() -> {

    System.out.println(user.get());

});


  • Inmutables
  • Seguros en entornos concurrentes
  • Ideales para usar con Virtual Threads


Structured Concurrency (avance)

  • Mejor organización de tareas relacionadas
  • Manejo más claro de errores y cancelaciones
  • Código más predecible


Lenguaje: más simple y expresivo


Pattern Matching (refinamientos)

  • Mejoras en switch
  • Menos casting manual
  • Código más limpio


switch (obj) {

    case String s -> System.out.println(s);

    case null -> System.out.println("null");

    default -> {}

}


Interoperabilidad


Foreign Function & Memory API

  • Más estable y usable
  • Reemplazo progresivo de JNI


Permite:

  • llamar a código nativo
  • manejar memoria off-heap de forma segura


JVM y rendimiento

Garbage Collectors


Mejoras continuas en:

  • G1
  • ZGC


Optimizaciones generales

  • Menor latencia
  • Mejor throughput
  • Ajustes en el JIT


Ecosistema

  • Mejor integración con frameworks modernos
  • Tooling más alineado con Virtual Threads
  • Preparación para proyectos futuros como Valhalla


Java 25 no busca revolucionar, sino consolidar lo que ya empezó a cambiar Java profundamente:

  • Concurrencia moderna ya usable en producción
  • Lenguaje más expresivo
  • Mejor performance


martes, 7 de abril de 2026

Java 24: Novedades y características principales


Java SE 24 continúa la evolución de la plataforma con foco en rendimiento, concurrencia y simplificación del lenguaje.

Ojo es no LTS!


Concurrencia moderna (Project Loom)

Uno de los ejes más importantes sigue siendo la evolución de la concurrencia.


Virtual Threads (mejoras)

  • Threads livianos gestionados por la JVM
  • Permiten manejar miles o millones de tareas concurrentes
  • Menor costo que los threads tradicionales


Structured Concurrency (incubating)

  • Permite tratar múltiples tareas como una sola unidad lógica
  • Mejora el manejo de errores y cancelaciones


Ejemplo conceptual:


try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {

    scope.fork(() -> servicioA());

    scope.fork(() -> servicioB());

    scope.join();

    scope.throwIfFailed();

}


Lenguaje: más expresividad


Pattern Matching (evolución)

  • Mejora continua en switch
  • Código más declarativo y menos verboso


switch (obj) {

    case String s -> System.out.println(s.length());

    case Integer i -> System.out.println(i * 2);

    default -> {}

}


Interoperabilidad nativa

Foreign Function & Memory API (evolución)

  • Reemplazo moderno de JNI
  • Acceso seguro a memoria fuera del heap
  • Llamadas a código nativo (C/C++)


Beneficios:

  • más performance
  •  menos complejidad que JNI


Rendimiento y JVM

Garbage Collectors

Mejoras en:

  • G1
  • ZGC


Optimizaciones generales

  • Mejor uso de CPU y memoria
  • Reducción de pausas


Otras mejoras

  • Refinamientos en APIs estándar
  • Mejoras internas en la JVM
  • Preparación para futuros proyectos como Valhalla


Java 24 no introduce cambios “revolucionarios”, pero sí consolida tendencias clave:

  • Concurrencia moderna (Loom)
  • Código más expresivo (Pattern Matching)
  • Mejor interoperabilidad (FFM API)
  • Performance constante


Es una versión que prepara el terreno para cambios más grandes en futuras releases.

domingo, 25 de enero de 2026

Scala 3.8: Una evolución significativa del lenguaje


El 22 de enero de 2026 se anunció el lanzamiento de Scala 3.8, una versión que moderniza partes importantes del lenguaje y prepara el camino para Scala 3.9 LTS (soporte a largo plazo). Esta actualización trae cambios técnicos profundos, mejoras en la biblioteca estándar y estabilizaciones de características esperadas por la comunidad. 


Scala 3.8 requiere JDK 17 o posterior para compilar y ejecutar programas. Esto marca una ruptura con versiones anteriores que soportaban JDK 8, y responde a cambios en las futuras versiones de la JVM donde APIs internas como sun.misc.Unsafe ya no son accesibles por defecto. 


Hasta ahora la biblioteca estándar de Scala se compilaba con Scala 2.13 y se reutilizaba desde Scala 3 gracias a la compatibilidad binaria cuidada. En Scala 3.8 la biblioteca estándar ya se compila con Scala 3 nativamente.

Esto no solo moderniza su implementación, sino que sienta las bases para liberar la biblioteca estándar de las dependencias históricas de Scala 2 en versiones futuras. 


A partir de Scala 3.8 el REPL ya no se incluye directamente en la distribución principal del compilador. Ahora es un artefacto independiente que debe agregarse explícitamente si lo necesitás. 

Esto permite:

  • Reducir el tamaño base de Scala.
  • Integrar mejor el REPL en herramientas y entornos de desarrollo.
  • Mejorar la experiencia de uso con nuevas librerías como fansi y pprint, haciendo la salida más legible. 


Scala 3.8 estabiliza varias mejoras importantes al lenguaje que estaban en preview:

“Better Fors”:  La versión mejorada de las comprensiones for que se desugarizan de forma más eficiente y natural ya está activada por defecto. Esto hace al código más predecible y evita algunas operaciones innecesarias de map/flatMap. 


La nueva característica runtimeChecked (SIP-57) reemplaza el uso histórico de: @unchecked cuando queremos que ciertas verificaciones de patrón se hagan en tiempo de ejecución. Es más clara y usable en cadenas de operaciones que pueden lanzar excepciones por patrones no exhaustivos. 


Características experimentales:


Scala 3.8 también introduce varias mejoras en estado de preview, disponibles con el flag -preview:

  • Implicits con into: Permite permitir conversiones implícitas de forma más controlada usando la palabra clave into.
  • Igualdad estricta con patrones: Una novedad experimental que facilita el uso de pattern matching seguro bajo strictEquality.
  • Varargs flexibles: Soporte experimental para spread múltiple en argumentos, haciendo el uso de varargs más expresivo y menos limitado. 


Además hay cambios del compilador y la biblioteca estándar:

  • Recomendaciones en herramientas de construcción: SBT, Mill y Scala CLI necesitan versiones actualizadas para trabajar correctamente con Scala 3.8. 
  • IDEs como IntelliJ IDEA y Metals están adaptándose para dar soporte completo a las nuevas características y cambios en la librería. 


Scala 3.8 marca el final del ciclo de nuevas características antes de entrar en feature freeze para preparar la versión Scala 3.9 LTS, que se espera sea la próxima distribución con soporte a largo plazo. 

Esto significa que muchas de las funciones estabilizadas en 3.8 serán la base para la próxima versión LTS, sin cambios incompatibles mayores cuando se publique. 


Scala 3.8 representa un hito en la evolución del lenguaje:

  • Moderniza la biblioteca estándar.
  • Eleva el requisito de JDK 17+.
  • Mejora el desugaring y el runtimeChecked.
  • Separa el REPL como artefacto.
  • Estabiliza mejoras largamente esperadas.
  • Abre la puerta a nuevas características experimentales.


En resumen, es una versión que cambia la base técnica del lenguaje, prepara el terreno para el futuro y consolida Scala como una de las opciones más potentes en la JVM para programación moderna. 


Dejo link:  https://www.scala-lang.org/news/3.8/

viernes, 19 de diciembre de 2025

std::variant en C++ — Unión segura y moderna


Desde C++17, std::variant forma parte de la STL como una alternativa segura y tipada a las uniones clásicas (union) y a jerarquías con herencia cuando solo necesitamos representar uno de varios tipos posibles.

std::variant<Ts...> es un tipo discriminado que puede contener exactamente uno de los tipos listados en Ts....


#include <variant>


std::variant<int, double, std::string> v;


En todo momento, v contiene un solo valor, y el compilador sabe qué tipos son válidos.

¿Por qué no usar union? Los union tradicionales:

  • No son type-safe
  • No manejan bien tipos no triviales
  • Requieren bookkeeping manual


std::variant soluciona esto:

  • Seguridad de tipos
  • Manejo correcto de constructores/destructores
  • Integración con la STL


std::variant<int, std::string> v1 = 10;

v1 = std::string("hola");


Si intentás asignar un tipo que no está en el variant, el código no compila.

Veamos como accedemos al valor: 


int x = std::get<int>(v1);


Lanza std::bad_variant_access si el tipo activo no coincide.


std::get_if<T> (forma segura)


if (auto p = std::get_if<std::string>(&v1)) {

    std::cout << *p;

}


Devuelve nullptr si el tipo no coincide.

Cómo saber qué tipo está activo


if (v1.index() == 0) {

    // es int

}


Usar index() suele ser menos expresivo que std::visit.

std::visit permite aplicar una función al valor contenido, sin saber su tipo concreto.


std::visit([](auto&& value) {

    std::cout << value;

}, v1);


El compilador genera una implementación segura para cada alternativa.


Overload pattern (muy usado)


template<class... Ts>

struct overloaded : Ts... {

    using Ts::operator()...;

};


template<class... Ts>

overloaded(Ts...) -> overloaded<Ts...>;


std::visit(overloaded{

    [](int i) { std::cout << "int: " << i; },

    [](const std::string& s) { std::cout << "string: " << s; }

}, v1);


Si una excepción ocurre durante una asignación:


if (v1.valueless_by_exception()) {

    // estado inválido

}


Es raro, pero importante en código robusto.


  • variant evita jerarquías artificiales
  • No necesita `virtual`
  • Más eficiente en muchos casos


Veamos el caso de uso típico: 


using Result = std::variant<int, std::string>;


Result parse(const std::string& s) {

    if (s.empty()) return "error";

    return std::stoi(s);

}


En C++26 o 29 contaremos con pattern matching, hoy tenemos que hacer :


std::visit(overloaded{

    [](int i) { /* ... */ },

    [](std::string s) { /* ... */ }

}, v);


Futuro (propuesto):


v match {

    int i        => /* ... */,

    std::string s => /* ... */

};


std::variant es la base natural para el pattern matching moderno.

  • std::variant es la unión segura del C++ moderno
  • Reduce errores de diseño
  • Elimina jerarquías innecesarias
  • Es clave para código expresivo y funcional


Si usás C++17 o superior, debería ser tu primera opción cuando necesitás representar una de varias alternativas.


lunes, 15 de diciembre de 2025

Pattern Matching en C++26 — ¿Qué es y cómo cambiará tu código?

 


C++ ha sido históricamente rico en herramientas de selección de flujo (if, switch, visitantes sobre std::variant, etc.), pero carece de una estructura nativa y unificada de pattern matching como la que vemos en Rust, Haskell o Swift. C++26 apunta (a través de la propuesta P2688R5) a llenar ese vacío. 

En ciencias de la computación, pattern matching es una forma declarativa de comparar una estructura de datos con uno o más patrones y —si coincide— ejecutar código asociado, extraer valores de forma segura y reducir mucho el boilerplate de código de control. 


Si alguna vez usaste pattern matching en lenguajes funcionales o en Rust:


match value {

    Some(x) => println!("Tiene valor: {}", x),

    None    => println!("No tiene valor"),

}


C++ tiene herramientas poderosas (como std::variant + std::visit), pero:

  • No existe un constructo nativo para comparar y destructurar tipos de forma concisa.
  • El tradicional switch solo funciona con valores integrales y carece de destructuración o bindings.


Pattern matching permite cosas como:

  • Comprobar la forma estructural de un valor.
  • Extraer datos de una std::tuple, std::variant, o tipos compuestos.
  • Reducir código repetitivo en código de control complejo.


Esto hace que tu código sea más claro, seguro y mantenible. 

¿Pattern Matching estará en C++26? Todavía no es definitivo.

El estándar C++26 aún está en borrador y pattern matching aún está en discusión dentro del comité WG21. La propuesta principal —P2688R5— introduce un nuevo constructo match muy similar a lo que otros lenguajes usan. 

Algunos desarrolladores creen que puede entrar en C++26 si todo marcha rápido. 

Otros opinan que podría retrasarse hasta C++29 debido a temas de diseño/sintaxis. 


Desde la propuesta P2688, se perfila un enfoque parecido a:


if (expr match [0, let foo]) {

    // foo está ligado a algo útil aquí

}


expr match -> result_type {

    pattern1 => /* acción 1 */,

    pattern2 => /* acción 2 */,

    _        => /* caso por defecto */

}


Aquí, match intenta casar expr con cada patrón.

let introduce variable(s) extraídas.

La sintaxis con => recuerda a Rust/Haskell, pero adaptada a C++ con sus propias reglas. 


Esta sintaxis aún no es parte definitiva del estándar — puede cambiar hasta la ratificación final.


sábado, 15 de noviembre de 2025

Comportamiento especial según el tipo genérico en C#


En C# no se puede sobrescribir un método solo porque el parámetro genérico es un tipo particular. El lenguaje no admite la especialización de clases genéricas como en C++, y la herencia no distingue entre MiClase<int> y MiClase<IMiInterfaz>.

Aun así, hay formas de lograr un comportamiento distinto según el tipo.

Una opción simple es decidir el comportamiento dentro del propio método:


class MiClase<T>

{

    public void Procesar(T valor)

    {

        if (valor is IMiInterfaz especial)

            ProcesarEspecial(especial);

        else

            ProcesarNormal(valor);

    }


    void ProcesarEspecial(IMiInterfaz x) => Console.WriteLine("Especial!");

    void ProcesarNormal(T x) => Console.WriteLine("Normal");

}


Si querés un despacho más flexible, podés usar dynamic:


class MiClase<T>

{

    public void Procesar(T valor)

        => ProcesarInterno((dynamic)valor);


    void ProcesarInterno(object v) => Console.WriteLine("Normal");

    void ProcesarInterno(IMiInterfaz v) => Console.WriteLine("Especial!");

}


También podés crear una subclase que se active solo para tipos que implementen una interfaz:


class MiClase<T>

{

    public virtual void Procesar(T x) => Console.WriteLine("Normal");

}


class MiClaseEspecial<T> : MiClase<T> where T : IMiInterfaz

{

    public override void Procesar(T x) => Console.WriteLine("Especial!");

}


En resumen, C# no permite sobrescribir métodos genéricos por tipo, pero sí es posible lograr comportamiento especializado combinando pattern matching, dynamic o herencia con restricciones.

viernes, 26 de septiembre de 2025

Pattern Matching en Elm: Desestructurando Datos de Forma Segura


El pattern matching en Elm es una herramienta poderosa para trabajar con listas, tuplas, registros y tipos algebraicos.

Permite desestructurar datos y cubrir todos los casos posibles de manera clara y segura, gracias al compilador.

Pattern Matching con listas:


primerElemento : List Int -> String

primerElemento lista =

    case lista of

        [] ->

            "Lista vacía"

        x :: xs ->

            "El primer elemento es " ++ String.fromInt x


[] → lista vacía.

x :: xs → descompone la lista en el primer elemento x y el resto xs.


Ejemplo:


primerElemento []          -- "Lista vacía"

primerElemento [10,20,30]  -- "El primer elemento es 10"


Pattern Matching con tuplas


sumaTupla : (Int, Int) -> Int

sumaTupla tupla =

    case tupla of

        (a, b) ->

            a + b


sumaTupla (3, 4)   -- 7


También podés combinar:


describeTupla : (String, Int) -> String

describeTupla (nombre, edad) =

    nombre ++ " tiene " ++ String.fromInt edad ++ " años"


Pattern Matching con registros


type alias Persona =

    { nombre : String

    , edad : Int

    }


saludo : Persona -> String

saludo persona =

    case persona of

        { nombre, edad } ->

            "Hola " ++ nombre ++ ", tenés " ++ String.fromInt edad ++ " años"


Ejemplo:

saludo { nombre = "Ana", edad = 30 }

-- "Hola Ana, tenés 30 años"


Pattern Matching con tipos algebraicos


Elm brilla con sus tipos definidos por el usuario.


type Resultado

    = Exito String

    | Error String


procesar : Resultado -> String

procesar res =

    case res of

        Exito mensaje ->

            "Todo salió bien: " ++ mensaje

        Error mensaje ->

            "Ocurrió un error: " ++ mensaje


Ejemplo:


procesar (Exito "Archivo guardado")

-- "Todo salió bien: Archivo guardado"


procesar (Error "Permiso denegado")

-- "Ocurrió un error: Permiso denegado"


Algo clave en Elm: el compilador exige cubrir todos los casos posibles en un case.

Si olvidás uno, el código no compila → esto evita errores en tiempo de ejecución.


  • case ... of permite desestructurar listas, tuplas, registros y tipos algebraicos.
  • Es una forma segura y declarativa de trabajar con datos.
  • El compilador obliga a cubrir todos los casos, garantizando que no haya estados no contemplados.


 El pattern matching es uno de los pilares que hace a Elm un lenguaje expresivo y confiable.


viernes, 15 de agosto de 2025

Pattern Matching en elm


Aprendimos a crear tipos personalizados con la palabra clave type. Nuestro ejemplo principal fue un usuario en una sala de chat:


type User

  = Regular String Int

  | Visitor String


Los usuarios habituales tienen nombre y edad, mientras que los visitantes solo tienen nombre. Ya tenemos nuestro tipo personalizado, pero ¿cómo lo usamos?

Supongamos que queremos una función toName que decida el nombre que se mostrará para cada usuario. Necesitamos usar una expresión case:


toName : User -> String

toName user =

  case user of

    Regular name age ->

      name

    Visitor name ->

      name


-- toName (Regular "Thomas" 44) == "Thomas"

-- toName (Visitor "kate95")    == "kate95"


La expresión case nos permite ramificar según la variante que veamos, así que, independientemente de si vemos a Thomas o a Kate, siempre sabremos cómo mostrar su nombre.

Y si probamos argumentos inválidos como toName (Visitar "kate95") o toName Anonymous, el compilador nos lo notifica inmediatamente. Esto significa que muchos errores simples se pueden corregir en segundos, en lugar de informar a los usuarios y consumir mucho más tiempo.

La función toName que acabamos de definir funciona de maravilla, pero ¿se da cuenta de que la edad no se utiliza en la implementación? Cuando algunos de los datos asociados no se utilizan, es habitual usar un comodín en lugar de asignarles un nombre:


toName : User -> String

toName user =

  case user of

    Regular name _ ->

      name

    Visitor name ->

      name


El _ reconoce los datos, pero también indica explícitamente que nadie los está utilizando.



sábado, 9 de agosto de 2025

is, as de C# y el viejo amigo instanceof de Java


En C#, trabajar con tipos en tiempo de ejecución es bastante cómodo gracias a dos operadores clave: is y as.

is nos dice si un objeto es de un tipo determinado, y desde C# 7 nos deja incluso declarar la variable directamente en la comprobación.


if (obj is string texto)

{

    Console.WriteLine(texto.ToUpper());

}


De esta forma, la verificación y el cast se hacen en una sola línea, clara y segura.


El otro operador, as, va un paso más allá: intenta convertir el objeto y, si no puede, devuelve null en lugar de lanzar una excepción. Ideal para esos casos donde la conversión no es obligatoria, pero sí útil si ocurre:


var texto = obj as string;

if (texto != null)

{

    Console.WriteLine(texto.Length);

}


Con estos dos, en C# rara vez tenemos que hacer casts inseguros, y eso se agradece.


Ahora, en el mundo Java siempre hemos tenido a instanceof, que durante años fue… digamos… más básico: verificaba el tipo, pero luego había que castear manualmente:


if (obj instanceof String) {

    String texto = (String) obj;

    System.out.println(texto.toUpperCase());

}


Esto cambió en Java 14, cuando instanceof adoptó pattern matching. Ahora podemos escribir:


if (obj instanceof String texto) {

    System.out.println(texto.toUpperCase());

}


Sí, muy similar a lo que C# ya hacía.



Y con Java 21 llegó la verdadera vuelta de tuerca: pattern matching dentro de switch. Esto no solo ahorra código, sino que hace más expresivas estructuras de control que antes eran un desfile de if/else.


switch (obj) {

    case String s -> System.out.println("Cadena: " + s.toUpperCase());

    case Integer i -> System.out.println("Entero: " + (i * 2));

    default -> System.out.println("Otro tipo");

}


Así que hoy, si trabajás en C#, tenés en is y as dos herramientas muy potentes para escribir código seguro y legible. Y si venís de Java, sabé que instanceof ya no es el operador limitado de antes: está alcanzando la versatilidad de C#, y con switch incluso ofrece caminos nuevos para organizar la lógica.

En ambos lenguajes, dominar estas técnicas significa escribir código más limpio, menos propenso a errores y, sobre todo, más agradable de mantener.


martes, 29 de julio de 2025

Comparando obj != null, obj is { } y obj is not null en C#


En C#, hay varias formas de verificar si una variable no es null. Con la introducción de pattern matching, surgieron nuevas opciones como obj is { } y obj is not null. Aunque parecen equivalentes, no son exactamente lo mismo y conviene entender cuándo usar cada uno.


obj != null


Es la forma clásica de comprobar si una referencia no es null. Evalúa directamente la igualdad.


if (obj != null)

{

    // obj no es null

}


Cuándo usarlo:

  • Para chequeos simples de null.
  • En código tradicional o cuando la variable no participa en pattern matching.


Pero esto, no hace type test ni pattern matching. Solo evalúa igualdad.


obj is not null

Usa pattern matching para comprobar si la variable no es null.


if (obj is not null)

{

    // obj no es null

}


Cuándo usarlo:

  • Cuando querés mantener consistencia con otros patrones (`is int`, `is string s`, etc.).
  • Útil en expresiones más complejas con pattern matching.
  • Funciona igual que obj != null, pero es más expresivo en escenarios donde se usan patrones.


obj is { }


Este patrón comprueba que obj no sea null y coincida con un patrón de objeto.

{ } significa un objeto con cualquier valor de propiedades.


if (obj is { })

{

    // obj no es null

}


Cuándo usarlo:

  • Cuando querés usar pattern matching y, además, extraer propiedades en la misma expresión.


Ejemplo:


if (obj is { Nombre: var nombre, Edad: > 18 })

{

    Console.WriteLine($"{nombre} es mayor de edad");

}


¿Cuál es más performante?

obj != null : más rápido, es una simple comparación de referencia.

obj is not null: ligeramente más lento, pero casi idéntico (usa pattern matching internamente).

obj is { } : un poco más costoso porque implica verificar coincidencia de patrón de objeto.


Pero en la práctica, la diferencia de rendimiento es mínima y solo importa en código crítico (hot paths).


También se puede usar el operador null-coalescing (??):


var valor = obj ?? new MiObjeto();


O el null-conditional (?.):


obj?.Metodo();


Pero ese tema es para otro post ... 


domingo, 18 de mayo de 2025

Tipos Abstractos y Polimorfismo en Programación Funcional


Cuando pensamos en abstracción y polimorfismo, solemos imaginar clases, interfaces y herencia. Pero ¿sabías que la programación funcional también tiene sus propios superpoderes para modelar el comportamiento genérico y abstraer detalles? 

En POO, usamos clases abstractas o interfaces para definir estructuras que deben ser implementadas. En programación funcional, el enfoque es distinto, pero el objetivo es similar: ocultar detalles de implementación y exponer un comportamiento general.

Un ADT define un tipo por sus operaciones, no por cómo están implementadas. Por ejemplo:


data Pila a = Vacía | Empujar a (Pila a)


Este tipo Pila podría representar una pila genérica, y podríamos tener funciones que operen sobre ella sin importar cómo esté construida internamente.

En la programación funcional se identifican varios tipos de polimorfismo:

Polimorfismo Paramétrico: Permite escribir funciones genéricas sobre cualquier tipo. Es como los genéricos de Java, pero más poderoso:


identidad :: a -> a

identidad x = x


La función identidad funciona para cualquier tipo a.


Polimorfismo ad-hoc (Typeclasses / Traits / Protocolos) : En Haskell, Rust, Scala o Elixir podemos definir interfaces de comportamiento según el tipo. Esto recuerda al "método virtual" de POO.


class Metrico a where

    distancia :: a -> a -> Double


instance Metrico (Double, Double) where

    distancia (x1, y1) (x2, y2) =

        sqrt ((x2 - x1)^2 + (y2 - y1)^2)


Veamos un ejemplo en Scala:


trait Metrico[T] {

  def distancia(a: T, b: T): Double

}


implicit object Punto2D extends Metrico[(Double, Double)] {

  def distancia(a: (Double, Double), b: (Double, Double)) =

    math.sqrt(math.pow(a._1 - b._1, 2) + math.pow(a._2 - b._2, 2))

}


def calcularDistancia[T](a: T, b: T)(implicit m: Metrico[T]) =

  m.distancia(a, b)


Pattern Matching como Polimorfismo Estructural: Otro recurso poderoso es el pattern matching, que permite seleccionar comportamiento según la "forma" del dato.


sealed trait Forma

case class Circulo(r: Double) extends Forma

case class Rectangulo(ancho: Double, alto: Double) extends Forma


def area(f: Forma): Double = f match {

  case Circulo(r)       => math.Pi * r * r

  case Rectangulo(a, h) => a * h

}


¿Y qué ganamos con esto?

  • Abstracción sin herencia: no hay jerarquías rígidas.
  • Mayor seguridad de tipos: muchos errores se detectan en tiempo de compilación.
  • Separación de datos y comportamiento: las funciones no "viven" dentro de las estructuras de datos, lo cual facilita la composición y el testing.


La programación funcional ofrece mecanismos muy sólidos y expresivos para manejar abstracción y polimorfismo. Aunque no se usa herencia, se logra el mismo efecto (o incluso uno más flexible) usando funciones genéricas, pattern matching y typeclasses.


sábado, 15 de marzo de 2025

Clases Selladas y coincidencia de patrones en Java


Con la evolución de Java, dos características han tomado mayor protagonismo: las clases selladas (sealed classes) y el coincidencia de patrones (pattern matching). La combinación de ambas permite escribir código más seguro, expresivo y mantenible.

Las clases selladas permiten restringir qué clases pueden extender o implementar una clase o interfaz. Se declaran con la palabra clave sealed y deben especificar sus subclases con permits.


Por ejemplo:


public sealed class Figura permits Circulo, Poligono {}

public final class Circulo extends Figura {}

public sealed class Poligono extends Figura permits Triangulo {}

public non-sealed class Triangulo extends Poligono {}


Aquí, Figura solo puede ser extendida por Circulo y Poligono, mientras que Triangulo puede ser extendido libremente.

El pattern matching simplifica la lógica condicional al combinar la comprobación de tipos y la conversión en una sola operación.

Por ejemplo:


Object obj = "Hola, mundo!";

if (obj instanceof String s) {

    System.out.println(s.toUpperCase());

}


Si obj es un String, se asigna automáticamente a s, evitando el casting manual.


Tambien lo podemos usar con switch: 


static void procesar(Object obj) {

    switch (obj) {

        case String s -> System.out.println("Es una cadena: " + s.toUpperCase());

        case Integer i -> System.out.println("Es un número: " + (i * 2));

        default -> System.out.println("Tipo no soportado");

    }

}


El switch maneja diferentes tipos de objetos de manera más clara y concisa.

La combinación de ambas características permite que el compilador conozca todas las posibles subclases en tiempo de compilación, facilitando el uso de switch y garantizando que se manejen todos los casos.

Por ejemplo: 


public sealed interface Operacion permits Suma, Resta, Multiplicacion {}


public record Suma(int a, int b) implements Operacion {}

public record Resta(int a, int b) implements Operacion {}

public record Multiplicacion(int a, int b) implements Operacion {}


public int evaluar(Operacion op) {

    return switch (op) {

        case Suma s -> s.a() + s.b();

        case Resta r -> r.a() - r.b();

        case Multiplicacion m -> m.a() * m.b();

    };

}


Aquí, el switch cubre todas las implementaciones posibles de Operacion, asegurando que no haya casos no manejados.

Las clases selladas y el pattern matching en Java permiten escribir código más seguro y expresivo. Al proporcionar control sobre la herencia y simplificar la lógica condicional, facilitan la creación de software más mantenible y alineado con las tendencias modernas del desarrollo en Java.


lunes, 24 de febrero de 2025

Pattern Matching en Java


La coincidencia de patrones (Pattern Matching) en switch es una característica introducida en Java 17 como una mejora en la sintaxis del switch, permitiendo evaluar tipos de manera más sencilla y concisa. Esta funcionalidad facilita la escritura de código más limpio y seguro al eliminar la necesidad de casting manual.

Veamos un ejemplo, antes de java 17 haciamos:

 

static void procesar(Object obj) {

    switch (obj.getClass().getSimpleName()) {

        case "String":

            String s = (String) obj;

            System.out.println("Es una cadena: " + s.toUpperCase());

            break;

        case "Integer":

            Integer i = (Integer) obj;

            System.out.println("Es un número: " + (i * 2));

            break;

        default:

            System.out.println("Tipo no soportado");

    }

}


Y en Java 17: 


static void procesar(Object obj) {

    switch (obj) {

        case String s -> System.out.println("Es una cadena: " + s.toUpperCase());

        case Integer i -> System.out.println("Es un número: " + (i * 2));

        default -> System.out.println("Tipo no soportado");

    }

}


Esta disponible a partir de Java 17 en vista previa y consolidado en versiones posteriores. Algo a tener en cuanta es que solo se puede utilizar en switch con expresiones y no con estructuras más antiguas.

El pattern matching en switch es una mejora significativa que permite un código más limpio y expresivo. Reduce el uso de casts y mejora la legibilidad del código, haciendo que la programación en Java sea más moderna y eficiente.


sábado, 22 de febrero de 2025

Clases Selladas en Java


Las clases selladas en Java, introducidas en Java 15 como una característica en vista previa y estandarizadas en Java 17, permiten restringir qué clases pueden heredar de una clase base, mejorando la seguridad y el diseño del código.

Las clases selladas (sealed) establecen un conjunto específico de clases que pueden extender o implementar una clase base, evitando la herencia no controlada.


public sealed class Figura permits Circulo, Rectangulo {}


public final class Circulo extends Figura {}

public final class Rectangulo extends Figura {}


Aquí, Figura es una clase sellada, lo que significa que solo Circulo y Rectangulo pueden heredar de ella.

Las clases que extienden una clase sellada deben usar una de las siguientes opciones:

  • final: No permite más subclases.
  • sealed: Permite definir otro conjunto restringido de subclases.
  • non-sealed: Permite herencia sin restricciones.


Ejemplo con diferentes modificaciones:


public sealed class Figura permits Circulo, Poligono {}


public final class Circulo extends Figura {}

public sealed class Poligono extends Figura permits Triangulo {}

public non-sealed class Triangulo extends Poligono {}


Las clases selladas en Java proporcionan un control más preciso sobre la herencia, facilitando la definición de modelos de dominio más seguros y expresivos. Son especialmente útiles cuando se combinan con switch y pattern matching para estructurar mejor el código.