Translate

viernes, 7 de marzo de 2025

API de Vectores en Java: Alto Rendimiento con Operaciones SIMD


Java ha introducido mejoras significativas en el rendimiento con la API de Vectores, una funcionalidad experimental desde Java 16 y evolucionada en Java 19 y 20. Esta API permite aprovechar las capacidades de SIMD (Single Instruction, Multiple Data), lo que mejora la eficiencia en cálculos intensivos, como gráficos, procesamiento de señales y machine learning.

La API de Vectores en Java permite realizar operaciones con múltiples valores en paralelo utilizando instrucciones SIMD, optimizando así el uso del hardware subyacente. Está diseñada para ejecutarse de manera más eficiente en CPU modernas con conjuntos de instrucciones avanzados como AVX y NEON.

Las características principales son:

  • Operaciones eficientes con vectores: Realiza cálculos numéricos sobre múltiples datos simultáneamente.
  • Compatibilidad con JVM y JIT: Se integra con el compilador Just-In-Time para optimizar el rendimiento.
  • Optimización automática: Se adapta a la arquitectura del procesador para aprovechar el mejor conjunto de instrucciones disponible.
  • API expresiva y segura: Usa un enfoque declarativo y sin riesgo de desbordamiento de memoria.


Veamos un ejemplo: 


import jdk.incubator.vector.FloatVector;

import jdk.incubator.vector.VectorSpecies;


public class VectorExample {

    static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;


    public static void main(String[] args) {

        float[] a = {1.0f, 2.0f, 3.0f, 4.0f};

        float[] b = {5.0f, 6.0f, 7.0f, 8.0f};

        float[] result = new float[a.length];


        var va = FloatVector.fromArray(SPECIES, a, 0);

        var vb = FloatVector.fromArray(SPECIES, b, 0);

        var vc = va.add(vb);

        vc.intoArray(result, 0);


        System.out.println("Resultado: ");

        for (float v : result) {

            System.out.print(v + " ");

        }

    }

}


  • Se define una especie de vector (SPECIES_PREFERRED) que se ajusta a la arquitectura del CPU.
  • Se cargan los datos en vectores FloatVector.
  • Se realiza una operación de suma en paralelo.
  • Se almacenan los resultados en un array de salida.


En Java 19 y 20 se continuo trabajando obteniendo estas mejoras:

  • Mayor estabilidad y optimización en JVM: Se han reducido las sobrecargas en JIT.
  • Soporte extendido para más arquitecturas: Ahora es compatible con ARM y RISC-V.
  • Nuevas operaciones matemáticas: Más funciones avanzadas como cálculos trigonométricos y logarítmicos.


La API de Vectores en Java es una gran incorporación para quienes necesitan alto rendimiento en operaciones matemáticas y científicas. Con el soporte mejorado en arquitecturas modernas y una mayor optimización en la JVM, esta API se perfila como una herramienta clave para el desarrollo de aplicaciones de alto rendimiento.

miércoles, 5 de marzo de 2025

Cómo Spring Implementa AOP


Spring AOP se basa en el Patrón Proxy y la generación dinámica de clases para aplicar aspectos sin modificar el código fuente original.

Spring AOP crea proxies para interceptar llamadas a métodos y aplicar lógica adicional. Dependiendo de la estructura de la clase objetivo, Spring elige entre dos enfoques:

JDK Dynamic Proxies: Si la clase implementa una interfaz, Spring usa java.lang.reflect.Proxy para generar un proxy en tiempo de ejecución.

CGLIB (Code Generation Library): Si la clase no implementa interfaces, se genera una subclase dinámica con CGLIB.


Veamos un ejemplo de un proxy con JDK:


import java.lang.reflect.*;


interface Servicio {

    void ejecutar();

}


class ServicioImpl implements Servicio {

    public void ejecutar() {

        System.out.println("Ejecutando servicio...");

    }

}


class ProxyHandler implements InvocationHandler {

    private final Object target;

    public ProxyHandler(Object target) {

        this.target = target;

    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        System.out.println("[AOP] Antes de ejecutar");

        Object result = method.invoke(target, args);

        System.out.println("[AOP] Después de ejecutar");

        return result;

    }

}


public class Main {

    public static void main(String[] args) {

        Servicio servicio = (Servicio) Proxy.newProxyInstance(

            Servicio.class.getClassLoader(),

            new Class[]{Servicio.class},

            new ProxyHandler(new ServicioImpl()));

        servicio.ejecutar();

    }

}


Si la clase no implementa interfaces, Spring usa CGLIB para generar una subclase dinámica.


import net.sf.cglib.proxy.*;


class Servicio {

    public void ejecutar() {

        System.out.println("Ejecutando servicio...");

    }

}


class Interceptor implements MethodInterceptor {

    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {

        System.out.println("[AOP] Antes de ejecutar");

        Object result = proxy.invokeSuper(obj, args);

        System.out.println("[AOP] Después de ejecutar");

        return result;

    }

}


public class Main {

    public static void main(String[] args) {

        Enhancer enhancer = new Enhancer();

        enhancer.setSuperclass(Servicio.class);

        enhancer.setCallback(new Interceptor());

        Servicio proxy = (Servicio) enhancer.create();

        proxy.ejecutar();

    }

}


Spring AOP aplica aspectos mediante proxies dinámicos. Si la clase tiene una interfaz, usa JDK Dynamic Proxies; si no, usa CGLIB para generar una subclase dinámica. Esto permite agregar lógica sin modificar el código fuente ni el bytecode.



lunes, 3 de marzo de 2025

Programación Orientada a Objetos en Python parte 2


Ya vimos lo complejo que es crear software y como la programación orientada a objetos nos ayuda.

Ahora veamos como se aplican en Python, empecemos con las reglas de Nomenclatura.

En Python existen convenciones de nomenclatura recomendadas por la PEP 8, que es la guía de estilo oficial del lenguaje. Las reglas principales para definir clases, métodos y atributos:

Nombres de Clases  

- Se usa PascalCase (también conocido como **CamelCase** con la primera letra en mayúscula).  

