Proyecto Lombok, ¡facilítame la vida!

De un tiempo a esta parte, estamos siendo testigos de varias iniciativas que nos facilitan la vida como desarrolladores y hacen mucho más fácil nuestro día a día.

Una de ellas ha sido bautizada como el “Proyecto Lombok” y, aunque yo lo he empezado a utilizar hace relativamente poco tiempo, sí puedo decir que me está siendo muy útil y a estas alturas ya tengo la suficiente perspectiva como para dar una visión al respecto. ¿Empezamos?

¿Qué nos ofrece Lombok?

Lombok es un proyecto que nació en el año 2009 que, mediante contribuciones, ha ido ganando en riqueza y variedad de recursos. Es una librería para Java que a través de anotaciones nos reduce el código que codificamos, es decir, nos ahorra tiempo y mejora la legibilidad del mismo. Las transformaciones de código que realiza se hacen en tiempo de compilación.

Así que hecha esta breve introducción, vamos a entrar en materia para describir algunas anotaciones útiles.

@NonNull

Con esta anotación en el parámetro de entrada de un método o de un constructor conseguimos despreocuparnos del checkeo de que ese parámetro de entrada sea nulo. Esta anotación realiza esta sentencia:

if (param == null) throw new NullPointerException(“param”);

Hay que tener en cuenta que para los constructores el chequeo se realizará inmediatamente después de cualquier llamada al this() o super().

Un ejemplo sería:

import lombok.Cleanup;
import lombok.NonNull;
import java.io.*;
public class AddressBean {
    private String address;
    public AddressBean(@NonNull String address){
        this.address=address;
    }
    public static void main(String[] args){
        new AddressBean(null);
    }
}

Si lo ejecutamos vemos la siguiente salida: Exception in thread “main” java.lang.NullPointerException: address

Como vemos al realizar el check se lanza una excepción NullPointerException indicando el campo que produce esa salida.

@Cleanup

Esta anotación nos permite que cuando tengamos recursos del tipo InputStream, Outputstream, etc… donde normalmente, a través de una sentencia try, catch, englobamos una secuencia de instrucciones y acabamos con un finally para cerrar el recurso con una sentencia del tipo “reader.close()”.

Lo que hacemos el anotar la variable y automáticamente cerrar el recurso cuando la ejecución finalice su scope.

import lombok.Cleanup;
import java.io.*;
public class CleanupExampleLombok {
    public static void main(String[] args) throws IOException {
        @Cleanup  InputStream is = null;
        int i;
        char c;
        try {
            // new input stream created
            is = new FileInputStream("C://test.txt");
            System.out.println("Characters printed:");
            // reads till the end of the stream
            while((i = is.read())!=-1) {
                // converts integer to character
                c = (char)i;
                // prints character
                System.out.print(c);
            }
        } catch(Exception e) {
            // if any I/O error occurs
            e.printStackTrace();
        }
    }
}

@Getter and @Setter

Podemos anotar cualquier campo con las anotaciones Getter y Setter para generar automáticamente los métodos getter y setter de dicho campo.

Los métodos get y set serán públicos a no ser que se especifique pasándole un parámetro a la anotación a través de la clase AccessLevel. Los niveles de acceso que se permiten son PUBLIC, PROTECTED, PACKAGE y PRIVATE.

También puede poner estas anotaciones a nivel de clase. En este caso es como si anotáramos todos los campos no estáticos en la clase uno por uno.

Adicionalmente también podemos desactivar la generación de los métodos get y set para un determinado campo si ponemos el nivel de acceso NONE.

Por otro lado, debemos de tener en cuenta que en los método anotados con @Getter podemos indicar el parámetros “lazy” que actúa de forma similar a como se hace desde JPA al anotar los campos de una entidad

De esta manera el valor de dicho campo se almacenará en caché y solo se calcula una vez (normalmente esto se hace para objetos pesados de procesar).

