Lombok Deep Dive: Exploring Lesser-Used Lombok Annotations
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 aname
andage
field. - We also have a
PersonHelper
class which contains methods to perform actions related to aPerson
. - We annotate the
helper
field in thePerson
class with@Delegate
. This tells Lombok to generate all the public methods inPersonHelper
within thePerson
class. - So, without writing any additional code, the
Person
class will havesayHello()
andsayAge(int age)
methods fromPersonHelper
.
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:
- We import
@Cleanup
from Lombok and standard Java IO classes. - We define a method
copyFile
that takes the source file and destination file paths as arguments. - Inside the
copyFile
method:
- We declare
FileInputStream
andFileOutputStream
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 typeString
andage
of typeint
. - 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 theequals()
method that compares the object's fields for equality.hashCode()
: An implementation of thehashCode()
method that generates a hash code based on the object's fields.toString()
: AtoString()
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
, orpackage-private
) for fields in the class.
Let’s understand how @FieldDefaults
works 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