- Cada palabra comienza con mayúscula, sin guiones bajos.  

Por ejemplo: 

  class MiClase:

      pass


Nombres de Métodos y Atributos Publicos  

- Se usa snake_case (minúsculas separadas por guiones bajos).  

Por ejemplo:

  class Persona:

      def obtener_nombre(self):

          return "Juan"


Nombres de Métodos y Atributos Privados

- Se usa un guion bajo inicial (_) para indicar que es un atributo/método de uso interno (convención, no es privado en sentido estricto).  

Por ejemplo:

  class Persona:

      def _calcular_edad(self):

          return 30


Atributos y Métodos Realmente Privados  

- Se usa doble guion bajo (`__`) para evitar colisiones de nombres en clases hijas (_name mangling_).  

Por ejemplo:

  class Persona:

      def __metodo_secreto(self):

          print("Este método es realmente privado")


Atributos y Métodos Estáticos o de Clase  

- Métodos de clase (@classmethod) y atributos de clase siguen la convención snake_case.  

- Métodos estáticos (@staticmethod) también siguen snake_case.

Por ejemplo:

  class Utilidades:

      @classmethod

      def metodo_de_clase(cls):

          pass


      @staticmethod

      def metodo_estatico():

          pass


Constantes

- Se usan mayúsculas con guiones bajos.  

Por ejemplo:

  class Config:

      MAX_INTENTOS = 5


Definiendo clases

Vista ya la parte teórica, vamos a ver como podemos hacer uso de la programación orientada a objetos en Python. Lo primero es crear una clase, para ello usaremos el ejemplo de perro.


# Creando una clase vacía

class Perro:

    pass


Se trata de una clase vacía y sin mucha utilidad práctica, pero es la mínima clase que podemos crear. Nótese el uso del pass que no hace realmente nada, pero daría un error si después de los : no tenemos contenido.

Ahora que tenemos la clase, podemos crear un objeto de la misma. Podemos hacerlo como si de una variable normal se tratase. Nombre de la variable igual a la clase con (). Dentro de los paréntesis irían los parámetros de entrada si los hubiera.


# Creamos un objeto de la clase perro

mi_perro = Perro()


Definiendo atributos

A continuación vamos a añadir algunos atributos a nuestra clase. Antes de nada es importante distinguir que existen dos tipos de atributos:

Atributos de instancia: Pertenecen a la instancia de la clase o al objeto. Son atributos particulares de cada instancia, en nuestro caso de cada perro.

Atributos de clase: Se trata de atributos que pertenecen a la clase, por lo tanto serán comunes para todos los objetos.

Empecemos creando un par de atributos de instancia para nuestro perro, el nombre y la raza. Para ello creamos un método __init__ que será llamado automáticamente cuando creemos un objeto. Se trata del constructor.


class Perro:

    

    # El método __init__ es llamado al crear el objeto

    def __init__(self, nombre, raza):

        print(f"Creando perro {nombre}, {raza}")


        # Atributos de instancia

        self.nombre = nombre

        self.raza = raza


Ahora que hemos definido el método init con dos parámetros de entrada, podemos crear el objeto pasando el valor de los atributos. Usando type() podemos ver como efectivamente el objeto es de la clase Perro.


mi_perro = Perro("Toby", "Bulldog")

print(type(mi_perro))

# Creando perro Toby, Bulldog

# <class '__main__.Perro'>


Seguramente te hayas fijado en el self que se pasa como parámetro de entrada del método. Es una variable que representa la instancia de la clase, y deberá estar siempre ahí.

El uso de __init__ y el doble __ no es una coincidencia. Cuando veas un método con esa forma, significa que está reservado para un uso especial del lenguaje. En este caso sería lo que se conoce como constructor. 

Por último, podemos acceder a los atributos usando el objeto y . (el punto)


print(mi_perro.nombre) # Toby

print(mi_perro.raza)   # Bulldog


Hasta ahora hemos definido atributos de instancia, ya que son atributos que pertenecen a cada perro concreto. Ahora vamos a definir un atributo de clase, que será común para todos los perros. Por ejemplo, la especie de los perros es algo común para todos los objetos Perro.


class Perro:

    # Atributo de clase

    especie = 'mamífero'


    # El método __init__ es llamado al crear el objeto

    def __init__(self, nombre, raza):

        print(f"Creando perro {nombre}, {raza}")


        # Atributos de instancia

        self.nombre = nombre

        self.raza = raza


Dado que es un atributo de clase, no es necesario crear un objeto para acceder al atributos. Podemos hacer lo siguiente.


print(Perro.especie)

# mamífero


Se puede acceder también al atributo de clase desde el objeto.


mi_perro = Perro("Toby", "Bulldog")

mi_perro.especie

# 'mamífero'


De esta manera, todos los objetos que se creen de la clase perro compartirán ese atributo de clase, ya que pertenecen a la misma.


Definiendo métodos

En realidad cuando usamos __init__ anteriormente ya estábamos definiendo un método, solo que uno especial. A continuación vamos a ver como definir métodos que le den alguna funcionalidad interesante a nuestra clase, siguiendo con el ejemplo de perro.

Vamos a codificar dos métodos, ladrar y caminar. El primero no recibirá ningún parámetro y el segundo recibirá el número de pasos que queremos andar. Como hemos indicado anteriormente self hace referencia a la instancia de la clase. Se puede definir un método con def y el nombre, y entre () los parámetros de entrada que recibe, donde siempre tendrá que estar self el primero.


class Perro:

    # Atributo de clase

    especie = 'mamífero'


    # El método __init__ es llamado al crear el objeto

    def __init__(self, nombre, raza):

        print(f"Creando perro {nombre}, {raza}")


        # Atributos de instancia

        self.nombre = nombre

        self.raza = raza


    def ladra(self):

        print("Guau")


    def camina(self, pasos):

        print(f"Caminando {pasos} pasos")


Por lo tanto si creamos un objeto mi_perro, podremos hacer uso de sus métodos llamándolos con . y el nombre del método. Como si de una función se tratase, pueden recibir y devolver argumentos.


