Boosting productivity in Java: An introduction to Lombok

23 mei 2024 - Jordy Pannemans

As One teambuilding jeu de boules 2023

As a developer, I’m always excited to find tools or libraries that simplify my work, clean up my code or reduce the amount of code needed. Lombok is such a library which improves the readability of my code by reducing boilerplate code. In this blogpost, we’ll scratch the surface of what Lombok is and how it can be used in Java projects.

What is Lombok?

Lombok is a Java library that eliminates boilerplate code by generating common code constructs during compilation. This is done using annotations to generate getters, setters, constructors, toString methods, and more. Therefore, it’s useful when you want to speed up development and improve readability inside your POJO’s.

Getters and Setters

The two annotations that I use the most are @Getter and @Setter. You can set them at field level to generate getters and setters.

public class Person {
    @Getter
    @Setter
    private String name;

    @Getter
    @Setter
    private int age;
} 

Compiled to:

public class Person {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

You can also add these annotations at class level (my preference) to generate getters and setters for all fields of that class. This tightens your code even more. If for example, there are fields that would need no getter or setter method, then you can exclude these fields by specifying the access level.

@Getter
@Setter
public class GetterSetterExample {
    private String name;
    @Getter(AccessLevel.NONE)
    @Setter(AccessLevel.NONE)
    private int age;
}

Compiled to:

public class GetterSetterExample {
    private String name;
    private int age;

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

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

When most of your fields don't need a getter or setter, then it’s preferable to annotate the class fields that do need them. This requires less code and is better for readability.

Constructors

Lombok also provides three annotations to generate constructors to take away the repetitive task of assigning each argument to its corresponding class attribute. The annotation @NoArgsConstructor creates a constructor with no parameters. And @AllArgsConstructor creates one that has a parameter for each field in your class.

@NoArgsConstructor
@AllArgsConstructor
public class AllArgsNoArgsConstructorsExample {
    private String name;
    private int age;
}

Compiles to:

public class AllArgsNoArgsConstructorsExample {
    private String name;
    private int age;

    public AllArgsNoArgsConstructorsExample() {

    }

    public AllArgsNoArgsConstructorsExample(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

Lastly, there’s @RequiredArgsConstructor which adds a constructor that only takes the required (final) attributes as parameters.

@RequiredArgsConstructor
public class RequiredArgsConstructorExample {
    private final String name;
    private int age;
}

Compiles to:

public class RequiredArgsConstructorExample {
    private final String name;
    private int age;

    public RequiredArgsConstructorExample(String name) {
        this.name = name;
    }
}

toString

Even toString methods can be generated with the @ToString annotation. This eliminates the need for manual implementation and allows developers to inspect objects during debugging or logging.

@ToString
public class ToStringExample {
    private String name;
    private int age;
}

Compiles to:

public class ToStringExample {
    private String name;
    private int age;

    public String toString() {
        return "ToStringExample(name=" + this.name + ", age=" + this.age + ")";
    }
}

equals and hashCode

The next annotation we’ll look at is @EqualsAndHashCode. This annotation generates equals() and hashCode() methods. So, you don’t have to worry about the complexities of manual implementation of object comparison and hashing.

@EqualsAndHashCode
public class EqualsAndHashCodeExample {
    private String name;
    private int age;
}

Compiles to:

public class EqualsAndHashCodeExample {
    private String name;
    private int age;

    public boolean equals(final Object o) {
        if (o == this) return true;
        if (!(o instanceof EqualsAndHashCodeExample)) return false;
        EqualsAndHashCodeExample other = (EqualsAndHashCodeExample) o;
        if (!other.canEqual((Object) this)) return false;
        if (this.name == null ? other.name != null : !this.name.equals(other.name)) return false;
        if (this.age != other.age) return false;
        return true;
    }

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

    public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        result = result * PRIME + (this.name == null ? 43 : this.name.hashCode());
        result = result * PRIME + this.age;
        return result;
    }
}

Builder

If you decide to use the builder design pattern because of complex objects creation or your class containing a long list of constructors. Then you could utilize the @Builder annotation to make Lombok generate an inner builder class with methods to create objects with a fluent syntax.

@Builder
public class BuilderExample {
    private String name;
    private int age;
}

Compiles to:

public class BuilderExample {
    private String name;
    private int age;

