5 de marzo de 2019

Lombok, escribiendo menos código y logrando más cosas

En este tutorial no veremos un Framework, sino una herramienta Java que nos ayudará a simplificar mucho el código que escribimos en cada una de nuestras clases de datos; o sea, las clases que no contienen lógica de negocio, sino que nos ayudan a "transportar" información, como son los POJOs o los Value Objects.

Lombok es una librería Java que automáticamente se conecta a nuestro editor o herramienta de construcción (como pueden ser Maven o Eclipse) y que nos ayuda a generar código para las tareas más repetitivas de nuestras clases como son la generación de métodos setter y getter, constructores, toString, equals, etc. ¿Recuerdan en los tutoriales de Hibernate como la parte más tardada era generar las entidades por la cantidad de métodos getter y setter que estas tenían? Esas clases ocupan mucho espacio, en término de líneas de código; todo ese código podremos ahorrárnoslo con Lombok, ya que por medio de anotaciones le indicaremos qué es lo que queremos que genere.

En este tutorial aprenderemos cómo instalar Lombok en nuestro IDE y veremos algunos ejemplos de uso.

Lo primero que veremos es cómo instalar Lombok en nuestro IDE (en este caso Eclipse).

El primer paso es descargar el jar del instalador de Lombok desde esta URL en su sitio oficial. La versión más reciente de Lombok al momento de escribir este tutorial es la 1.18.6.

Simplemente hacemos clic en el botón de descarga de la página anterior y listo, tendremos un jar de poco menos de 2Mb de tamaño.



Este jar podemos usarlo directamente agregándolo al classpath, o podemos integrarlo al IDE a través de un asistente que se abrirá al hacer doble clic sobre el jar. Es importante agregar Lombok a nuestro IDE por las características de autocorrección que estos tienen, si el IDE no sabe que estamos usando Lombok y tratamos de completar o escribimos una llamada a un getter que aún no existe (porque Lombok trabaja en tiempo de compilación para generar estos métodos) obtendremos un error, en algunos casos incluso no nos dejará compilar el proyecto.

Para evitarnos problemas a futuro, haremos doble clic sobre el jar que acabamos de descargar.



Con esto comenzará a buscar las instalaciones que tenemos de Eclipse para configurarse en forma automática. En mi caso tengo dos instalaciones, una que ya tiene Lombok y otra que no, por lo que lo agregaré a este último. Si tu instalación de Eclipse no aparece puede ser que esté instalado en una ubicación diferente al default, en eso caso no te preocupes, puedes seleccionar la opción de "Sepecify Location..." y seleccionar la instalación a la que quieres agregarlo.



Hacemos clic sobre el botón "Install/Update" y con esto se realizará la instalación, que es prácticamente instantánea. Al terminar podemos presionar el botón "Quit Installer" y listo, ya tendremos instalado Lombok en nuestro IDE. Para comprobar que la instalación se realizó de forma correcta, debemos ir a ver la información de la versión de Eclipse ("Help -> About Eclipse IDE") y después de la información del copy right debemos ver la versión de Lombok instalada.



¿Sólo podemos instalar Lombok en Eclipse? No, se puede instalar en prácticamente cualquier IDE que soporte Java. El procedimiento para todos los IDEs basados en Eclipse, es el mismo, estas son las instrucciones para instalarlo en NetBeans, Visual Studio Code e IntelliJ.

En este tutorial veremos sólo algunas de las funcionalidades más útiles de "Lombok".

Comencemos creando un nuevo proyecto Gradle ("File -> New -> Gradle -> Gradle Project"), daremos un nombre al proyecto (en mi caso será "LombokTutorial"), damos una ubicación y presionamos el botón "Finish", y después de unos minutos veremos nuestro nuevo proyecto en el panel de proyectos.

El siguiente paso es agregar la dependencia de Lombok al archivo "build.gradle". Al abrir este archivo veremos que Gradle ya ha agregado algunas dependencias y un repositorio. En lo personal no me gustan los elementos por default que maneja Gradle, por lo que cambiaremos el contenido del archivo por el siguiente, en el que declaramos que usaremos el repositorio central de Maven y usaremos la librería de Lombok para compilar y como pre-procesador de anotaciones, de la siguiente forma:

dependencies {
    compileOnly 'org.projectlombok:lombok:1.18.6'
	annotationProcessor 'org.projectlombok:lombok:1.18.6'
}

