Skip to main content

Command Palette

Search for a command to run...

Java Lombok Core Annotations

Updated
•7 min read•View as Markdown
Java Lombok Core Annotations

0. Introduction

When working with Java DTOs, entities, or simple data holder classes, developers often write a large amount of repetitive boilerplate code. Common examples include getters, setters, equals(), hashCode(), and toString() methods.

Project Lombok is a popular library that reduces this boilerplate by generating code at compile time using annotations.

However, Lombok annotations are often used without fully understanding what code they generate, especially since several of them provide overlapping functionality.

This article explains five commonly used Lombok annotations that frequently appear in Java projects:

  1. @Getter

  2. @Setter

  3. @ToString

  4. @EqualsAndHashCode

  5. @Data

Understanding these annotations helps developers write cleaner DTO classes while still being aware of what code Lombok actually generates.


1. @Getter

The @Getter annotation generates getter methods for fields.

1.1. Example

import lombok.Getter;

@Getter
class Person {
    private final UUID id;
    private String name;
    private int age;
}

Lombok automatically generates:

public UUID getId() {
    return this.id;
}

public String getName() {
    return this.name;
}

public int getAge() {
    return this.age;
}

1.2. Usage Notes

  • Can be applied to a class or to individual fields

  • Commonly used for DTOs and immutable objects

Example with field-level annotation:

@Getter
private String name;

2. @Setter

The @Setter annotation generates setter methods for fields.

2.1. Example

import lombok.Setter;

@Setter
class Person {
    private final UUID id;
    private String name;
    private int age;
}

Generated methods:

public void setName(String name) {
    this.name = name;
}

public void setAge(int age) {
    this.age = age;
}

Lombok does not generate setters for final fields because setters would require reassigning the field, which is prohibited by the final modifier.

2.2. Usage Notes

  • Can be applied to a class or individual fields

  • Often avoided in immutable classes


3. @ToString

The @ToString annotation generates a toString() method automatically.

3.1. Example

import lombok.ToString;

@ToString
class Person {
    private final UUID id;
    private String name;
    private int age;
}

Generated method conceptually looks like:

public String toString() {
    UUID var10000 = this.getId();
    return "Person(id=" + var10000 + ", name=" + this.getName() + ", age=" + this.getAge() + ")";
}

3.2. Usage Notes

Useful for debugging and logging.

Fields can also be excluded:

@ToString(exclude = "password")

4. @EqualsAndHashCode

The @EqualsAndHashCode annotation generates both equals() and hashCode() methods.

4.1. Why This Matters

Java’s default equals() method compares object references, meaning it only returns true when two references point to the same object.

In practice, we often override equals() to compare objects based on their field values instead. When doing so, we must also override hashCode() to maintain the Java contract: equal objects must produce the same hash code.

4.2. Example

import lombok.EqualsAndHashCode;

@EqualsAndHashCode
class Person {
    private final UUID id;
    private String name;
    private int age;
}
Lombok generates implementations like:
public boolean equals(Object o) {
    if (o == this) {
        return true;
    } else if (!(o instanceof Person)) {
        return false;
    } else {
        Person other = (Person)o;
        if (!other.canEqual(this)) {
            return false;
        } else if (this.getAge() != other.getAge()) {
            return false;
        } else {
            Object this$id = this.getId();
            Object other$id = other.getId();
            if (this$id == null) {
                if (other$id != null) {
                    return false;
                }
            } else if (!this\(id.equals(other\)id)) {
                return false;
            }

        Object this$name = this.getName();
        Object other$name = other.getName();
        if (this$name == null) {
            if (other$name != null) {
                return false;
            }
        } else if (!this\(name.equals(other\)name)) {
            return false;
        }

        return true;
    }
}

}

protected boolean canEqual(Object other) { return other instanceof Person; }

public int hashCode() { int PRIME = 59; int result = 1; result = result * 59 + this.getAge(); Object $id = this.getId(); result = result * 59 + (\(id == null ? 43 : \)id.hashCode()); Object $name = this.getName(); result = result * 59 + (\(name == null ? 43 : \)name.hashCode()); return result; }

