Genera Representaciones de String con Lombok en JAVA

Introducción

El método toString() en JAVA se utiliza para obtener la representación en cadena de un objeto. Sin embargo, crear implementaciones de este método para cada clase puede resultar tedioso y agregar un código innecesario que podría haber sido evitado. Afortunadamente, Project Lombok llega al rescate al permitirnos generar representaciones de cadena consistentes sin el desorden de escribir manualmente el código. En este tutorial, exploraremos cómo auto-generar el método toString() utilizando Lombok y las diversas opciones de configuración disponibles para ajustar la salida resultante.

1. Resumen

Como sabemos, el método toString() se utiliza para obtener la representación en cadena de un objeto en JAVA. Project Lombok puede ayudarnos a generar representaciones de cadena consistentes sin el boilerplate, mejorando también la mantenibilidad, especialmente en clases que pueden contener un gran número de campos.

2. Configuración

Comencemos por incluir la dependencia de Project Lombok en nuestro proyecto de ejemplo:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.30</version>
    <scope>provided</scope>
</dependency>

En nuestros ejemplos, utilizaremos una simple clase POJO Account con algunos campos para demostrar la funcionalidad y las diversas opciones de configuración.

3. Uso Básico

Podemos anotar cualquier clase con la anotación Lombok @ToString. Esto modifica el bytecode generado y crea una implementación del método toString().

import lombok.ToString;

@ToString
public class Account {
    private String id;
    private String name;

    // getters y setters estándar
}

Por defecto, la anotación @ToString imprime el nombre de la clase, junto con cada nombre de campo no estático y su valor obtenido por la llamada al getter (si está declarado). Los campos aparecen en el orden de declaración en la clase fuente y están separados por comas.

Ahora, una llamada al método toString() en una instancia de esta clase genera la siguiente salida:

Account(id=12345, name=An account)

En la mayoría de los casos, esto es suficiente para generar una representación de cadena estándar y útil de los objetos JAVA.

4. Opciones de Configuración

Existen varias opciones de configuración disponibles que nos permiten modificar y ajustar el método toString() generado. Vamos a explorar estas configuraciones más a fondo.

4.1. toString() de Superclase

Por defecto, la salida no contiene datos de la implementación del método toString() de la superclase. Sin embargo, podemos modificar esto configurando el atributo callSuper en true:

@ToString(callSuper = true)
public class SavingAccount extends Account {
    private String savingAccountId;

    // getters y setters estándar
}

Esto produce la siguiente salida que incluye la información de la superclase seguida de los campos y valores de la subclase:

SavingAccount(super=Account(id=12345, name=An account), savingAccountId=6789)

4.2. Omitiendo Nombres de Campo

Como vimos anteriormente, la salida por defecto contiene nombres de campos seguidos de sus valores. Sin embargo, podemos omitir los nombres de campo configurando el atributo includeFieldNames en false en la anotación @ToString:

@ToString(includeFieldNames = false)
public class Account {
    private String id;
    private String name;

    // getters y setters estándar
}

Como resultado, la salida ahora muestra una lista separada por comas de todos los valores de los campos sin los nombres de los campos:

Account(12345, An account)

4.3. Usando Campos en lugar de Getters

Por defecto, los métodos getter proporcionan los valores de los campos para imprimir. No obstante, podemos configurar Lombok para que siempre use los valores de los campos directos en lugar de los getters configurando el atributo doNotUseGetters en true:

@ToString(doNotUseGetters = true)
public class Account {
    private String id;
    private String name;

    // getter ignorado
    public String getId() {
        return "este es el id:" + id;
    }

    // getters y setters estándar
}

Sin esta configuración, obtendremos la salida que se obtendría invocando a los getters:

Account(id=este es el id:12345, name=An account)

Sin embargo, con el atributo doNotUseGetters, la salida muestra el valor del campo id, sin invocar el getter:

Account(id=12345, name=An account)

4.4. Inclusión y Exclusión de Campos

A veces, desearíamos excluir ciertos campos de la representación en cadena, como contraseñas o información sensible. Podemos omitir dichos campos simplemente anotándolos con @ToString.Exclude:

@ToString
public class Account {
    private String id;

    @ToString.Exclude
    private String name;

    // getters y setters estándar
}

Alternativamente, podemos especificar solo los campos que se requieren en la salida. Logramos esto usando @ToString(onlyExplicitlyIncluded = true) a nivel de clase y luego anotando cada campo requerido con @ToString.Include:

@ToString(onlyExplicitlyIncluded = true)
public class Account {
    @ToString.Include
    private String id;

