Lombok Deep Dive: Exploring Lesser-Used Lombok Annotations

Srikanth Dannarapu
Javarevisited
Published in
7 min readFeb 13, 2024

Introduction:

In the world of Java development, writing clean, concise, and maintainable code is paramount. However, achieving this goal often involves writing tedious boilerplate code that detracts from the clarity and readability of our programs. Enter Lombok: a library that seeks to alleviate this burden by automatically generating common code structures, reducing verbosity, and enhancing developer productivity.

While many Java developers are familiar with Lombok’s popular annotations like @Getter, @Setter, and @ToString, there exists a treasure trove of lesser-known annotations that can further streamline our development workflow. In this post, we'll embark on a journey to explore these lombok annotations, uncovering advanced features, and unlocking the full potential of Lombok.

@Delegate:

Consider a scenario where you have a class Person and another class PersonHelper that provides various utility methods for working with Person objects. Instead of manually creating methods in the Person class to delegate calls to the PersonHelper, you can use Lombok's @Delegate annotation to automatically generate these methods for you.

An example here shows how you can use @Delegate:

import lombok.experimental.Delegate;

public class Person {
@Delegate
private final PersonHelper helper = new PersonHelper();

private String name;
private int age;

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

// Other custom methods for the Person class
}

class PersonHelper {
public void sayHello() {
System.out.println("Hello!");
}

public void sayAge(int age) {
System.out.println("I am " + age + " years old.");
}
}

In this example:

  • We have a Person class with a name and age field.
  • We also have a PersonHelper class which contains methods to perform actions related to a Person.
  • We annotate the helper field in the Person class with @Delegate. This tells Lombok to generate all the public methods in PersonHelper within the Person class.
  • So, without writing any additional code, the Person class will have sayHello() and sayAge(int age) methods from PersonHelper.

You can use the Person class as shown here:

public class Main {
public static void main(String[] args) {
Person person = new Person("John", 30);

person.sayHello(); // Delegated call to PersonHelper's sayHello()
person.sayAge(person.getAge()); // Delegated call to PersonHelper's sayAge()
}
}

This approach helps in keeping the Person class clean and focused on its main responsibilities, while still allowing it to access functionality provided by PersonHelper without explicitly implementing it. It's especially useful when you have a utility class with a set of methods that you want to expose through another class without writing wrapper methods manually.

@Cleanup:

The @Cleanup annotation in Lombok helps with automatic resource cleanup in Java, particularly with resources that need to be explicitly closed, like streams, database connections, or files. It ensures that resources are properly released, even in the presence of exceptions, without the need for explicit finally blocks.

Let’s illustrate the @Cleanup annotation with an example of copying a file.

import lombok.Cleanup;

import java.io.*;

public class FileCopyExample {

public static void main(String[] args) throws IOException {
String sourceFile = "source.txt";
String destFile = "destination.txt";
copyFile(sourceFile, destFile);
}

public static void copyFile(String sourceFile, String destFile) throws IOException {
@Cleanup FileInputStream in = new FileInputStream(sourceFile); // Automatically closed after leaving scope
@Cleanup FileOutputStream out = new FileOutputStream(destFile); // Automatically closed after leaving scope

byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
}

In this example:

