Lombok es una librería que escribe por ti el código aburrido de Java: getters, setters, constructores, toString… Tú pones una etiqueta encima de la clase y el resto aparece solo. Aquí vamos a ver qué hace cada etiqueta, cuándo usarla y en qué charcos no meterse.
Antes de ver la herramienta, hay que entender qué dolor quita.
En Java, casi todas las clases que guardan datos (un conductor, un vehículo, una multa…) necesitan lo mismo: campos private, un método para leer cada campo (getter), otro para cambiarlo (setter), uno o varios constructores, un toString() para poder imprimirlo y un equals()/hashCode() para poder compararlo. Ese código se llama boilerplate: relleno repetitivo que no aporta nada nuevo pero que hay que escribir, leer y mantener.
Mira la diferencia con una clase de tres campos:
Sin Lombok (≈ 60 líneas)
public class Conductor {
private String dni;
private String nombre;
private int puntos;
public Conductor() { }
public Conductor(String dni, String nombre, int puntos) {
this.dni = dni;
this.nombre = nombre;
this.puntos = puntos;
}
public String getDni() { return dni; }
public void setDni(String dni) { this.dni = dni; }
public String getNombre() { return nombre; }
public void setNombre(String nombre) { this.nombre = nombre; }
public int getPuntos() { return puntos; }
public void setPuntos(int puntos) { this.puntos = puntos; }
@Override
public String toString() {
return "Conductor(dni=" + dni + ", nombre=" + nombre
+ ", puntos=" + puntos + ")";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Conductor)) return false;
Conductor c = (Conductor) o;
return puntos == c.puntos
&& Objects.equals(dni, c.dni)
&& Objects.equals(nombre, c.nombre);
}
@Override
public int hashCode() {
return Objects.hash(dni, nombre, puntos);
}
}
Con Lombok (≈ 10 líneas)
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Conductor {
private String dni;
private String nombre;
private int puntos;
}
Las dos clases hacen exactamente lo mismo. Puedes llamar a conductor.getNombre(), conductor.setPuntos(8), new Conductor("123A", "Ana", 12) o imprimirlo con System.out.println(conductor) igual que si lo hubieras escrito tú.
Y si mañana añades un campo email, con Lombok no tocas nada más: getter, setter, constructor y toString se actualizan solos. Sin Lombok, tendrías que acordarte de tocar cinco sitios (y alguno se te olvidaría).
Saber esto te ahorra muchos sustos cuando algo «no compila».
Las cosas que empiezan por @ se llaman anotaciones: son etiquetas que pegas encima de una clase, campo o método. Por sí solas no hacen nada; alguien tiene que leerlas.
Lombok es un procesador de anotaciones: un programa que se «cuela» en el compilador de Java (javac). Cuando compilas, Lombok lee tus etiquetas y añade el código que falta antes de que se genere el .class. Resultado:
.java no verás los getters: el fuente se queda corto y limpio..class compilado sí están, como si los hubieras escrito tú.<scope>provided</scope> (se usa al compilar, no se empaqueta en el .jar final).@Data: aparecen todos los métodos generados aunque no estén en el texto. También puedes usar Refactor → Delombok para ver el código «largo» que genera.Dos piezas: la dependencia y avisar al compilador.
Si creas el proyecto con Spring Initializr y marcas la dependencia Lombok, ya viene todo hecho. Si lo añades a mano, en el pom.xml:
<!-- 1) La dependencia: solo hace falta al compilar -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope> <!-- en Spring Boot la versión la pone el padre -->
</dependency>
<!-- 2) Registrarlo como procesador de anotaciones (obligatorio desde JDK 23) -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
cannot find symbol: method getNombre() aunque tengas @Data puesto. En el curso usamos Java 25, así que este paso es obligatorio. (Fuera de Spring Boot tendrás que poner también <version> en ambos sitios.)java -jar lombok.jar) y reiniciar.Si el IDE no está configurado, el proyecto compila con Maven pero el editor te lo llena todo de rojo porque «no ve» los métodos generados.
@Getter y @Setter: leer y cambiar camposLas más básicas. Todo lo demás se construye sobre ellas.
Puedes ponerlas sobre la clase (afectan a todos los campos) o sobre un campo (solo a ese).
@Getter // getters para TODOS los campos
public class Vehiculo {
private String matricula;
@Setter private String color; // setter SOLO para color
private boolean electrico;
}
// Lombok genera:
// getMatricula(), getColor(), isElectrico() ← boolean usa "is"
// setColor(String)
boolean (primitivo) el getter se llama isElectrico(), no getElectrico(). Es la convención de Java.final nunca tienen setter (no se pueden cambiar, así que no tendría sentido).@Setter(AccessLevel.PROTECTED) genera un setter protected; AccessLevel.NONE hace que no se genere para ese campo.@ToString: que al imprimirlo se entienda algoSin él, imprimir un objeto da algo como Conductor@1b6d3586.
@ToString
@AllArgsConstructor
public class Usuario {
private String login;
@ToString.Exclude private String password; // ¡nunca al log!
}
System.out.println(new Usuario("ana", "1234"));
// → Usuario(login=ana)
Usa @ToString.Exclude para dos cosas: datos sensibles (contraseñas, tokens) y relaciones entre objetos que se apuntan mutuamente (lo verás en las trampas, sección 14).
@EqualsAndHashCode: cuándo dos objetos son «el mismo»Importante en cuanto metes objetos en un Set o de clave en un Map.
En Java, == compara si son el mismo objeto en memoria. equals() compara si son iguales en contenido. Por defecto (sin sobrescribirlo), equals() se comporta como ==, y eso casi nunca es lo que quieres: dos Matricula("1234ABC") creadas por separado deberían considerarse iguales.
hashCode() es un número «resumen» del objeto que usan HashSet y HashMap para encontrarlo rápido. La regla de oro: si dos objetos son equals, deben tener el mismo hashCode. Por eso siempre van juntos, y por eso Lombok los genera a la vez.
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Conductor {
@EqualsAndHashCode.Include private String dni; // solo el DNI decide
private String nombre;
private int puntos;
}
// Dos conductores con el mismo DNI son iguales aunque tengan distintos puntos.
static ni transient.@EqualsAndHashCode.Exclude o, como arriba, decir «solo los que yo marque».callSuper = true si esos campos también deben contar.Cada uno crea un constructor distinto. Se pueden combinar.
| Anotación | Qué constructor genera | Para qué se usa típicamente |
|---|---|---|
@NoArgsConstructor | Uno vacío: new Conductor() | Frameworks que crean el objeto vacío y luego lo rellenan (JPA/Hibernate, Jackson al leer JSON). |
@AllArgsConstructor | Uno con todos los campos, en el orden en que están declarados | Crear objetos completos de una vez, tests. |
@RequiredArgsConstructor | Uno solo con los campos obligatorios: los final sin inicializar y los marcados con @NonNull | Inyección de dependencias en Spring (lo usarás muchísimo). |
En Spring, un servicio necesita otros objetos (un repositorio, un validador…). La forma recomendada de recibirlos es por constructor, con campos final. @RequiredArgsConstructor te escribe ese constructor:
Sin Lombok
@Service
public class ExpedienteService {
private final ExpedienteRepository repo;
private final NotificacionService avisos;
public ExpedienteService(ExpedienteRepository repo,
NotificacionService avisos) {
this.repo = repo;
this.avisos = avisos;
}
}
Con Lombok
@Service
@RequiredArgsConstructor
public class ExpedienteService {
private final ExpedienteRepository repo;
private final NotificacionService avisos;
}
Spring ve un único constructor y le pasa automáticamente el repositorio y el servicio de avisos. Si mañana necesitas otra dependencia, añades un campo final y listo.
final@NoArgsConstructor no puede dar valor a campos final, así que da error de compilación si los hay. Existe @NoArgsConstructor(force = true), que los rellena con null/0/false, pero úsalo solo si sabes por qué.@Data: el «todo en uno»La más famosa y, precisamente por eso, la que más se usa mal.
@Data es un atajo que equivale a poner a la vez:
@Getter // en todos los campos
@Setter // en todos los campos NO final
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
Perfecta para clases que solo transportan datos y cambian: un formulario, un objeto que rellenas por partes, un DTO mutable…
@Data no genera constructor vacío ni «con todo». Si no hay campos final, el constructor que genera no tiene parámetros; si los hay, solo recibe esos. Por eso es tan habitual verlo junto a @NoArgsConstructor y @AllArgsConstructor.@Data deja de generar el suyo.@Data te los deja cambiar. Piensa si de verdad quieres eso.@Value y su primo moderno, el recordPara datos que, una vez creados, no cambian (inmutables).
@Value es la versión «de solo lectura» de @Data: todos los campos pasan a ser private final, la clase se vuelve final, genera getters, toString, equals/hashCode y un constructor con todos los campos, y no genera setters.
Con Lombok
@Value
public class Coordenadas {
double latitud;
double longitud;
}
// c.getLatitud()
Con Java puro (desde Java 16)
public record Coordenadas(double latitud,
double longitud) { }
// c.latitud() ← sin "get"
record: es Java estándar, no necesita librerías y es justo lo que usa el dominio de MULTAGAL (Matricula, Coordenadas, Expediente…). Lombok sigue siendo muy útil para clases mutables (entidades JPA, objetos que se rellenan por partes), para los servicios de Spring (@RequiredArgsConstructor) y para el logger (@Slf4j). Comparativa completa al final, en Similitudes.@Builder: crear objetos con muchos campos sin volverse locoAdiós a los new Multa(null, 200, null, "A-12", true, null).
Cuando un constructor tiene muchos parámetros, es muy fácil equivocarse de orden (¿el 3º era el importe o los puntos?). El patrón Builder te deja nombrar cada valor:
@Builder(toBuilder = true)
@Getter
public class Multa {
private String matricula;
private double importe;
private int puntos;
@Builder.Default private boolean pagada = false;
}
Multa m = Multa.builder()
.matricula("1234ABC")
.importe(200)
.puntos(4)
.build(); // pagada = false (gracias a @Builder.Default)
Multa pagada = m.toBuilder().pagada(true).build(); // copia con pagada = true
null, 0, false)… aunque les hayas dado un valor inicial, salvo que lleven @Builder.Default.@Builder(toBuilder = true) añade toBuilder(): crea una copia del objeto para cambiarle algo. Muy útil con objetos inmutables.@Singular sobre una lista te permite añadir elementos uno a uno: .alegacion(a1).alegacion(a2).@Slf4j: un logger gratisPara escribir mensajes de log en vez de System.out.println.
@Slf4j
@Service
public class RadarService {
public void procesar(Denuncia d) {
log.info("Procesando denuncia {}", d.id());
log.debug("Detalle: {}", d);
}
}
// Lombok añade por ti:
// private static final org.slf4j.Logger log =
// org.slf4j.LoggerFactory.getLogger(RadarService.class);
La variable siempre se llama log. Las llaves {} se sustituyen por los argumentos, en orden. Spring Boot ya trae SLF4J, así que funciona sin añadir nada.
No son las del día a día, pero conviene reconocerlas.
| Anotación | Qué hace, en cristiano |
|---|---|
@NonNull | Sobre un campo o parámetro: si alguien pasa null, lanza NullPointerException en el momento, en vez de fallar diez líneas después. En campos, además, los hace «obligatorios» para @RequiredArgsConstructor. |
@With | Genera withPuntos(8): devuelve una copia del objeto con ese campo cambiado. Para objetos inmutables. |
@Getter(lazy = true) | El valor se calcula la primera vez que se pide y luego se reutiliza. Útil si calcularlo es caro. |
@Cleanup | Cierra un recurso (fichero, conexión) al acabar el bloque. Hoy es mejor el try-with-resources de Java. |
@SneakyThrows | Permite lanzar excepciones comprobadas sin declararlas. Esconde errores: evítala en clase. |
@SuperBuilder | Como @Builder, pero funciona con herencia (el builder del hijo incluye los campos del padre). |
Combinaciones que funcionan bien en un proyecto Spring Boot.
| Tipo de clase | Anotaciones recomendadas |
|---|---|
| Servicio / controlador de Spring | @RequiredArgsConstructor + campos private final (y @Slf4j si registra cosas) |
| Dato inmutable sencillo (DTO, valor) | record de Java, sin Lombok |
| Objeto mutable «de datos» (formulario, config) | @Data (+ @NoArgsConstructor / @AllArgsConstructor si los necesitas) |
Entidad JPA (@Entity) | @Getter @Setter @NoArgsConstructor y un @ToString que excluya relaciones. Nada de @Data (ver trampas). |
| Objeto con muchos campos opcionales | @Builder (+ @AllArgsConstructor y @NoArgsConstructor si también lo usa un framework) |
// Entidad JPA "segura" con Lombok
@Entity
@Getter @Setter
@NoArgsConstructor
@ToString
public class Conductor {
@Id @GeneratedValue
private Long id;
private String dni;
private String nombre;
@ToString.Exclude
@OneToMany(mappedBy = "conductor")
private List<Multa> multas = new ArrayList<>();
}
Lo que suele costar una tarde entera de depuración.
@DataLombok no se está ejecutando al compilar. Casi siempre es que falta el bloque annotationProcessorPaths del pom.xml (sección 3), sobre todo con Java 23 o superior. Si solo falla en el editor pero ./mvnw compile va bien, es el IDE: activa el procesamiento de anotaciones.
StackOverflowError al imprimir o al meter en un SetSi un Conductor tiene una lista de Multa y cada Multa apunta a su Conductor, el toString() del conductor imprime sus multas, que imprimen su conductor, que imprime sus multas… hasta reventar. Con equals/hashCode pasa lo mismo. Solución: excluye la relación en al menos un lado con @ToString.Exclude y @EqualsAndHashCode.Exclude.
@Data en entidades JPAAdemás del bucle anterior, en JPA el equals/hashCode que usa todos los campos da problemas: el id cambia de null a un número al guardar (el objeto «cambia de identidad» dentro de un HashSet), y leer relaciones perezosas (lazy) desde toString puede lanzar consultas extra o LazyInitializationException. Por eso la recomendación habitual es @Getter @Setter @NoArgsConstructor y escribir equals/hashCode con cuidado (o no sobrescribirlos).
@Builder junto a @NoArgsConstructor no compila@Builder necesita un constructor con todos los campos. Solo lo crea él mismo si tú no has declarado otro constructor. En cuanto añades @NoArgsConstructor, deja de hacerlo. Solución: añade también @AllArgsConstructor.
private boolean pagada = false; se ignora al usar builder(). Añade @Builder.Default encima del campo.
equals que no entiendes.Una línea por anotación.
| Anotación | Genera |
|---|---|
@Getter | getX() (o isX() si es boolean) |
@Setter | setX(valor) para campos no final |
@ToString | toString() legible con los campos |
@EqualsAndHashCode | equals() + hashCode() a partir de los campos |
@NoArgsConstructor | Constructor vacío |
@AllArgsConstructor | Constructor con todos los campos |
@RequiredArgsConstructor | Constructor con los final y @NonNull |
@Data | Getter + Setter + ToString + EqualsAndHashCode + RequiredArgsConstructor |
@Value | Como @Data pero inmutable (sin setters, todo final, constructor con todo) |
@Builder | Clase.builder().campo(v).build() |
@Slf4j | Campo log para registrar mensajes |
@NonNull | Comprobación de null automática |
Piensa la respuesta antes de abrirla.
No. Trabaja solo durante la compilación: cuando se ejecuta, el código generado ya está dentro de los .class. Por eso su scope en Maven es provided.
@Data y dos campos no final. ¿Puedo hacer new Clase("a", "b")?No. @Data incluye @RequiredArgsConstructor, que solo recibe campos final o @NonNull; aquí no hay ninguno, así que el constructor generado es vacío. Necesitas añadir @AllArgsConstructor.
@Service con dos dependencias private final?@RequiredArgsConstructor: genera el constructor con esas dos dependencias y Spring las inyecta por él.
@Getter para private boolean activo;?isActivo().
Que Lombok esté registrado en annotationProcessorPaths del maven-compiler-plugin (obligatorio desde JDK 23) y que el IDE tenga activado el procesamiento de anotaciones.
@Value o record para Matricula?record: es inmutable, estándar de Java y no depende de ninguna librería. @Value tiene sentido en proyectos antiguos (antes de Java 16) o si necesitas algo que el record no permite, como heredar de otra clase.
Para situar Lombok en el mapa: qué más resuelve el mismo problema y en qué se diferencia.
record: la alternativa oficial de Java a @Value (y a medio @Data)Desde Java 16, un record es una clase especial pensada para guardar datos que no cambian. Con una sola línea, Java genera por ti el constructor con todos los campos, un método de lectura por campo, toString(), equals() y hashCode(). Es decir: casi lo mismo que @Value, pero sin ninguna librería.
Lombok
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Conductor {
private String dni;
private String nombre;
private int puntos;
}
c.getNombre();
c.setPuntos(8); // se puede cambiar
record
public record Conductor(String dni,
String nombre,
int puntos) {
// Validación opcional en el "constructor compacto"
public Conductor {
if (puntos < 0) throw new IllegalArgumentException();
}
}
c.nombre(); // sin "get"
c.withPuntos(8); // ✗ no existe: hay que crear otro
new Conductor(c.dni(), c.nombre(), 8);
record | Lombok (@Data / @Value) | |
|---|---|---|
| ¿Hace falta instalar algo? | ✅ No, es Java puro | ❌ Dependencia + configurar compilador e IDE |
| ¿Se pueden cambiar los datos? | ❌ Nunca (inmutable) | ✅ Con @Data sí; con @Value no |
| Nombre de los getters | nombre() | getNombre() (convención JavaBean) |
| Constructor vacío | ❌ No puede tenerlo | ✅ @NoArgsConstructor |
| Herencia | ❌ No puede heredar de otra clase ni ser heredado | ✅ Una clase normal puede heredar |
Builder, with…, logger | ❌ Hay que escribirlos a mano | ✅ @Builder, @With, @Slf4j |
| Validar datos al crear | ✅ Constructor compacto, muy cómodo | 🟡 Posible, pero escribiendo tú el constructor |
¿Sirve como entidad JPA (@Entity)? | ❌ No (JPA necesita constructor vacío y campos modificables) | ✅ Sí, con @Getter @Setter @NoArgsConstructor |
| ¿Sirve como DTO para JSON (Jackson / Spring)? | ✅ Sí, funciona directamente | ✅ Sí |
| ¿Lo entiende cualquiera que sepa Java? | ✅ Sí | 🟡 Hay que conocer qué genera cada anotación |
record. ¿Objetos que un framework necesita crear vacíos y rellenar (entidades JPA), que cambian con el tiempo o que heredan? → clase normal + Lombok. Por eso en MULTAGAL el dominio va con record y los servicios con @RequiredArgsConstructor.IntelliJ, Eclipse y VS Code tienen un menú Generate que escribe getters, setters, constructores, toString, equals y hashCode con un par de clics. El resultado final es el mismo que con Lombok, pero con una diferencia clave:
equals desactualizado = error silencioso).Consejo de profe: escribe a mano o con el IDE una clase completa al menos una vez. Si entiendes lo que Lombok te ahorra, sabrás arreglarlo cuando falle.
@Override y los métodos de ObjectYa has usado anotaciones: @Override es una. La diferencia es que @Override solo comprueba (el compilador se queja si el método no sobrescribe nada), mientras que las de Lombok generan código.
Y lo que genera no es nada nuevo: toda clase de Java hereda de Object los métodos toString(), equals() y hashCode(). @ToString y @EqualsAndHashCode simplemente los sobrescriben por ti, igual que harías tú con @Override.
@Autowired en Spring frente a @RequiredArgsConstructorEn tutoriales antiguos verás las dependencias inyectadas así: @Autowired private ExpedienteRepository repo;. Funciona, pero el campo no puede ser final y la clase es difícil de probar sin Spring. La práctica recomendada hoy es la inyección por constructor, y @RequiredArgsConstructor la deja igual de corta que @Autowired pero con campos final. Mismo objetivo, forma más segura.
← Todas las explicaciones «en cristiano» de AD · Volver a los temas de AD