import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
public class GetterSetterExample {
    /**
     * Age of the person. Water is wet.
     *
     * @param name Name of person
     * @return The current value of this name
     */
    @Getter @Setter private String name;
    /**
     * last name of the person.
     * -- SETTER --
     * Changes the LastNaame of this person.
     *
     * @param lastName The new value.
     */
    @Setter(AccessLevel.PROTECTED) private String lastName;
    @Override public String toString() {
        setName("Raul");
        setLastName("Martínez");
        return "Name:"+ getName()+ "and lastName:"+ lastName;
    }
}

@ToString

Cualquier clase puede ser anotada con @ToString para generar una implementación del método toString (). De forma predeterminada, imprimirá el nombre de la clase, junto con cada campo, en orden, separados por comas.

Podemos establecer el parámetro includeFieldNames, lo cual nos puede dar mayor legibilidad.

Por defecto, se imprimirán todos los campos no estáticos. Si desea omitir algunos campos, puede anotar estos campos con @ToString.Exclude.

De forma alternativa, puede especificar exactamente qué campos desea utilizar con onlyExplicitlyIncluded = true y luego marcar cada campo que desee incluir con @ToString.Include.

También podemos obtener el resultado del método ToString de la clase super indicando el parámetro callSuper=true.

Puede cambiar el nombre utilizado para identificar al miembro con @ToString.Include (name = “Raúl”), y puede cambiar el orden en que se imprimen los miembros mediante @ ToString.Include (rank = -1).

Los miembros sin un rango se consideran de rango 0, los miembros de un rango superior se imprimen primero, y los miembros del mismo rango se imprimen en el mismo orden en que aparecen en el fichero fuente.

import lombok.ToString;
 
@ToString
public class ToStringExample {
    private String name="Ejemplo 1";
    private Vehicule ford = new Car(5, "Ford");
    @ToString.Exclude private int id;
    public static void main (String[] args){
        System.out.println(new ToStringExample().toString());
    }
    public String getName() {
        return this.name;
    }
    @ToString(callSuper=true, includeFieldNames=true)
    public static class Car extends Vehicule {
        private final String name;
        private final int wheels;
        public Car(int wheels, String name) {
            this.wheels = wheels;
            this.name = name;
        }
    }
}

ToStringExample(name=Ejemplo 1, ford=ToStringExample.Car(super=ToStringExample$Car@37a71e93, name=Ford, wheels=5))

@EqualsAndHashCode

Genera los métodos equals() y hashCode() a partir de los campos del objeto. De forma predeterminada para generar estos métodos utiliza todos los campos de la clase no transitorios y no estáticos, aunque podemos modificar los campos que se utilizan utilizando @EqualsAndHashCode.Include o @EqualsAndHashCode.Exclude.

Si utilizamos el Include adicionalmente tenemos que indicar lo siguiente onlyExplicitlyIncluded = true:

import lombok.EqualsAndHashCode;
@EqualsAndHashCode
public class EqualsAndHashCodeExample {
    private String name="Ejemplo 1";
    @EqualsAndHashCode.Exclude private Vehicule ford = new Car(5, "Ford");
    @EqualsAndHashCode.Exclude private int id;
    public static void main (String[] args){
        System.out.println(new EqualsAndHashCodeExample().toString());
    }
    public String getName() {
        return this.name;
    }
    @EqualsAndHashCode(callSuper=true)
    public static class Car extends Vehicule {
        private final String name;
        private final int wheels;
        public Car(int wheels, String name) {
            this.wheels = wheels;
            this.name = name;
        }
    }
}

@NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor

Genera constructores que no toman argumentos, un argumento por campo final/no nulo, o un argumento por cada campo.

  • @NoArgsConstructor generará un constructor sin parámetros. Si esto no es posible (debido a los campos finales), en su lugar se producirá un error de compilación, a menos que se utilice @NoArgsConstructor (force = true).