mi_perro = Perro("Toby", "Bulldog")

mi_perro.ladra()

mi_perro.camina(10)


# Creando perro Toby, Bulldog

# Guau

# Caminando 10 pasos


Métodos en Python: instancia, clase y estáticos

Es posible crear diferentes tipos de métodos:

  • Lo métodos de instancia “normales” que ya hemos visto como metodo()
  • Métodos de clase usando el decorador @classmethod
  • Y métodos estáticos usando el decorador @staticmethod

En la siguiente clase tenemos un ejemplo donde definimos los tres tipos de métodos.


class Clase:

    def metodo(self):

        return 'Método normal', self


    @classmethod

    def metododeclase(cls):

        return 'Método de clase', cls


    @staticmethod

    def metodoestatico():

        return "Método estático"


Veamos su comportamiento en detalle uno por uno.


Métodos de instancia

Los métodos de instancia son los métodos normales, de toda la vida, que hemos visto anteriormente. Reciben como parámetro de entrada self que hace referencia a la instancia que llama al método. También pueden recibir otros argumentos como entrada.

Para saber más: El uso de "self" es totalmente arbitrario. Se trata de una convención acordada por los usuarios de Python, usada para referirse a la instancia que llama al método, pero podría ser cualquier otro nombre. Lo mismo ocurre con "cls", que veremos a continuación.


class Clase:

    def metodo(self, arg1, arg2):

        return 'Método normal', self


Y como ya sabemos, una vez creado un objeto pueden ser llamados.


mi_clase = Clase()

mi_clase.metodo("a", "b")

# ('Método normal', <__main__.Clase at 0x10b9daa90>)


En vista a esto, los métodos de instancia:

  •  Pueden acceder y modificar los atributos del objeto.
  •  Pueden acceder a otros métodos.
  •   Dado que desde el objeto self se puede acceder a la clase con ` self.class`, también pueden modificar el estado de la clase


Métodos de clase (classmethod)

A diferencia de los métodos de instancia, los métodos de clase reciben como argumento cls, que hace referencia a la clase. Por lo tanto, pueden acceder a la clase pero no a la instancia.


class Clase:

    @classmethod

    def metododeclase(cls):

        return 'Método de clase', cls


Se pueden llamar sobre la clase.


Clase.metododeclase()

# ('Método de clase', __main__.Clase)


Pero también se pueden llamar sobre el objeto.


mi_clase.metododeclase()

# ('Método de clase', __main__.Clase)


Por lo tanto, los métodos de clase:

  • No pueden acceder a los atributos de la instancia.
  • Pero si pueden modificar los atributos de la clase.
  • Métodos estáticos (staticmethod)

Por último, los métodos estáticos se pueden definir con el decorador @staticmethod y no aceptan como parámetro ni la instancia ni la clase. Es por ello por lo que no pueden modificar el estado ni de la clase ni de la instancia. Pero por supuesto pueden aceptar parámetros de entrada.


class Clase:

    @staticmethod

    def metodoestatico():

        return "Método estático"


mi_clase = Clase()

Clase.metodoestatico()

mi_clase.metodoestatico()


# 'Método estático'

# 'Método estático'


Por lo tanto el uso de los métodos estáticos pueden resultar útil para indicar que un método no modificará el estado de la instancia ni de la clase. Es cierto que se podría hacer lo mismo con un método de instancia por ejemplo, pero a veces resulta importante indicar de alguna manera estas peculiaridades, evitando así futuros problemas y malentendidos.

En otras palabras, los métodos estáticos se podrían ver como funciones normales, con la salvedad de que van ligadas a una clase concreta.

sábado, 1 de marzo de 2025

Programación Orientada a Aspectos (AOP) en Spring


La Programación Orientada a Aspectos (AOP, Aspect-Oriented Programming) es un paradigma que permite separar las preocupaciones transversales del código principal, como logging, seguridad, manejo de transacciones y monitoreo. Spring proporciona Spring AOP y compatibilidad con AspectJ para implementar esta funcionalidad de manera eficiente.

En una aplicación, hay funcionalidades que se repiten en diferentes partes del código (logging, manejo de excepciones, validaciones). Sin AOP, estas funciones deben implementarse de forma manual en cada clase, lo que genera código repetitivo y difícil de mantener.

AOP permite interceptar la ejecución de métodos y agregar lógica adicional sin modificar la implementación original.

Veamos un ejemplo. Para implementar un aspecto en Spring, necesitamos habilitar AOP y definir un aspecto con consejos (`advice`).

Primero agregamos la dependencia, en este ejemplo uso maven: 


<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-aop</artifactId>

</dependency>


@Configuration

@EnableAspectJAutoProxy

@ComponentScan("com.example")

public class AppConfig {

}


Ahora definimos el aspecto de Logging


import org.aspectj.lang.annotation.*;

import org.springframework.stereotype.Component;


@Aspect

@Component

public class LoggingAspect {

    

    @Before("execution(* com.example.service.*.*(..))")

    public void logBeforeMethod() {

        System.out.println("[LOG] - Método invocado...");

    }

}


@Aspect: Indica que la clase es un aspecto.

@Before("execution(* com.example.service.*.*(..))"): Aplica el aspecto antes de ejecutar cualquier método dentro del paquete `com.example.service`.

execution(* com.example.service.*.*(..)): Todos los métodos de cualquier clase en com.example.service.

Podemos usar @Around para envolver la ejecución de un método y medir su tiempo de respuesta.


import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.*;

import org.springframework.stereotype.Component;


@Aspect

@Component

public class PerformanceAspect {

    

    @Around("execution(* com.example.service.*.*(..))")

    public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {

        long start = System.currentTimeMillis();

        Object result = joinPoint.proceed(); // Llama al método original

        long end = System.currentTimeMillis();

        System.out.println("Tiempo de ejecución: " + (end - start) + "ms");

        return result;

    }

}

@Around: Permite modificar la ejecución del método original.

joinPoint.proceed(): Ejecuta el método interceptado.

