1. Avoid Repetitive Code
Java is a great language, but it can sometimes get too verbose for common tasks we have to do in our code or compliance with some framework practices. This often doesn't bring any real value to the business side of our programs, and that's where Lombok comes in to make us more productive.
The way it works is by plugging into our build process and auto-generating Java bytecode into our .class files as per a number of project annotations we introduce in our code. Including it in our builds, in whichever system we're using, is very straight forward. Project Lombok's project page has detailed instructions on the specifics. Most of my projects are maven based, so I just typically drop their dependency in the provided scope and I'm good to go:
<dependencies>
...
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>provided</scope>
</dependency>
...
</dependencies>
We can check for the most recent available version here. Note that depending on Lombok won't make users of our .jars depend on it as well, as it is a pure build dependency, not runtime.
2. Getters/Setters, Constructors – So Repetitive
Encapsulating object properties via public getter and setter methods is such a common practice in the Java world, and lots of frameworks rely on this “Java Bean” pattern extensively (a class with an empty constructor and get/set methods for “properties”).
This is so common that most IDE's support auto-generating code for these patterns (and more). However, this code needs to live in our sources and be maintained when a new property is added or a field renamed.
Let's consider this class we want to use as a JPA entity:
@Entity
public class User implements Serializable {
private @Id Long id; // will be set when persisting
private String firstName;
private String lastName;
private int age;
public User() {
}
public User(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
// getters and setters: ~30 extra lines of code
}
This is a rather simple class, but imagine if we had added the extra code for getters and setters. We would have ended up with a definition where there would be more boilerplate zero-value code than the relevant business information: “a User has first and last names, and age.”
Let's now Lombok-ize this class:
@Entity
@Getter @Setter @NoArgsConstructor // <--- THIS is it
public class User implements Serializable {
private @Id Long id; // will be set when persisting
private String firstName;
private String lastName;
private int age;
public User(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
}
By adding the @Getter and @Setter annotations, we told Lombok to generate these for all the fields of the class. @NoArgsConstructor will lead to an empty constructor generation.
Note that this is the whole class code, we're not omitting anything unlike the version above with the // getters and setters comment. For a three relevant attributes class, this is a significant saving in code!
If we further add attributes (properties) to our User class, the same will happen; we apply the annotations to the type itself so they will mind all fields by default.
What if we want to refine the visibility of some properties? For example, if we want to keep our entities' id field modifiers package or protected visible because they are expected to be read, but not explicitly set by application code, we can just use a finer grained @Setter for this particular field:
private @Id @Setter(AccessLevel.PROTECTED) Long id;
3. Lazy Getter
For instance, let's say we need to read static data from a file or a database. It's generally good practice to retrieve this data once, and then cache it to allow in-memory reads within the application. This saves the application from repeating the expensive operation.
Another common pattern is to retrieve this data only when it's first needed. In other words, we only get the data when we call the corresponding getter the first time. We call this lazy-loading.
Let's suppose that this data is cached as a field inside a class. The class must now make sure that any access to this field returns the cached data. One possible way to implement such a class is to make the getter method retrieve the data only if the field is null. We call this a lazy getter.
Lombok makes this possible with the lazy parameter in the @Getter annotation we saw above.
For example, consider this simple class:
public class GetterLazy {
@Getter(lazy = true)
private final Map<String, Long> transactions = getTransactions();
private Map<String, Long> getTransactions() {
final Map<String, Long> cache = new HashMap<>();
List<String> txnRows = readTxnListFromFile();
txnRows.forEach(s -> {
String[] txnIdValueTuple = s.split(DELIMETER);
cache.put(txnIdValueTuple[0], Long.parseLong(txnIdValueTuple[1]));
});
return cache;
}
}
This reads some transactions from a file into a Map. Since the data in the file doesn't change, we'll cache it once and allow access via a getter.
If we now look at the compiled code of this class, we'll see a getter method which updates the cache if it was null and then returns the cached data:
public class GetterLazy {
private final AtomicReference<Object> transactions = new AtomicReference();
public GetterLazy() {
}
//other methods
public Map<String, Long> getTransactions() {
Object value = this.transactions.get();
if (value == null) {
synchronized(this.transactions) {
value = this.transactions.get();
if (value == null) {
Map<String, Long> actualValue = this.readTxnsFromFile();
value = actualValue == null ? this.transactions : actualValue;
this.transactions.set(value);
}
}
}
return (Map)((Map)(value == this.transactions ? null : value));
}
}
It's interesting to point out that Lombok wrapped the data field in an AtomicReference. This ensures atomic updates to the transactions field. The getTransactions() method also makes sure to read the file if transactions is null. We discourage using the AtomicReference transactions field directly from within the class. We recommend using the getTransactions() method for accessing the field.
For this reason, if we use another Lombok annotation like ToString in the same class, it will use getTransactions() instead of directly accessing the field.