Luego todos los campos finales se inicializan con 0/falso/nulo. Para los campos con restricciones, como los campos @NonNull, no se genera ninguna verificación, así que tenga en cuenta que estas restricciones generalmente no se cumplirán hasta que esos campos se inicien correctamente más tarde.

  • @RequiredArgsConstructor genera un constructor con un parámetro para cada campo que requiere un manejo especial. Todos los campos finales no inicializados obtienen un parámetro, así como también los campos que están marcados como @NonNull que no se inicializan donde se declaran.
  • @AllArgsConstructor genera un constructor con un parámetro para cada campo en su clase.

Cada una de estas anotaciones permite una forma alternativa, donde el constructor generado es siempre privado, y se genera un método estático adicional que envuelve al constructor privado.

Este modo se habilita suministrando el valor de staticName para la anotación, así: @RequiredArgsConstructor (staticName = “additional”):

import lombok.*;

@RequiredArgsConstructor(staticName = "additional")
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class ConstructorExample<T> {
    private int field1, field2;
    @NonNull private T description;
    @NoArgsConstructor
    public static class NoArgsExample {
        @NonNull private String field;
    }
}

Es equivalente a generar:

private ConstructorExample(String description) {
    if (description == null) throw new NullPointerException("description");
    this.description = description;
}
public static ConstructorExample  additional(String description) {
    return new ConstructorExample(description);
}
 
protected ConstructorExample(int field1, int field2, String description) {
    if (description == null) throw new NullPointerException("description");
    this.field1 = field1;
    this.field2 = field2;
    this.description = description;
}
public NoArgsExample() {
}
}

@Data

@Data es la anotación de acceso directo que agrupa las características de @ToString, @EqualsAndHashCode, @Getter/@Setter y @RequiredArgsConstructor juntas: en otras palabras, @Data genera todos los estándares que normalmente se asocian con POJOS y beans:

  • Getters para todos los campos.
  • Setters para todos los campos no finales y las implementaciones toString.
  • Equals y hashCode que involucran los campos de la clase.
  • Un constructor que inicializa todos los campos finales.
  • Así como todos los campos no finales sin inicializador que haya sido marcado con @NonNull, para garantizar que el campo nunca sea nulo.
@Data
public class ExecuteFulfillmentBean {
    CustomersItems customersItem;
    File fulfillmentFile;
    boolean hasNextElement;
}

@Value

@Value es la variante inmutable de @Data; todos los campos son privados y finales de forma predeterminada, y los setters no son generados.

La clase en sí misma también se convierte en definitiva por defecto, porque la inmutabilidad no es algo que pueda forzarse en una subclase.

Al igual que @Data, también se generan métodos útiles toString (), equals () y hashCode (), cada campo obtiene un método getter y también se genera un constructor que cubre todos los argumentos (excepto los campos finales que se inicializan en la declaración de campo).

En la práctica, @Value es una abreviación de: final @ToString @EqualsAndHashCode @AllArgsConstructor @FieldDefaults (makeFinal = true, level = AccessLevel.PRIVATE) @Getter

@Value
public class ValueExample {
    private Object left;
    private Object right;
}

Esto es equivalente a codificar lo siguiente:

import lombok.*;
public class ValueExample {
    private Object left;
    private Object right;
    @java.beans.ConstructorProperties({ "left", "right" })
    ValueExample(Object left, Object right) {
        this.left = left;
        this.right = right;
    }
    public Object getLeft() {
        return this.left;
    }
    public Object getRight() {
        return this.right;
    }
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof ValueExample)) return false;
        final ValueExample other = (ValueExample) o;
        final Object this$left = this.left;
        final Object other$left = other.left;
        if (this$left == null ? other$left != null : !this$left.equals(other$left)) return false;
        final Object this$right = this.right;
        final Object other$right = other.right;
        if (this$right == null ? other$right != null : !this$right.equals(other$right)) return false;
        return true;
    }
    public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        final Object $left = this.left;
        result = result * PRIME + ($left == null ? 0 : $left.hashCode());
        final Object $right = this.right;
        result = result * PRIME + ($right == null ? 0 : $right.hashCode());
        return result;
    }
    public String toString() {
        return "ValueExample(left=" + this.left + ", right=" + this.right + ")";
    }
}