repositories {
    mavenCentral()
}
Si usan Maven tan solo deben declarar la siguiente dependencia:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.6</version>
	<scope>provided</scope>
</dependency>
*Nota: Para que Eclipse vea los cambios en el archivo de Gradle, es necesario refrescar su configuración. Para esto hacemos clic derecho sobre el proyecto y en el menú que se abre seleccionamos la opción "Gradle -> Refresh Gradle Project". Es importante saber que la opción de Gradle sólo aparecerá si hacemos clic sobre la raíz del proyecto o sobre alguno de los archivos de configuración de Gradle.



Ahora crearemos un nuevo paquete, que en mi caso será "com.javatutoriales.lombok" y adentro de esta clase crearemos otra llamada "Main". Esta clase nos ayudará a probar que efectivamente Lombok está funcionando como lo esperamos. Esta clase tendrá por el momento un método "main" vacío, de la siguiente forma:
package com.javatutoriales.lombok;

public class Main {

	public static void main(String[] args) {

	}
	
}

Comenzaremos con algo fácil. ¿Cuántas veces no hemos tenido problemas con nuestra aplicación en ejecución porque a alguien se le ocurrió mandar un parámetro nulo y nosotros no estábamos preparados para recibirlo o no sabemos cuál objeto es exactamente el que es null? Pues bien, con Lombok esto se terminó. Lombok proporciona la anotación "@NonNull" que verifica que el argumento que hayamos marcado con esta anotación no tenga un valor nulo, de lo contrario arroja una "NullPointerExeption" indicando cuál parámetro es el que tiene el valor nulo.

Para probar esta anotación crearemos una nueva clase llamada "EjemploNonNull" en el paquete "com.javatutoriales.lombok" y adentro de esta clase un método llamado "saluda" que reciba un parámetro de tipo String llamado "nombre". Lo que haremos dentro de este método es regresar un saludo a la persona cuyo nombre se haya recibido como parámetro, y para elevar el nivel de complejidad convertiremos esta cadena en mayúsculas al momento de regresarlo:

public String saluda(String nombre) {
    return "Hola " + nombre.toUpperCase();
}
Eso traerá un problema potencial, ya que, si "nombre" es nulo, nuestra aplicación se romperá. Vamos a comprobar esto. En el método "main" de la clase que creamos hace un momento, colocaremos el siguiente código:

EjemploNonNull nonNull = new EjemploNonNull();
System.out.format("%s\n\n", nonNull.saluda(null));
El problema está justamente en la segunda línea, ya que como el nombre que estamos pasando es un valor nulo, nuestra aplicación arrojará la siguiente excepción:

Exception in thread "main" java.lang.NullPointerException
	at com.javatutoriales.lombok.EjemploNonNull.saluda(EjemploNonNull.java:7)
No es nada que no hayamos visto antes, pero tampoco nos dice mucho. Este es un ejemplo simple, pero ¿qué pasaría si tuviéramos varias llamadas encadenadas a varios métodos con varios parámetros, cómo sabríamos cuál es el valor nulo?

Bien, con Lombok esto es fácil, sólo agregamos la anotación "@NonNull" al parámetro "nombre", de esta forma:

public String saluda(@NonNull String nombre)
Y eso es todo. Si volvemos a ejecutar nuestra aplicación veremos la siguiente salida:

Exception in thread "main" java.lang.NullPointerException: nombre is marked @NonNull but is null
	at com.javatutoriales.lombok.EjemploNonNull.saluda(EjemploNonNull.java:6)
Con esto ahora podemos saber qué "nombre" es quien es nulo y por lo tanto podremos corregir el error.

Algo importante de "@NonNull" es que debe agregarse en cada parámetro que queramos validar, ya sea de constructores o métodos, no existe una forma de decir que se validen todos los parámetros de un método.

Ahora veremos la segunda característica útil de "Lombok". Al inicio del tutorial hablábamos del problema de escribir los setter y getters a mano dentro de una clase Java; para ahorrarnos todo este trabajo Lombok tiene un par de anotaciones que nos serán muy útiles. Crearemos una clase llamada "GettersSetters" que nos ayudará a probar esa funcionalidad.

A esta clase agregaremos tres propiedades de forma inicial, una cadena, un entero primitivo y una fecha:

package com.javatutoriales.lombok;

import java.util.Date;