System.currentTimeMillis(): Mide el tiempo antes y después de la ejecución.


Ahora veamos un ejemplo con AspectJ: Spring AOP solo funciona con beans de Spring, pero si queremos interceptar cualquier clase, podemos usar AspectJ full.

Agregamos a nuestro pom: 


<dependency>

    <groupId>org.aspectj</groupId>

    <artifactId>aspectjweaver</artifactId>

    <version>1.9.7</version>

</dependency>


Habilitamos AspectJ en Spring Boot


@EnableAspectJAutoProxy(proxyTargetClass = true)


Definimos un aspecto con AspectJ


@Aspect

public class SecurityAspect {

    

    @Before("execution(* *(..))") // Aplica a cualquier método

    public void checkSecurity() {

        System.out.println("[SECURITY] - Verificando permisos...");

    }

}

Spring AOP permite separar lógica transversal, reduciendo la repetición de código y mejorando la mantenibilidad. Con @Aspect y @Pointcut, podemos interceptar métodos y agregar funcionalidades como logging, seguridad y performance. Para casos avanzados, AspectJ permite interceptar cualquier clase en la aplicación.


viernes, 28 de febrero de 2025

API de Servidor Web Simple en Java



A partir de Java 18, se introdujo una API de Servidor Web Simple que permite crear servidores HTTP ligeros sin necesidad de dependencias externas. Esta funcionalidad es ideal para pruebas rápidas, prototipos y aplicaciones livianas.

Como características principales podemos nombrar:

  • Servidor HTTP embebido sin necesidad de librerías adicionales.
  • Manejo de solicitudes GET y POST.
  • Facilidad de uso con pocas líneas de código.
  • Soporte para configuración de rutas y respuestas personalizadas.


Veamos un hola mundo : 


import com.sun.net.httpserver.HttpServer;

import com.sun.net.httpserver.HttpHandler;

import com.sun.net.httpserver.HttpExchange;

import java.io.IOException;

import java.io.OutputStream;

import java.net.InetSocketAddress;


public class SimpleWebServer {

    public static void main(String[] args) throws IOException {

        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);

        server.createContext("/hello", new HelloHandler());

        server.setExecutor(null);

        server.start();

        System.out.println("Servidor iniciado en http://localhost:8080/");

    }


    static class HelloHandler implements HttpHandler {

        @Override

        public void handle(HttpExchange exchange) throws IOException {

            String response = "¡Hola, mundo!";

            exchange.sendResponseHeaders(200, response.length());

            OutputStream os = exchange.getResponseBody();

            os.write(response.getBytes());

            os.close();

        }

    }

}


La API de Servidor Web Simple en Java es una herramienta útil para desarrollar servidores HTTP de forma rápida y sencilla. Aunque tiene limitaciones en cuanto a escalabilidad y características avanzadas, es perfecta para pruebas y aplicaciones pequeñas sin necesidad de frameworks externos.


jueves, 27 de febrero de 2025

Concurrencia en Erlang parte 14


Ahora que sabemos qué tiene que hacer cada componente y cómo comunicarse, una buena idea sería hacer una lista de todos los mensajes que se enviarán y especificar cómo se verán. Comencemos primero con la comunicación entre el cliente y el servidor de eventos.

El cliente puede enviar {subscribe, Self} al servidor de eventos, que solo puede responder con "ok". Tenga en cuenta que tanto el cliente como el servidor se monitorean entre sí. Aquí elegí usar dos monitores porque no hay una dependencia obvia entre el cliente y el servidor. Es decir, por supuesto, el cliente no funciona sin el servidor, pero el servidor puede vivir sin un cliente. Un enlace podría haber hecho el trabajo aquí, pero como queremos que nuestro sistema sea extensible con muchos clientes, no podemos asumir que todos los demás clientes querrán bloquearse cuando el servidor muera. Y tampoco podemos asumir que el cliente realmente se puede convertir en un proceso del sistema y que se produzcan salidas de trampa en caso de que el servidor muera. Ahora, el siguiente conjunto de mensajes.

El cliente puede enviar el mensaje {add, Name, Description, TimeOut}, al que el servidor puede responder 'ok' o {error, Reason}

Esto agrega un evento al servidor de eventos. Se envía una confirmación en forma de átomo ok, a menos que algo salga mal (tal vez el TimeOut esté en el formato incorrecto). La operación inversa, eliminar eventos, se puede realizar de la siguiente manera.

El cliente puede enviar el mensaje {cancel, Name} y el servidor de eventos debería devolver ok como un átomo

El servidor de eventos puede enviar una notificación cuando se deba realizar el evento.

El servidor de eventos reenvía un mensaje {done, Name, Description} al cliente

Luego, solo necesitamos los dos casos especiales siguientes para cuando queremos apagar el servidor o cuando falla.

Cuando el cliente envía el átomo 'shutdown' al servidor de eventos, este se apaga y devuelve {'DOWN', Ref, process, Pid, ​​shutoff} porque fue monitoreado

No se envía una confirmación directa cuando el servidor se apaga porque el monitor ya nos advertirá de eso. Eso es prácticamente todo lo que sucederá entre el cliente y el servidor de eventos. Ahora, los mensajes entre el servidor de eventos y los procesos de eventos en sí.

Una cosa a tener en cuenta aquí antes de empezar es que sería muy útil tener el servidor de eventos vinculado a los eventos. La razón de esto es que queremos que todos los eventos mueran si el servidor lo hace: no tienen sentido sin él.

Bien, volvamos a los eventos. Cuando el servidor de eventos los inicia, le da a cada uno de ellos un identificador especial (el nombre del evento). Una vez que llega el momento de uno de estos eventos, necesita enviar un mensaje que lo indique:

Un evento puede enviar {done, Id} al servidor de eventos

Por otro lado, el evento tiene que estar atento a las llamadas de cancelación del servidor de eventos.

El servidor envía 'cancel' a un evento, que responde con 'ok'

Y eso debería ser todo. Se necesitará un último mensaje para nuestro protocolo, el que nos permite actualizar el servidor.