4.3. Things to be aware of

In general, we should avoid using Lombok to generate the equals() and hashCode() methods for our JPA entities.

By default, Lombok generates equals() and hashCode() using all fields of the class. However, JPA entities often rely on a database identifier (@Id) to define identity. Before the entity is persisted, the identifier may be null, which can lead to inconsistent equality behavior.

In addition, including relationships or mutable fields in equals() and hashCode() can cause unexpected issues such as incorrect comparisons or performance problems.

For this reason, many JPA best practices recommend implementing equals() and hashCode() manually and basing them only on the entity’s identifier.


5. @Data

The @Data annotation is a shortcut annotation that bundles several Lombok features together.

5.1. What @Data Includes

@Data automatically generates:

  • @Getter

  • @Setter

  • @ToString

  • @EqualsAndHashCode

  • @RequiredArgsConstructor

5.2. Example

import lombok.Data;

@Data
class Person {
    private final UUID id;
    private String name;
    private int age;
}

Using @Data is convenient for simple DTO classes where all fields need standard methods.

Generated methods:
public class Person {
    private final UUID id;
    private String name;
    private int age;

public Person(UUID id) {
    this.id = id;
}

public UUID getId() {
    return this.id;
}

public String getName() {
    return this.name;
}

public int getAge() {
    return this.age;
}

public void setName(String name) {
    this.name = name;
}

public void setAge(int age) {
    this.age = age;
}

public boolean equals(Object o) {
    if (o == this) {
        return true;
    } else if (!(o instanceof Person)) {
        return false;
    } else {
        Person other = (Person)o;
        if (!other.canEqual(this)) {
            return false;
        } else if (this.getAge() != other.getAge()) {
            return false;
        } else {
            Object this$id = this.getId();
            Object other$id = other.getId();
            if (this$id == null) {
                if (other$id != null) {
                    return false;
                }
            } else if (!this\(id.equals(other\)id)) {
                return false;
            }

            Object this$name = this.getName();
            Object other$name = other.getName();
            if (this$name == null) {
                if (other$name != null) {
                    return false;
                }
            } else if (!this\(name.equals(other\)name)) {
                return false;
            }

            return true;
        }
    }
}

protected boolean canEqual(Object other) {
    return other instanceof Person;
}

public int hashCode() {
    int PRIME = 59;
    int result = 1;
    result = result * 59 + this.getAge();
    Object $id = this.getId();
    result = result * 59 + (\(id == null ? 43 : \)id.hashCode());
    Object $name = this.getName();
    result = result * 59 + (\(name == null ? 43 : \)name.hashCode());
    return result;
}

public String toString() {
    UUID var10000 = this.getId();
    return "Person(id=" + var10000 + ", name=" + this.getName() + ", age=" + this.getAge() + ")";
}

}

5.3. Things to be aware of

The inclusion of @RequiredArgsConstructor is important to understand. This annotation generates a constructor that accepts all required fields, which are:

  • final fields

  • fields annotated with @NonNull

For example, if a class contains a final field, Lombok generates a constructor that requires that field as a parameter.

public Person(UUID id) {
    this.id = id;
}

Because a constructor is now explicitly generated, the default no-argument constructor may no longer exist.

This can become important when working with frameworks such as JPA, Jackson, or Spring, which sometimes require a no-argument constructor. In such cases, developers often explicitly add @NoArgsConstructor alongside @Data.


6. When to Use Lombok Carefully

Although Lombok reduces boilerplate, it should be used thoughtfully.

Potential considerations include:

  • Generated methods are not visible in the source code

  • Debugging sometimes requires checking generated bytecode

  • Overusing @Data in domain models can expose unwanted setters

For example, immutable objects often prefer:

@Getter
@EqualsAndHashCode

instead of @Data.


✨ Conclusion

Lombok helps reduce repetitive Java boilerplate by generating common methods automatically. Understanding what each annotation generates is important so that developers remain aware of the actual behavior of their classes. The most commonly used Lombok annotations include @Getter, @Setter, @ToString, @EqualsAndHashCode, and the convenience annotation @Data.


🔗 References

5 views