    private String name;

    // getters y setters estándares
}

Ambos enfoques anteriores producen la siguiente salida con solo el campo id:

Account(id=12345)

Además, Lombok automáticamente excluye cualquier variable que comience con el símbolo $. Sin embargo, podemos anular este comportamiento e incluirlas agregando la anotación @ToString.Include a nivel de campo.

4.5. Ordenación de la Salida

Por defecto, la salida contiene los campos según el orden de declaración en la clase. Sin embargo, podemos ajustar el orden agregando el atributo rank a la anotación @ToString.Include.

Por ejemplo, modifiquemos nuestra clase Account para que el campo id se renderice primero, independientemente de su posición de declaración en la definición de la clase:

@ToString
public class Account {
    private String name;

    @ToString.Include(rank = 1)
    private String id;

    // getters y setters estándar
}

Ahora, el campo id se renderiza primero en la salida:

Account(id=12345, name=An account)

La salida contiene los miembros de un rango superior primero, seguidos por rangos más bajos. El valor de rango predeterminado para los miembros sin el atributo de rango es 0. Los miembros con el mismo rango se imprimen de acuerdo con su orden de declaración.

4.6. Salida de Métodos

Además de los campos, también es posible incluir la salida de un método de instancia que no tome argumentos. Podemos hacer esto marcando el método sin argumentos con @ToString.Include:

@ToString
public class Account {
    private String id;
    private String name;

    @ToString.Include
    String description() {
        return "Descripción de la cuenta";
    }

    // getters y setters estándar
}

Esto añade la description como clave y su salida como valor a la representación de Account:

Account(id=12345, name=An account, description=Descripción de la cuenta)

Si el nombre del método coincide con un nombre de campo, entonces el método toma prioridad sobre el campo. En otras palabras, la salida contiene el resultado de la invocación del método en lugar del valor del campo coincidente.

4.7. Modificando Nombres de Campos

Podemos cambiar cualquier nombre de campo especificando un valor diferente en el atributo name de la anotación @ToString.Include:

@ToString
public class Account {
    @ToString.Include(name = "identificación")
    private String id;

    private String name;

    // getters y setters estándares
}

Ahora, la salida contiene el nombre alternativo de campo desde el atributo de anotación en lugar del nombre del campo real:

Account(identificación=12345, name=An account)

5. Imprimiendo Arrays

Los arrays se imprimen utilizando el método Arrays.deepToString(). Esto convierte los elementos del array a sus representaciones de cadena correspondientes. Sin embargo, es posible que el array contenga ya sea una referencia directa o una referencia circular indirecta.

Para evitar la recursión infinita y los errores de tiempo de ejecución asociados, este método renderiza cualquier referencia circular al array desde dentro de sí mismo como “[[…]]”.

Veamos esto agregando un campo de array Object a nuestra clase Account:

@ToString
public class Account {
    private String id;
    private Object[] relatedAccounts;

    // getters y setters estándar
}

El array relatedAccounts ahora se incluye en la salida:

Account(id=12345, relatedAccounts=[54321, [...]])

Importante, la referencia circular es detectada por el método deepToString() y renderizada apropiadamente por Lombok, sin provocar ningún StackOverflowError.

6. Puntos a Recordar

Las siguientes consideraciones son importantes para evitar resultados inesperados:

  • En presencia de cualquier método llamado toString() en la clase (sin importar el tipo de retorno), Lombok no genera su método toString().
  • Diferentes versiones de Lombok pueden cambiar el formato de salida del método generado. En cualquier caso, debemos evitar un código que dependa de analizar la salida del método toString(), así que esto no debería ser un problema.
  • Además, podemos agregar esta anotación a enum, produciendo una representación donde el valor del enum sigue al nombre de la clase enum, por ejemplo, AccountType.SAVING.

7. Conclusión

En este artículo, hemos visto cómo utilizar las anotaciones de Lombok para generar una representación en String de objetos JAVA con un esfuerzo y boilerplate mínimo. Inicialmente, observamos el uso básico, que suele ser suficiente para la mayoría de los casos. Luego, cubrimos una amplia gama de opciones disponibles para ajustar y sintonizar la salida generada, optimizando la forma en que representamos nuestros objetos en cadena.

Además, la facilidad de configuración de Lombok permite a los desarrolladores centrarse más en la lógica y menos en la gestión de código repetitivo, lo que puede llevar a un desarrollo más rápido y limpio. Te invitamos a explorar Lombok y ver cómo puede mejorar tu trabajo en JAVA.