el servidor de eventos tiene que aceptar un mensaje 'code_change' del shell

No es necesaria ninguna respuesta. Veremos por qué cuando realmente programemos esa función y verás que tiene sentido.

Una vez definido el protocolo y con la idea general de cómo se verá nuestra jerarquía de procesos, podemos empezar a trabajar en el proyecto.

miércoles, 26 de febrero de 2025

API de Acceso a Memoria Externa en Java


La API de Acceso a Memoria Externa (Foreign Function & Memory API) en Java es una funcionalidad introducida para interactuar de manera segura y eficiente con memoria fuera del heap de Java. Esta API ha evolucionado en versiones recientes y permite mejorar la interoperabilidad con código nativo sin necesidad de JNI (Java Native Interface).

Esta API proporciona una forma segura y estructurada de acceder a la memoria fuera del control del recolector de basura, lo que permite mejorar el rendimiento en aplicaciones que requieren interacción con bibliotecas nativas o manipulación intensiva de datos.

Veamos un ejemplo básico de cómo asignar y manipular memoria externa en Java utilizando la API:


import java.lang.foreign.*;

import java.lang.invoke.VarHandle;


public class ForeignMemoryExample {

    public static void main(String[] args) {

        try (MemorySegment segment = MemorySegment.allocateNative(100)) {

            VarHandle intHandle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder());

            intHandle.set(segment, 0, 42);

            System.out.println("Valor almacenado: " + intHandle.get(segment, 0));

        }

    }

}


Casos de uso:

  •  Interoperabilidad con bibliotecas en C: Facilita la comunicación con código nativo sin JNI.
  •  Manipulación de grandes volúmenes de datos: Ideal para manejar grandes bloques de memoria sin afectar el GC.
  •  Optimización en aplicaciones de alto rendimiento: Como motores de bases de datos y procesamiento de señales.


La API de Acceso a Memoria Externa en Java representa un avance importante en la forma en que interactuamos con memoria nativa. Al ofrecer una interfaz más segura y eficiente, facilita el desarrollo de aplicaciones de alto rendimiento sin comprometer la seguridad y estabilidad del código 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.

viernes, 21 de febrero de 2025

Programación Orientada a Objetos en Python


Nos encontramos hoy en día en un mundo en donde debemos realizar software que cumpla al menos con las siguientes características, las cuales al menos hacen que ésta tarea sea bastante compleja:

    • El Dominio del Problema: muchas veces las herramientas y tecnologías a utilizar dependen del problema que queremos resolver, ésto quiere decir que en base al dominio del mismo se definen requisitos, características y modelos que nosotros como desarrolladores debemos plasmar; los mismos hacen que sea difícil diseñar soluciones adaptables a varios escenarios, ya que dichos requisitos son muchas veces contradictorios.

    • El Proceso de Desarrollo: si nos encontramos desarrollando software de mediano o gran porte, ésto hace que necesitemos trabajar en equipo, en conjunto con uno o más desarrolladores. Si bien ésto suena simple, trabajar en equipo requiere coordinar y definir metodologías de trabajo, nomenclaturas de archivos, estilos, efectos visuales, entre otras cosas; además muchas veces trae aparejada la tarea de desarrollar de forma que tal que una modificación realizada por una persona no vuelva atrás o cambie una funcionalidad hecha por otro desarrollador.

    • La Flexibilidad del software: muy raras veces un prototipo o primera versión de un sistema que desarrollados será lo que finalmente nuestro cliente use, generalmente el mismo querrá cambios o agregar/eliminar funcionalidades; puesto que el software constantemente cambia. En algunos proyectos de desarrollo algunas veces lamentablemente se debe cambiar de lenguaje o de tecnología, porque lo diseñado no puede ser escalable a las necesidades del cliente; ésto es visto y considerado como una gran pérdida de tiempo.

    • La Reutilización del código: al desarrollar sistemas similares o con características iguales, en el mundo de la Ingeniería del Software se trata de aplicar el principio de “No reinventar la rueda”, ésto quiere decir que nuestro código debe poder ser reusable en varios escenarios y en la medida de lo posible en distintos sistemas.

Principalmente por éstos 4 puntos decimos que el software es complejo y que su ciclo de desarrollo también lo es; es por ésto que se hace necesario contar con alguna herramienta o tecnología que nos permita sortear éstos problemas.

Definimos a la POO como a un paradigma de programación, es decir a una forma de analizar, diseñar y realizar soluciones en el mundo del software. Existen distintos paradigmas de programación (funcional, lógico, procedural, entre otros) los cuales tienen como objetivo el dar un marco de trabajo para el diseño y desarrollo de soluciones de software. En el caso de la POO vamos a pensar, diseñar e implementar nuestro sistema como un conjunto de Objetos que se comunican entre sí mediante mensajes para llevar a cabo un objetivo en común.

Objetivos que platea este Paradigma

    • Escribir software fácilmente modificable y escalable.

    • Escribir software reusable.

    • Disponer de un modelo natural para representar un dominio.


Ventajas que ofrece

    • Fomenta la reutilización del código.

    • Permite desarrollar software más flexible al cambio y escalable.

    • Nos ofrece una forma de pensar más cercana a la realidad de las personas.


Para desarrollar aplicaciones orientadas a objetos necesitamos poseer la cualidad de la abstracción. Esto consiste en aislar un elemento de su contexto y tomar de él sólo las características que importan del mismo y las acciones que éste puede realizar. 

Un mismo objeto (una Persona) puede ser observado desde diferentes puntos de vista dependiendo de su contexto y de lo que se desee modelar. Si estamos desarrollando un sistema de gestión de personal para una empresa, importaran sus datos personales (sexo, fecha de nacimiento, DNI); mientras que si programamos una aplicación para resultados de encuestas importarán más otros datos (opinión, postura a favor de ciertos aspectos, etc). 

El objetivo de la Abstracción es identificar objetos en el dominio del problema que buscamos resolver y definir claramente que es lo que cada uno puede hacer (comportamiento) y qué información nos interesa almacenar de cada uno (atributos).