  1. We import @Cleanup from Lombok and standard Java IO classes.
  2. We define a method copyFile that takes the source file and destination file paths as arguments.
  3. Inside the copyFile method:
  • We declare FileInputStream and FileOutputStream variables, annotated with @Cleanup. These variables will be automatically closed when they go out of scope.
  • We create a buffer to read and write the contents of the file.
  • We read from the input stream and write to the output stream until the end of the file is reached.

4. In the main method, we call copyFile with source and destination file paths.

With the @Cleanup annotation, we don't need to manually close the streams using a finally block. Lombok takes care of adding the necessary cleanup code behind the scenes, making our code cleaner and less error-prone.

It’s worth noting that @Cleanup is not limited to just streams; it can be used with any resource that implements the java.lang.AutoCloseable interface, which includes many IO and JDBC classes. However, it's essential to be cautious when using @Cleanup with resources that might throw exceptions during closing, as Lombok will silently swallow these exceptions by default. You can customize the behavior by providing a parameter to @Cleanup, but it's essential to handle such cases appropriately.

@Value:

The @Value annotation in Lombok is used to create immutable value objects in Java. Immutable objects are objects whose state (the object's data) cannot be modified after the object is created. They offer several advantages, such as thread safety, ease of reasoning about code, and resistance to bugs caused by unintended modifications.

Let’s understand how @Value works with an example:

import lombok.Value;

@Value
public class ImmutablePerson {
String name;
int age;
}

In this example:

  • We have a class ImmutablePerson annotated with @Value.
  • We define two fields: name of type String and age of type int.
  • The @Value annotation generates several things for us:
  • Constructor: A constructor that takes parameters for all fields and initializes the object.
  • Getters: Getter methods for all fields.
  • equals(): An implementation of the equals() method that compares the object's fields for equality.
  • hashCode(): An implementation of the hashCode() method that generates a hash code based on the object's fields.
  • toString(): A toString() method that returns a string representation of the object's state.

With the @Value annotation, our ImmutablePerson class becomes effectively immutable. Once an ImmutablePerson object is created, its state cannot be modified. Let's see how we can use it:

public class Main {
public static void main(String[] args) {
ImmutablePerson person = new ImmutablePerson("John", 30);

System.out.println(person.getName()); // Output: John
System.out.println(person.getAge()); // Output: 30

// ImmutablePerson objects cannot be modified
// person.setAge(31); // Compilation error: Cannot assign a value to final variable 'age'
}
}

In the Main class, we create an ImmutablePerson object with the name "John" and age 30. We then use the getter methods generated by Lombok to access the object's fields.

One thing to note is that Lombok automatically makes the fields final in the generated constructor, making them immutable. This means that once the fields are set during object creation, they cannot be changed.

Overall, the @Value annotation in Lombok simplifies the creation of immutable value objects by reducing boilerplate code, making your code cleaner and more concise while ensuring immutability and thread safety.

@Wither:

  • The @Wither annotation generates methods to create a new instance of the class with one or more fields changed. It's similar to an immutable setter.
  • It’s particularly useful when working with immutable objects, as it allows you to create modified copies of objects without altering the original.
  • Let’s understand how @Wither works with an example:
import lombok.Value;
import lombok.experimental.Wither;

@Value
public class ImmutablePerson {
String name;
int age;

@Wither
int age; // Generates withAge(int age) method
}

Usage:

ImmutablePerson person = new ImmutablePerson("John", 30);
ImmutablePerson modifiedPerson = person.withAge(31);

In this example, the @Wither annotation on the age field generates a withAge(int age) method in the ImmutablePerson class. When invoked, this method returns a new ImmutablePerson object with the specified age, leaving the original object unchanged.

@SneakyThrows:

  • The @SneakyThrows annotation allows you to throw checked exceptions without declaring them in the method signature or handling them explicitly.
  • It’s useful for reducing boilerplate code when working with methods that throw checked exceptions.
  • Let’s understand how @Wither works with an example:
import lombok.SneakyThrows;

public class Example {
@SneakyThrows
public void riskyMethod() {
throw new InterruptedException();
}
}

In this example, the riskyMethod() throws an InterruptedException without explicitly declaring it in the method signature or catching it within the method. Lombok's @SneakyThrows annotation handles the checked exception for us, allowing us to write concise code without cluttering it with exception handling.

@FieldDefaults:

  • The @FieldDefaults annotation can be used to control the access level of fields in a class.
  • It allows you to specify the default access level (private, protected, public, or package-private) for fields in the class.

Let’s understand how @FieldDefaultsworks with an example:

import lombok.AccessLevel;
import lombok.experimental.FieldDefaults;

@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class Example {
String name;
int age;
}

In this example, all fields in the Example class will have private access level by default and will be initialized with default values.

@Builder.Default:

  • The @Builder.Default annotation is used in conjunction with the @Builder annotation to provide default values for fields in the builder pattern.
  • It allows you to specify default values for fields that are not explicitly set during object creation using the builder.

Example:

import lombok.Builder;
import lombok.Builder.Default;

@Builder
public class Example {
@Default
String name = "John";
int age;
}

In this example, the name field will have a default value of "John" if not explicitly set during object creation using the builder.

@Builder:

  • The @Builder annotation generates a builder pattern for your class, allowing for fluent and expressive object creation.
  • It generates a builder class with methods to set each field in the annotated class.

Example:

import lombok.Builder;
import lombok.Getter;

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

usage:

Person person = Person.builder()
.name("John")
.age(30)
.build();

Thanks, before you go:

  • 👏 Please clap for the story and follow the author 👉
  • Please share your questions or insights in the comments section below. Let’s help each other and become better Java developers.
  • Let’s connect on LinkedIn

--

--