@Builder

@Builder le permite generar automáticamente el código requerido para que su clase sea instanciable. Se puede colocar en una clase, o en un constructor, o en un método.

@Builder
public class ExecuteFulfillmentBean {
    CustomersItems customersItem;
    File fulfillmentFile;
    boolean hasNextElement;
}

Para que sea utilizable por otra clase o método debemos de hacer algo como esto:

ExecuteFulfillmentBean.builder().customersItem(pCustomersItem).

fulfillmentFile(pFulfillmentFile).hasNextElement(pHasNextElement).build();

Esto nos genera un objeto de tipo ExecuteFulfillmentBean con todos los valores ya inicializados.

@Synchronized

@Synchronized es una variante más segura del modificador “synchronized”.  Funciona de manera similar a la palabra clave “synchronized”, con la salvedad de que no es necesario declarar el recurso a bloquear, se puede generar de forma automática.

Si este es el caso se crea un objeto estático $LOCK o $lock que se crea implícitamente para obtener el bloqueo a la hora de modificar un recurso.

import lombok.Synchronized;
public class SynchronizedExample {
    private final Object resource = new Object();
    @Synchronized
    public static void main() {
        System.out.println("Hello");
    }
    @Synchronized
    public int yourAge() {
        return 37;
    }
    @Synchronized("resource")
    public void print() {
        System.out.println("print in resource");
    }
}

Es lo mismo que codificar lo siguiente:

public class SynchronizedExample {
    private static final Object $LOCK = new Object[0];
    private final Object $lock = new Object[0];
    private final Object resource = new Object();
 
    public static void main() {
        synchronized($LOCK) {
            System.out.println("Hello");
        }
    }
    public int yourAge() {
        synchronized($lock) {
            return 37;
        }
    }
    public void print() {
        synchronized(resource) {
            System.out.println("print in resource");
        }
    }
}

Conclusiones

Como vemos, Lombok es una librería fácil de usar, con sus peculiaridades que no hay que dar por supuestas. De hecho, es recomendable documentarse y tener claro qué función realiza cada anotación.

Por ejemplo, cuando lo comencé a utilizar inicialmente, no leí cada uno de los casos, y me di cuenta a través de un @Data la cobertura a través de un test unitario no cuadraba con las líneas de código que estaban testeadas.

Al ver los fuentes que generaba vi que los equalsAndHashCode, toString, etc… eran métodos que efectivamente no testeaba. Ante esto, lo que vi es que el uso que hacía de la anotación no era correcta.

Aunque inicialmente es sencillo de usar, si queremos hacer lógica algo más complicada debemos de ser cuidadosos.

En definitiva, después de utilizar Lombok creo que facilita muchas tareas que suelen ser bastante mecánicas, tal y como indica el título, simplifica bastante el código, nos abstrae de ciertas operaciones, y mejora la legibilidad.

Ingeniero Informático con 11 años de experiencia en el desarrollo de aplicaciones web en entornos J2EE. Después de 8 años en Indra, donde trabajó en proyectos para el Ministerio de Educación, DGT (Dirección General de Tráfico), RFEF (Real Federación Española de Fútbol) y SELAE (Loterías y Apuestas del Estado), vio en Paradigma una oportunidad de seguir creciendo. Con gran experiencia en frameworks Spring (Spring 4, Spring Boot, Spring Webflow, Spring Data, etc.), actualmente inmerso en proyectos de eCommerce con ATG 10.2 para El Corte Inglés.

Ver toda la actividad de Raúl Martínez