public class GettersSetters {
	private String cadena;
	private int entero;
	private Date fecha;
	private boolean boleano;
}
El outline de Eclipse nos ayudará a ver qué es lo que pasa con nuestra clase conforme agregamos estos métodos. En este momento debe verse así, lo que indica que nuestra clase solo tiene los campos marcados como privados:



Ahora agregaremos nuestra primera anotación de "Lombok": @Getter. Esta anotación podemos agregarla a nivel de clase, con lo que todos los atributos no estáticos de la clase recibirán un getter, de esta forma:

@Getter
public class GettersSetters {
	private String cadena;
	private int entero;
	private Date fecha;
	private boolean boleano;
}
Con esto, en el panel de outline debemos ver que se han agregado los getters de estos atributos; también, podemos ver que los métodos tienen una visibilidad pública.



Con esta simple anotación nos hemos ahorrado alrededor de 12 líneas de código, esto hace nuestra clase más compacta además de ahorrarnos tiempo al escribirla. Lo mejor es que si agregamos un nuevo atributo se agrega también en automático su getter.

¿Qué ocurre si no queremos que los métodos getter generados tengan una visibilidad pública? No hay ningún problema, Lombok lo tiene considerado, dentro de la anotación @Getter se tiene un atributo "value", que recibe como valor un tipo "AccessLevel" con los siguientes valores posibles:
  • MODULE
  • NONE
  • PACKAGE
  • PRIVATE
  • PROTECTED
  • PUBLIC
Su valor por default es "PUBLIC", esto quiere decir que los métodos generados son públicos, si queremos cambiar este comportamiento y hacer que, por ejemplo, los getters generados tengan un nivel de visibilidad privado, lo único que tenemos que hacer es pasar esto como valor de nuestra anotación de esta forma:

@Getter(AccessLevel.PRIVATE)
Y listo, con esto todos nuestros métodos generados tendrán un nivel de visibilidad privado

¿Qué pasa si solo queremos cambiar el nivel de visibilidad de un método? Es muy sencillo, ya que la anotación @Getter también se puede poner a nivel de un método, y ahí podemos realizar estas adecuaciones:

@Getter
public class GettersSetters {
	private String cadena;
	private int entero;
	@Getter(AccessLevel.PRIVATE)
	private Date fecha;
	private boolean boleano;
}
Con esto, sólo el getter de "fecha" ha quedado con un tipo de acceso privado. ¿Qué pasa si, por el contrario, hay un atributo del que no queremos que se genere un getter? Muy simple, sólo usamos el valor AccessLevel.NONE en la anotación y listo, no se generará su getter:

@Getter
public class GettersSetters {
	private String cadena;
	private int entero;
	@Getter(AccessLevel.NONE)
	private Date fecha;
	private boolean boleano;
}
¿Qué más podemos hacer con esta anotación? Bueno, un par de cosas, la más importante a mi parecer es el poder indicar que el getter generado debe tener una anotación, la cual podemos configurar. Esto es muy útil cuando usamos algún framework que requiere que coloquemos anotaciones a nivel de getter, como puede ser Hibernate o JPA. En este caso debemos usar el atributo "onMethod", el cual recibe las anotaciones que se colocarán en el getter, de la siguiente forma:

@Getter(onMethod=@__({@OneToOne}))
private Date fecha;
Con esto, todos los getters generados por Lombok contendrán la anotación @OneToOne que usamos para mapear relaciones en Hibernate.

Ya vimos cómo funciona @Getter, ¿qué pasa con @Setter? En realidad, funciona exactamente de la misma forma, se puede colocar a nivel de método o de atributo. Si lo ponemos en la clase creará todos los setters de los atributos no estáticos de la clase; si lo ponemos en un atributo particular, sólo creará el setter de ese atributo.

Coloquémoslo para nuestro ejemplo a nivel de clase, de la siguiente forma:

@Getter
@Setter
public class GettersSetters {
...
}
Con lo cual, se generarán los setters de todos los atributos (junto con los getters que ya se tenían):



Las mismas reglas que aplican para @Getter también aplican para @Setter, incluso el atributo "onMethod". Prácticamente la única diferencia entre estas dos anotaciones es que @Setter permite agregar una anotación al parámetro que esta recibiendo (esto adicional a poder agregar una anotación al método completo), para esto proporciona un atributo "onParam", de la siguiente forma:



Con esto, el parámetro "fecha" del método "setFecha(Data fecha)" será decorado con la anotación "@NotNull".

Ambas anotaciones pueden convivir sin problema. Nuestra clase "GettersSetters" se ve así:

@Getter
@Setter
public class GettersSetters {
	private String cadena;
	private int entero;
	@Getter(AccessLevel.NONE)
	@Setter(onParam=@__({@NonNull}))
	private Date fecha;
	private boolean boleano;
}
Una cosa que hay que saber de "@Getter" y "@Setter" es que estos sólo funcionan con atributos de entidad; estos son los atributos no estáticos de una clase. En el caso de "@Setter", este tampoco generará los métodos correspondientes para atributos final, ya que estos no pueden modificar su valor una vez establecidos.

Comprobemos que todo funciona correctamente, paro eso regresaremos a nuestra clase "Main" y dentro del método principal crearemos una instancia de "GettersSetters" y llamaremos alguno de los métodos que se generaron de forma automática con las anotaciones de "Lombok", en mi caso el método queda de la siguiente forma:

GettersSetters getSet = new GettersSetters();
getSet.setCadena("ABC123");
String valor = getSet.getCadena();

System.out.format("El valor del atributo cadena es: %s", valor);
Con esto podemos ejecutar nuestra aplicación como una aplicación Java (Alt + Shift + X, J) y si todo está bien, debemos ver la siguiente salida en la consola:



Con esto podemos comprobar que Lombok ha generado los métodos de forma correcta.

Ahora, esto no es lo único que se puede hacer con "LombokLombok
ofrece tres alternativas:
  • Generar un constructor sin argumentos.
  • Generar un constructor que recibe todos los parámetros obligatorios.
  • Generar un constructor que recibe todos los argumentos.
Para probar el funcionamiento de Lombok con constructores, crearemos una nueva clase llamada "Constructores" dentro del paquete "com.javatutoriales.lombok". Colocaremos algunos atributos en esta clase, noten que algunos de estos atributos son estáticos y algunos otros están marcados como final.

public class Constructores {
	private String cadena;
	private int entero;
	private static String cadenaEstatica;
	private final String cadenaFinal;
	private final int enteroFinal;
}
Para la primera opción usamos la anotación "@NoArgsConstructor", que generará el constructor sin parámetros. Este constructor se generará únicamente si no se tienen atributos de instancia marcados como "final", ya que estos deben ser inicializados a más tardar en el constructor para que funcione de manera correcta; también podemos optar por que estos campos se inicialicen de forma automática con valores por default (0 / false / null) colocando el valor de "true" en el atributo "force" ("@NoArgsConstructor(force = true)")

Agregaremos esta primera anotación en la declaración de la clase:

@NoArgsConstructor
public class Constructores {
...
}
Con esto veremos que en el panel de Outline de Eclipse ha aparecido un nuevo constructor, el cual no recibe ningún argumento.



Para probar el segundo caso, usaremos la anotación "@RequiredArgsConstructor" la cual generará un constructor con cada uno de los atributos final no inicializados, así como todos los atributos marcados como "@NotNull". Colocaremos la anotación de esta forma:

@NoArgsConstructor
@RequiredArgsConstructor
public class Constructores {
...
}
Con lo que veremos que se ha agregado un segundo constructor que recibe un String y un int, los cuales corresponden con los atributos final "cadenaFinal" y "enteroFinal":



Finalmente, "@AllArgsConstructor" genera un constructor con todos los parámetros no estáticos de la clase. Colocamos esta anotación así:

@NoArgsConstructor
@RequiredArgsConstructor
@AllArgsConstructor
public class Constructores {
...
}
Con esto, veremos que se agrega un nuevo constructor con cuatro parámetros, los cuales corresponden a todos los atributos no estáticos de la clase.



Para comprobar el correcto funcionamiento de esta clase haremos dos cosas, primero agregaremos la anotación "@Getter" al inicio de la clase, esto nos ayudará a leer los valores de los atributos que serán establecidos en el constructor, de esta forma:

@AllArgsConstructor
@Getter
public class Constructores {
...
}
Ahora regresaremos a nuestra clase "Main" y colocaremos el siguiente código en donde probaremos que los valores pasados a los constructores generados con "@RequiredArgsConstructor" y "@AllArgsConstructor" funcionen correctamente (no podremos probar "@NoArgsConstructor", ya que al tener campos final este constructor nos dará errores. Nuestro código para la prueba se ve de la siguiente forma:

Constructores constructoresRequired = new Constructores("cadenaFinal", 10);
System.out.format("getCadenaFinal: %s\n", constructoresRequired.getCadenaFinal());
System.out.format("getEnteroFinal: %d\n\n", constructoresRequired.getEnteroFinal());

Constructores constructoresAllArgs = new Constructores("cadena", 1, "cadenaFinal", 2);
System.out.format("getCadena: %s\n", constructoresAllArgs.getCadena());
System.out.format("getEntero: %d\n", constructoresAllArgs.getEntero());
System.out.format("getCadenaFinal: %s\n", constructoresAllArgs.getCadenaFinal());
System.out.format("getEnteroFinal: %d\n", constructoresAllArgs.getEnteroFinal());
Ahora, ejecutamos nuestra aplicación como una aplicación Java (Alt + Shift + X, J), con lo que debemos ver la siguiente salida en la consola de Eclipse:

getCadenaFinal: cadenaFinal
getEnteroFinal: 10

getCadena: cadena
getEntero: 1
getCadenaFinal: cadenaFinal
getEnteroFinal: 2
Con esto podemos comprobar que los constructores generados funcionan correctamente.

Cada una de estas anotaciones soporta un par de atributos interesantes. El primero es "access", el cual nos permite definir el nivel de acceso del constructor. Por default, los constructores son generados con visibilidad pública, pero si queremos cambiar esto podemos usar alguno de los valores de la enumeración "AccessLevel" para modificarlo. Por ejemplo, si queremos que el constructor que recibe todos los parámetros (el generado por "@AllArgsConstructor") tenga un nivel de acceso protegido, lo modificamos de la siguiente forma:

@AllArgsConstructor(access=AccessLevel.PROTECTED)
Con esto veremos que ahora su nivel de visibilidad ha cambiado.



Otro de los atributos soportados es "staticName", este atributo hará que el constructor generado sea privado y se agregará un método estático de fábrica que nos permitirá obtener la instancia de la clase. Este método generado recibirá todos los parámetros que requiera el constructor para funcionar correctamente. "staticName" recibe como valor el nombre del método estático que se generará, los nombres más comunes para este tipo de métodos son "getInstance" y "of". Modificaremos la anotación "@RequiredArgsConstructor" para agregar este atributo de la siguiente forma:

@RequiredArgsConstructor(staticName="getInstance")
Esto quiere decir que en vez de crear una instancia de nuestra clase "Constructores" de esta forma:

new Constructores("cadenaFinal", 10);
Ahora lo haremos así:

Constructores.getInstance("cadenaFinal", 10);
Hagamos este cambio en el código de la clase "Main" y volvamos a ejecutar el ejemplo. Con esto la salida no debe haber cambiado, con lo cual comprobamos que el ejemplo funciona correctamente.

Ahora veremos otra de esas utilidades de Lombok que pueden ayudarnos a ahorrar mucho tiempo, sobre todo en clases que pueden estar cambiando de forma constante. La clase Object, de la que todas las clases en Java heredan directa o indirectamente, define un método "toString" que regresa la representación de un objeto en forma de cadena. Este método es llamado en automático por muchas clases del core de java y muchos frameworks, por eso es importante realizar una correcta sobreescritura en nuestras clases. Lombok proporciona la anotación "@ToString" que permite realizar la implementación en automático del método "toString". La implementación por default imprimirá el nombre de la clase, junto con cada campo en el orden en el que aparecen en la declaración de la clase y los separará por comas; es importante mencionar que sólo se mostrarán los atributos no estáticos de la clase.

Para probarlo, creamos una nueva clase llamada "EjemploToString" y le agregaremos algunos atributos para tener material para usar en el método "toString", de esta forma:

public class EjemploToString {
	private String cadena;
	private char caracter;
	private int entero;
	private Date fecha;
	private boolean boleano;
	private String[] arreglo;
}
Ahora, colocaremos la anotación "@ToString" al inicio de la clase para generar este método; adicionalmente, pondremos también la anotación "@AllArgsConstructor" para generar un constructor que nos permita inicializar los atributos del nuevo objeto. La declaración de la clase queda de la siguiente forma:

@AllArgsConstructor
@ToString
public class EjemploToString {
	private String cadena;
	private char caracter;
	private int entero;
	private Date fecha;
	private boolean boleano;
	private String[] arreglo;
}
Ahora, en el método "main" inicializaremos una instancia de esta clase de la siguiente forma:

EjemploToString ejemploToString = new EjemploToString("cadena", 'c', 1, null, true, new String[]{"uno", "dos", "tres"});
Con esto le hemos dado un valor a cada uno de los atributos (con excepción de la fecha), y esperamos que en el método "toString" sean estos los valores que se muestren. Para probarlo, agregaremos la siguiente línea de código, que de manera implícita manda llamar al método "toString()":

System.out.format("toString: %s", ejemploToString);
Ahora, ejecutamos nuestra aplicación y debemos ver la siguiente salida en la consola de Eclipse:

toString: EjemploToString(cadena=cadena, caracter=c, entero=1, fecha=null, boleano=true, arreglo=[uno, dos, tres])
Con lo que podemos comprobar que el ejemplo ha funcionado correctamente.

¿Qué pasa si queremos que no se muestre alguno de los atributos? En ese caso, debemos marcar dicho atributo con la anotación "@ToString.Exclude". Para probarlo, colocaremos esta anotación en el atributo "fecha" de la clase "EjemploToString":

@ToString.Exclude
private Date fecha;
Si ahora volvemos a ejecutar nuestra aplicación, debemos ver la siguiente salida:

toString: EjemploToString(cadena=cadena, caracter=c, entero=1, boleano=true, arreglo=[uno, dos, tres])
Lombok también da una forma de modificar el nombre que se muestra del atributo en el método "toString". Podemos colocar este nombre en el atributo "name" de la anotación "@ToString.Include" que podemos colocar en el campo cuyo nombre queremos modificar; por ejemplo, si queremos que al llamar a "toString" en vez de mostrarse "arreglo" como el nombre de este campo se muestre "cadenas", colocamos la anotación de esta forma:

@ToString.Include(name="cadenas")
private String[] arreglo;
Si volvemos a ejecutar nuestra aplicación, debemos ver la siguiente salida en la consola:

toString: EjemploToString(cadena=cadena, caracter=c, entero=1, boleano=true, cadenas=[uno, dos, tres])
También, esta anotación nos da la posibilidad de definir una prioridad, que en el argot de Lombok es llamado rango (rank), para indicar el orden en el que serán mostrados los atributos. Por default, todos los atributos tienen el rango de 0; los campos con un rango mayor serán mostrados primeros. Agreguemos un rango de 1 al arreglo de cadenas (para que se muestre primero su valor) y un rango de -1 al caracter (para que sea el último valor en mostrarse), de la siguiente forma:

public class EjemploToString {
	private String cadena;
	
	@ToString.Include(rank=-1)
	private char caracter;
	
	private int entero;
	
	@ToString.Exclude
	private Date fecha;
	
	private boolean boleano;
	
	@ToString.Include(name="cadenas", rank=1)
	private String[] arreglo;
}
Si ahora ejecutamos nuevamente nuestra aplicación debemos ver la siguiente salida:

toString: EjemploToString(cadenas=[uno, dos, tres], cadena=cadena, entero=1, boleano=true, caracter=c)
Con esto podemos comprobar que el ejemplo funciona de manera correcta.

Una de las recomendaciones del lenguaje Java es siempre sobreescribir los métodos "equals" y "hashCode". La utilidad de estos métodos es que nos permiten comparar dos objetos para saber si son iguales (o equivalentes), usando para esto los valores de sus atributos a través del criterio que nosotros definamos.

En el caso de "equals", la implementación por default define que dos objetos son iguales si estos están en el mismo segmento de memoria; es decir, si los objetos que estamos comparando son la misma instancia (o, dicho de otra forma, sólo serán iguales si comparamos a una instancia con ella misma sin importar los valores de sus atributos). En el caso de la implementación de "hashCode" pasa algo parecido y la implementación por default regresa la representación de la ubicación en memoria del objeto.

Esto es poco útil en aplicaciones de mediana o gran escala en la que nos interesa saber si dos objetos son iguales (o equivalente) por los valores que les hemos dado a sus atributos como podrían ser un identificador o un nombre. Algo que es especialmente útil saber es que muchas colecciones en Java (ArrayList o las implementaciones de Set) obtienen los objetos con base al resultado obtenido de llamar a estos métodos, y si no los sobreescribimos de forma apropiada podríamos causar un enorme problema en nuestra aplicación sin siquiera saberlo.

Comprobemos que lo anterior es verdad con un ejemplo. Crearemos una nueva clase llamada "EjemploEqualsHashcode" que tendrá los siguientes atributos:

public class EjemploEqualsHashcode {
    private long id;
    private String matricula;
    private String nombre;
}
Decoraremos esta clase con la anotación "@AllArgsConstructor" para tener una forma rápida de inicializar los valores de las instancias que crearemos:

@AllArgsConstructor
public class EjemploEqualsHashcode {
	private long id;
	private String matricula;
	private String nombre;
}
Regresemos a nuestro método "main" y agregamos las siguientes líneas, las cuales crean dos instancias de "EjemploEqualsHashcode" con exactamente los mismos valores:

EjemploEqualsHashcode instancia1 = new EjemploEqualsHashcode(2, "ABC123", "Alex");
EjemploEqualsHashcode instancia2 = new EjemploEqualsHashcode(2, "ABC123", "Alex");
Ahora, para comprobar que con la implementación por default del JDK estos dos objetos no son equivalentes, agregaremos la siguiente línea de código:

System.out.printf("Las instancias son iguales?: %b\n", instancia1.equals(instancia2));
Y para comprobar que estas instancias están en ubicaciones diferentes de memoria usaremos lo siguiente (esto no es completamente correcto, pero es una forma sencilla de entender lo que está mostrando el método "hashCode"):

System.out.printf("Hash de la instancia 1: %h\n", instancia1.hashCode());
System.out.printf("Hash de la instancia 2: %h\n", instancia2.hashCode()); 
Ahora ejecutemos nuestra aplicación, debemos ver una salida similar a la siguiente (en su caso la salida de los hashes será diferente):

Las instancias son iguales?: false
Hash de la instancia 1: 4aa298b7
Hash de la instancia 2: 7d4991ad
Ahora, un ejemplo más alarmante (^_^!). Agregaremos ambas instancias a un Set y mostraremos que ambos objetos han quedado dentro de dicho Set. Recordemos que, por definición, un Set no admite objetos repetidos:

Set<EjemploEqualsHashcode> set = new HashSet<>();
set.add(instancia1);
set.add(instancia2);

System.out.printf("Los objetos del set son: %d, %s\n", set.size(), set); 
Si ahora ejecutamos nuestra aplicación, debemos ver la siguiente salida:

Los objetos del set son: 2, [com.javatutoriales.lombok.EjemploEqualsHashcode@7d4991ad, com.javatutoriales.lombok.EjemploEqualsHashcode@4aa298b7]
Con esto comprobamos que ambas instancias son completamente diferentes para Java. ¿Qué podemos hacer si queremos que las considere iguales dentro de la lógica de nuestra aplicación (por ejemplo, para no tener dos objetos con los mismos valores dentro del hash)? Muy fácil, para esto hay que implementar los métodos "equals" y "hashCode" con nuestra propia lógica para decidir si efectivamente ambos objetos serán iguales o no.

Al hacer esta implementación hay que tener en cuenta que existe una especie de contrato entre estos dos métodos:

If two objects are equal according to the equals(Object) method, then calling the hashcode() method on each of the two objects must produce the same integer result.

Con Lombok lograr esto es muy fácil, sólo debemos agregar la anotación "@EqualsAndHashCode" al inicio de nuestra clase, de la siguiente forma:

@EqualsAndHashCode
public class EjemploEqualsHashcode {
...
}
Con esto se realizará una implementación automática de ambos métodos, cumpliendo el contrato anterior. Para la implementación se usarán todos los atributos no estáticos de la clase. Si ahora volemos a ejecutar nuestro ejemplo, veremos la siguiente salida:

Las instancias son iguales?: true
Hash de la instancia 1: 6dd5ca43
Hash de la instancia 2: 6dd5ca43
Los objetos del set son: 1, [com.javatutoriales.lombok.EjemploEqualsHashcode@6dd5ca43]
Ahora, para Java ambas instancias son iguales y por lo tanto ya no hay repetidos en el Set.

¿Qué pasa si no queremos incluir todos los atributos en la implementación de estos métodos? Por ejemplo, si quisiéramos que sólo se tomaran en cuenta el nombre y la matrícula.

EjemploEqualsHashcode instancia1 = new EjemploEqualsHashcode(2, "ABC123", "Alex");
EjemploEqualsHashcode instancia2 = new EjemploEqualsHashcode(2, "ABC123", "Juan");
Si ejecutamos el ejemplo con la modificación anterior, volveremos a obtener los resultados del inicio:

Las instancias son iguales?: false
Hash de la instancia 1: 6dd5ca43
Hash de la instancia 2: 6dda02dd
Los objetos del set son: 2, [com.javatutoriales.lombok.EjemploEqualsHashcode@6dd5ca43, com.javatutoriales.lombok.EjemploEqualsHashcode@6dda02dd]
Arreglar esto es muy simple, solo debemos marcar los atributos que no deseamos tomar en cuenta con la anotación "@EqualsAndHashCode.Exclude", de esta forma:

@EqualsAndHashCode.Exclude
private String nombre;
Si volvemos a ejecutar nuestra aplicación obtendremos la siguiente salida:

Las instancias son iguales?: true
Hash de la instancia 1: 72ac50bf
Hash de la instancia 2: 72ac50bf
Los objetos del set son: 1, [com.javatutoriales.lombok.EjemploEqualsHashcode@72ac50bf]
Con esto, volvemos a obtener el resultado que estamos esperando.

Para terminar este tutorial, veremos la anotación "@Data", la cual es un atajo para escribir las anotaciones "@ToString", "@EqualsAndHashCode", "@Getter", "@Setter" y "@RequiredArgsConstructor" en nuestra clase, con todas las opciones de dichas anotaciones (incluyendo el atributo "staticConstructor" para generar un método estático de fábrica). Esta anotación es excelente para escribir POJOs.

Para probar esta anotación, creamos una nueva clase llamada "EjemploData" y le colocamos los siguientes atributos:

@Data
public class EjemploData {
	private final String cadena;
	private final int entero;
	private String[] cadenas;
	private Date fecha;
} 
Al decorar esta clase con la anotación "@Data" veremos que se han generado todos los métodos indicados anteriormente (getters, setters, toString, equals, etc.).



Haremos algunos ajustes en esta clase, como evitar que la fecha se muestre en el método "toString" o evitar que el arreglo y la fecha sean tomados en cuenta en los métodos "equals" y "hashCode", además evitaremos que se genere el getter del atributo "entero". La clase completa queda de la siguiente forma:

@Data
public class EjemploData {
	private final String cadena;
	
	@Getter(AccessLevel.NONE)
	private final int entero;
	
	@EqualsAndHashCode.Exclude
	private String[] cadenas;
	
    @ToString.Exclude
	@EqualsAndHashCode.Exclude
	private Date fecha;
}
Ahora, regresaremos al método "main", en el que colocaremos las siguientes líneas para realizar nuestra prueba:

EjemploData ejemploData1 = new EjemploData("cadena", 1);
EjemploData ejemploData2 = new EjemploData("cadena", 1);
		
System.out.format("getCadena: %s\n", ejemploData1.getCadena());
System.out.format("getCadena: %s\n", ejemploData2.getCadena());
System.out.printf("Las instancias son iguales?: %b\n", ejemploData1.equals(ejemploData2));
System.out.printf("Valores: %s\n\n", ejemploData1);
Con esto comprobamos que el ejemplo funciona correctamente y terminamos el tutorial.

Como pudimos ver a lo largo de este tutorial, Lombok es una herramienta muy útil que nos permite escribir menos código y seguir teniendo control sobre el código que se genera, lo que la hace una herramienta perfecta para escribir nuestros POJOs y VOs.

Lombok ofrece más opciones, si quieren verlas todas pueden consultar su documentación oficial.

Estamos probando una nueva herramienta para dar formato al código, ¿qué les parece, es más claro así o nos quedamos con la herramienta anterior? Déjenme sus comentarios. ¿Qué nuevo tutorial les gustaría ver en el siguiente post?

Espero que este tutorial les sea de utilidad. Si tienen alguna duda, sugerencia, comentario o aclaración, pueden dejarla en la sección de comentarios o enviar un correo a programadorjavablog@gmail.com (pueden agregarme al chat de gmail). También pueden seguir JavaTutoriales en las siguientes redes sociales:

Saludos y gracias.

Descarga los archivos de este tutorial desde aquí:

  • Lombok