En el mundo de la POO nos encontraremos con dos herramientas, las cuales son esenciales para el funcionamiento del paradigma, las cuales son:


Clases 

    • Son el molde o modelo sobre el cual se crean los objetos.

    • Definen atributos y comportamientos comunes a todos los objetos de la misma clase.

    • Son considerados como un plano o molde sobre el cual creamos los objetos.

    • Son definiciones estáticas.


Objetos

    • Están modelados en función de una clase.

    • Son entidades dinámicas, es decir, son los que se ejecutan durante el ciclo de vida de un programa.

    • Son instancias de una clase dada.


Denominaremos de ésta forma a las entidades que se ejecutan en nuestro sistema para poder llevar a cabo un objetivo o funcionalidad, colaborando entre sí. Todo objeto es capaz de almacenar información en sus atributos y de realizar operaciones a través de sus métodos. 

Cada entidad almacenará información dentro de su estructura, lo que le permitirá brindar o modificar esa información y además podrá interactuar con sí mismo o con otros objetos mediante operaciones o métodos, generalmente son éstos métodos o procedimientos los que permiten modificar u obtener información almacenada en una entidad u objeto. Podemos decir que la información que un objeto posee determina su estado, mientras que los métodos que el mismo contiene determinan su comportamiento. 

Definimos como estado, a el valor que contienen todos los atributos de un objeto en un momento dado. Una persona puede tener 33 años, tener por nombre Juan y por Apellido González y poseer una altura de 1.78, en éste caso estamos haciendo referencia a atributos deseables para un objeto de tipo Persona. 

Es recomendable no permitir modificar la información de un objeto (almacenada en sus atributos) con total libertad, sino que publicar esa información a través de su comportamiento o métodos, ésto se conoce como encapsulamiento.  A la hora de clasificar los atributos o propiedades de un objeto, podemos decir que el mismo tiene propiedades inherentes a cada objeto (propiedades dinámicas) y propiedades comunes a todos los objetos del mismo tipo (propiedades estáticas), exploraremos éstos conceptos más adelante en este capítulo.

Además de almacenar información, todo objeto puede realizar distintas acciones, comunicándose o no con otros objetos. Definimos ésta característica como el comportamiento de un objeto, el cual va a estar determinado por los distintos métodos que el objeto que posea y utilice. Es deseable en la mayoría de los casos que nos comuniquemos con un objeto a través de sus métodos y que, en todo caso, sean ellos los que modifican o publiquen la información del mismo.

Cada objeto es una instancia de una clase dada, por lo tanto debe ser identificable en todo momento y diferenciarse del resto. No debemos confundir el estado de un objeto con su identidad, ya que el estado puede ir variando en la medida en que cambia el valor de los atributos de un objeto, pero su identidad permanece constante durante todo su ciclo de vida. En muchos lenguajes de programación la identidad está definida por una variable que almacena la dirección de memoria de un objeto (similar al concepto de puntero en otros lenguajes), a través de la cual podemos acceder al comportamiento y a los métodos del mismo. 

En una aplicación OOP nos encontraremos con muchas instancias de objeto en la memoria, las cuales no actúan por sí solas, sino que colaboran en conjunto para llevar a cabo la ejecución del programa. Para realizar esta tarea dichos objetos se comunican a través de mensajes, es decir existirán objetos que emiten y objetos que reciben mensajes, por ejemplo un objeto Persona podría crear un objeto Venta y luego podría comunicarse con él mismo y solicitarle que realice una transacción, esta comunicación se da mediante el envío de un mensaje desde el primer objeto al segundo. Generalmente el objeto receptor del mensaje ejecuta un método como respuesta al llamado del mensaje.

Así como definimos a los objetos como entidades dinámicas en la ejecución de una aplicación POO, definiremos a las clases como entidades estáticas que se encargan de la definición de los atributos y métodos comunes a todos los objetos que sean instancias de ellas. Es decir, una clase es como un molde que define todo lo común que luego será utilizado por todos los objetos que sean instancias de sí. 

Un error muy común en el mundo de la POO es confundir y utilizar los términos “Objeto” y “Clase” en forma similar, veamos algunas diferencias:

    • En primer lugar las clases sirven como herramienta para la definición de atributos y métodos, pero solamente realizan esa tarea. Los objetos en cambio son los que llevan a cabo la ejecución del programa, es decir, contienen valores dentro de sus atributos y ejecutan el comportamiento de sus métodos, todo ésto de acuerdo a la clase de la que son instancia.

    • También podemos definir como diferencia a la relación que existe entre los mismos, es decir, un objeto es instancia de una sola clase, pero de una clase pueden existir N instancias (objetos) distintos al mismo tiempo.

Por lo tanto, resumiendo, las clases son entidades de definición (estáticas) y los objetos entidades de ejecución (dinámicas). 

Llamamos entonces instanciación al proceso de creación de un nuevo objeto (instancia) a partir de su clase dada y clasificación al proceso de definir la clase de la cual es instancia un objeto.

Al existir en la memoria en un momento dado, podemos decir que el estado de un objeto está formado por el valor de todos los atributos del mismo en un momento dado. Es decir que, por ejemplo, una instancia de la clase Persona tendrá un nombre, apellido y edad en un momento dado del programa, una instancia de la clase Venta tendrá una fecha de venta y un estado, una instancia de la clase Usuario tendrá un nombre y contraseña. Con esto queremos decir que el valor de los atributos define el estado de un objeto, el cual va variando con la ejecución de la aplicación.

Me quedo super largo, voy a tener que hacer varios post...

lunes, 17 de febrero de 2025

Pattern matching para instanceof en Java


Pattern matching para instanceof es una característica introducida en Java 14 como una mejora para escribir código más conciso y seguro al realizar comprobaciones de tipo y conversiones.

Antes de Java 14, verificar si un objeto era de cierto tipo y luego convertirlo requería código repetitivo:


if (obj instanceof String) {

    String str = (String) obj;

    System.out.println("Longitud: " + str.length());

}


La coincidencia de patrones para `instanceof` elimina la necesidad del casting explícito, reduciendo la repetición:


if (obj instanceof String str) {

    System.out.println("Longitud: " + str.length());

}


La coincidencia de patrones se puede usar en estructuras más complejas como `else if`:


public void imprimir(Object obj) {

    if (obj instanceof String str) {

        System.out.println("Es un String: " + str);

    } else if (obj instanceof Integer num) {

        System.out.println("Es un Integer: " + num);

    } else {

        System.out.println("Tipo desconocido");

    }

}


La variable resultante solo está disponible dentro del bloque if, lo que evita posibles accesos indebidos:


if (obj instanceof String str) {

    System.out.println(str.length()); // Válido

}

System.out.println(str); // Error: No está definido fuera del if


La coincidencia de patrones para instanceof simplifica la escritura de código en Java, haciéndolo más expresivo y seguro. Esta funcionalidad es el primer paso hacia mejoras más avanzadas en la manipulación de tipos en el lenguaje.


Lombok @Builder: Simplificando la Creación de Objetos en Java


En el blog nunca he hablado de lombok, porque no me gusta que exista un framework que haga lo que tendria que tener el lenguaje que programamos. Pero esta funcionalidad de lombok no la conocia y me esta buena. 

La creación de objetos con múltiples atributos puede volverse tediosa y propensa a errores si se usa el enfoque tradicional de constructores o setters. Lombok ofrece la anotación @Builder, que es una solución elegante que permite generar automáticamente un patrón de construcción de objetos de manera fluida y legible.

Lombok es una librería que reduce el código repetitivo en Java mediante anotaciones que generan automáticamente métodos como getter, setter, equals, hashCode y toString. Una de sus anotaciones más útiles es @Builder, que facilita la creación de objetos sin necesidad de escribir constructores manualmente.

Para agregar Lombok, si usas Maven, agrega la siguiente dependencia:


<dependency>

    <groupId>org.projectlombok</groupId>

    <artifactId>lombok</artifactId>

    <version>1.18.26</version>

    <scope>provided</scope>

</dependency>


Si usas Gradle:


dependencies {

    compileOnly 'org.projectlombok:lombok:1.18.26'

    annotationProcessor 'org.projectlombok:lombok:1.18.26'

}


Veamos un ejemplo de una clase que tiene un builder: 


import lombok.Builder;

import lombok.Getter;

import lombok.ToString;


@Getter

@ToString

@Builder

public class Usuario {

    private String nombre;

    private String email;

    private int edad;

}

public class Main {

    public static void main(String[] args) {

        Usuario usuario = Usuario.builder()

                .nombre("Juan Pérez")

                .email("juan.perez@example.com")

                .edad(30)

                .build();

        

        System.out.println(usuario);

    }

}


Se puede personalizar el constructor y el nombre del método de construcción con @Builder, veamos un ejemplo :


@Builder(builderMethodName = "nuevoUsuario")

public class Usuario {

    private String nombre;

    private String email;

    private int edad;

}


Uso:


Usuario usuario = Usuario.nuevoUsuario()

        .nombre("Ana López")

        .email("ana.lopez@example.com")

        .edad(28)

        .build();


El uso de @Builder en Lombok simplifica la creación de objetos en Java, haciendo que el código sea más conciso, flexible y fácil de mantener. Esta anotación es especialmente útil cuando se manejan objetos con múltiples atributos opcionales, evitando la necesidad de escribir múltiples constructores sobrecargados.


Dejo link:

https://www.baeldung.com/lombok-builder

Concurrencia en Erlang parte 13


Soy una persona un poco desorganizada. Con suerte, todavía necesitas recordatorios de lo que tienes que hacer, porque vamos a escribir una de estas aplicaciones de recordatorio de eventos que te incitan a hacer cosas y te recuerdan las citas.

El primer paso es saber qué diablos estamos haciendo. "Una aplicación de recordatorios", dices. "Por supuesto", digo yo. Pero hay más. ¿Cómo planeamos interactuar con el software? ¿Qué queremos que haga por nosotros? ¿Cómo representamos el programa con procesos? ¿Cómo sabemos qué mensajes enviar?

Como dice la cita, "Caminar sobre el agua y desarrollar software a partir de una especificación son fáciles si ambos están congelados". Así que obtengamos una especificación y apeguémonos a ella. Nuestro pequeño software nos permitirá hacer lo siguiente:

  • Agregar un evento. Los eventos contienen una fecha límite (el momento en el que se debe advertir), un nombre de evento y una descripción.
  • Mostrar una advertencia cuando haya llegado el momento.
  • Cancelar un evento por nombre.

Sin almacenamiento en disco persistente. No es necesario para mostrar los conceptos arquitectónicos que veremos. Sería un fastidio para una aplicación real, pero en su lugar solo mostraré dónde se podría insertar si quisiera hacerlo y también señalaré algunas funciones útiles. Dado que no tenemos almacenamiento persistente, tenemos que poder actualizar el código mientras se está ejecutando.

La interacción con el software se realizará a través de la línea de comandos, pero debería ser posible ampliarla más adelante para que se puedan utilizar otros medios (por ejemplo, una GUI, acceso a una página web, software de mensajería instantánea, correo electrónico, etc.)

Esta es la estructura del programa que elegí para hacerlo:

Hay 5 componentes: Un cliente (1) que puede comunicarse con un servidor de eventos (2) y 3 pequeños círculos etiquetados como 'x', 'y' y 'z'. Los tres están vinculados al servidor de eventos.

Donde el cliente, el servidor de eventos y x, y y z son todos procesos. Esto es lo que cada uno de ellos puede hacer:

Servidor de eventos

  • Acepta suscripciones de clientes
  • Reenvía notificaciones de procesos de eventos a cada uno de los suscriptores
  • Acepta mensajes para agregar eventos (e iniciar los procesos x, y, z necesarios)
  • Puede aceptar mensajes para cancelar un evento y, posteriormente, matar los procesos de eventos
  • Puede ser finalizado por un cliente
  • Puede hacer que su código se vuelva a cargar a través del shell.

Cliente