    BuilderExample(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public static BuilderExampleBuilder builder() {
        return new BuilderExampleBuilder();
    }

    public static class BuilderExampleBuilder {
        private String name;
        private int age;

        BuilderExampleBuilder() {
        }

        public BuilderExampleBuilder name(String name) {
            this.name = name;
            return this;
        }

        public BuilderExampleBuilder age(int age) {
            this.age = age;
            return this;
        }

        public BuilderExample build() {
            return new BuilderExample(this.name, this.age);
        }

        public String toString() {
            return "BuilderExample.BuilderExampleBuilder(name=" + this.name + ", age=" + this.age + ")";
        }
    }
}

Another use case with @Builder is to use it on the constructor level. The difference is that it only uses the parameters of that constructor to generate the inner builder class.

Data

Lombok also provides an annotation that bundles several features with @Data. It will generate all the boilerplate code for simple POJOs. This contains @ToString, @EqualsAndHashCode, @RequiredArgsConstructor, @Getter for all fields and @Setter for all non-final fields.

@Data
public class DataExample {
    private String name;
    private int age;
}

Compiles to:

public class DataExample {
    private String name;
    private int age;

    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(final Object o) {
        if (o == this) return true;
        if (!(o instanceof DataExample)) return false;
        DataExample other = (DataExample) o;
        if (!other.canEqual((Object) this)) return false;
        if (this.getName() == null ? other.getName() != null : !this.getName().equals(other.getName())) return false;
        if (this.getAge() != other.getAge()) return false;
        return true;
    }

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

    public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        result = result * PRIME + (this.getName() == null ? 43 : this.getName().hashCode());
        result = result * PRIME + this.getAge();
        return result;
    }

    public String toString() {
        return "DataExample(name=" + this.getName() + ", age=" + this.getAge() + ")";
    }
}

Inheritance

Lombok can be used with inheritance by annotating both super- and subclass with the required annotations. The subclass can call the methods generated in its superclass by Lombok. But the subclass would still need its own annotations for the methods that need to be generated.


The only annotation that needs special care is @Builder. To use this in the subclass you must annotate any constructor (that you want a builder for) and use the builderMethodName property in the super- or subclass’s annotation. Otherwise, you will get a compilation error because the subclass then contains two builder() methods with different return types.


For example:

@Builder(builderMethodName = "superBuilder")
public class InheritanceSuperExample {
    private String name;
    private int age;
}
public class InheritanceChildExample extends InheritanceSuperExample {
    private String email;

    @Builder
    public InheritanceChildExample(String name, String email) {
        super(name);
        this.email = email;
    }
}

Which in the subclass compiles to:

public class InheritanceChildExample extends InheritanceSuperExample {
    private String email;

    public InheritanceChildExample(String name, String email) {
        super(name);
        this.email = email;
    }

    public static InheritanceChildExampleBuilder builder() {
        return new InheritanceChildExampleBuilder();
    }

    public static class InheritanceChildExampleBuilder {
        private String name;
        private String email;

        InheritanceChildExampleBuilder() {
        }

        public InheritanceChildExampleBuilder name(String name) {
            this.name = name;
            return this;
        }

        public InheritanceChildExampleBuilder email(String email) {
            this.email = email;
            return this;
        }

        public InheritanceChildExample build() {
            return new InheritanceChildExample(this.name, this.email);
        }

        public String toString() {
            return "InheritanceChildExample.InheritanceChildExampleBuilder(name=" + this.name + ", email=" + this.email + ")";
        }
    }
}

Conclusion

Project Lombok accelerates Java development by providing several annotations that reduce boilerplate code, thus allowing developers to spend more time focusing on implementations that are more complex and require more attention. By incorporating annotations such as @Getter, @Setter, @ToString, @EqualsAndHashCode, @Builder and the constructor annotations into your projects, you can improve productivity, code readability and maintainability.