Se suscribe al servidor de eventos y recibe notificaciones como mensajes. Por lo tanto, debería ser fácil diseñar un grupo de clientes que se suscriban al servidor de eventos. Cada uno de ellos podría ser potencialmente una puerta de entrada a los diferentes puntos de interacción mencionados anteriormente (GUI, página web, software de mensajería instantánea, correo electrónico, etc.)

  • Pide al servidor que agregue un evento con todos sus detalles
  • Pide al servidor que cancele un evento
  • Monitorea el servidor (para saber si se cae)
  • Apaga el servidor de eventos si es necesario

x, y y z:

  • Representan una notificación que espera ser activada (básicamente son solo temporizadores vinculados al servidor de eventos)
  • Envían un mensaje al servidor de eventos cuando se acaba el tiempo
  • Reciben un mensaje de cancelación y mueren

Tenga en cuenta que todos los clientes (mensajería instantánea, correo, etc. que no están implementados en este libro) reciben notificaciones sobre todos los eventos, y una cancelación no es algo sobre lo que advertir a los clientes. Aquí el software está escrito para usted y para mí, y se supone que solo un usuario lo ejecutará.

Esto representa cada proceso que tendremos. Al dibujar todas las flechas allí y decir que son mensajes, hemos escrito un protocolo de alto nivel, o al menos su esqueleto.

Se debe tener en cuenta que usar un proceso por evento para recordar probablemente sea excesivo y difícil de escalar en una aplicación del mundo real. Sin embargo, para una aplicación de la que será el único usuario, esto es suficiente. Un enfoque diferente podría ser usar funciones como timer:send_after/2-3 para evitar generar demasiados procesos.

sábado, 15 de febrero de 2025

Record en Java


Con la introducción de los records en Java 14 como una feature en vista previa y su estandarización en Java 16, ahora es más fácil crear clases inmutables de manera concisa y sin la sobrecarga de escribir código repetitivo.

Un record es una clase especial diseñada para almacenar datos de manera inmutable. Se encarga automáticamente de generar los métodos equals(), hashCode(), toString() y los accesores (`getters`) sin necesidad de escribir código adicional.

Veamos un ejemplo: 


public record Usuario(String nombre, String email, int edad) {}


La declaración anterior equivale a escribir lo siguiente en una clase tradicional:


public final class Usuario {

    private final String nombre;

    private final String email;

    private final int edad;

    

    public Usuario(String nombre, String email, int edad) {

        this.nombre = nombre;

        this.email = email;

        this.edad = edad;

    }

    

    public String nombre() { return nombre; }

    public String email() { return email; }

    public int edad() { return edad; }

    

    @Override

    public boolean equals(Object o) { /* Implementación automática */ }

    

    @Override

    public int hashCode() { /* Implementación automática */ }

    

    @Override

    public String toString() { /* Implementación automática */ }

}


Veamos como usar un record: 


public class Main {

    public static void main(String[] args) {

        Usuario usuario = new Usuario("Juan Pérez", "juan.perez@example.com", 30);       

        System.out.println(usuario);

    }

}


Salida:

Usuario[nombre=Juan Pérez, email=juan.perez@example.com, edad=30]


Se puede agregar métodos adicionales si es necesario:


public record Usuario(String nombre, String email, int edad) {

    public boolean esMayorDeEdad() {

        return edad >= 18;

    }

}


Los record en Java son una herramienta poderosa para definir estructuras de datos inmutables de forma concisa y eficiente. Gracias a ellos, el código es más limpio, menos propenso a errores y más fácil de mantener.


jueves, 13 de febrero de 2025

AssertJ


En el mundo de las pruebas unitarias en Java, AssertJ se ha consolidado como una de las herramientas más poderosas y expresivas para realizar aserciones. Este framework proporciona una API fluida y altamente legible que facilita la validación de resultados en las pruebas, mejorando la mantenibilidad y claridad del código.

AssertJ es una librería de aserciones para Java con una API expresiva y encadenable. Entre sus principales características se encuentran:

  • Sintaxis fluida y encadenada.
  • Mejor manejo de colecciones y excepciones.
  • Comparaciones avanzadas con objetos y fechas.
  • Integración con JUnit y TestNG.

Para utilizar AssertJ en un proyecto Maven, agrega la siguiente dependencia:


<dependency>

    <groupId>org.assertj</groupId>

    <artifactId>assertj-core</artifactId>

    <version>3.24.2</version>

    <scope>test</scope>

</dependency>



Si usas Gradle:

testImplementation 'org.assertj:assertj-core:3.24.2'


Veamos  un ejemplo de cómo realizar aserciones básicas con AssertJ:



import static org.assertj.core.api.Assertions.*;

import org.junit.jupiter.api.Test;


class AssertJExampleTest {

    @Test

    void testBasicAssertions() {

        String mensaje = "Hola AssertJ";

        assertThat(mensaje)

            .isNotNull()

            .startsWith("Hola")

            .endsWith("AssertJ")

            .contains("AssertJ");

    }

}


Otro ejemplo: 

@Test

void testNumbers() {

    int resultado = 10;

    assertThat(resultado)

        .isPositive()

        .isGreaterThan(5)

        .isLessThanOrEqualTo(10);

}


@Test

void testListAssertions() {

    List<String> nombres = List.of("Juan", "Maria", "Carlos");

    assertThat(nombres)

        .hasSize(3)

        .contains("Maria")

        .doesNotContain("Pedro")

        .containsExactly("Juan", "Maria", "Carlos");

}


Veamos Aserciones con Excepciones:


@Test

void testException() {

    assertThatThrownBy(() -> {

        throw new IllegalArgumentException("Error de prueba");

    }).isInstanceOf(IllegalArgumentException.class)

      .hasMessageContaining("Error");

}


AssertJ es una excelente opción para mejorar las pruebas unitarias en Java, ofreciendo una API intuitiva y potente que facilita la validación de resultados. Su uso ayuda a escribir pruebas más claras y mantenibles, mejorando la calidad del código.


Dejo link: https://www.baeldung.com/introduction-to-assertj