Beyond POJOs – Ten More Ways to Reduce Boilerplate with Lombok
Table of Contents
Editor’s Note: At SitePoint we are always looking for authors who know their stuff. If you’re an experienced Java developer and would like to share your knowledge, why not write for us?
Lombok is a great library and its main selling point is how it declutters POJO definitions. But it is not limited to that use case! In this article, I will show you six stable and four experimental Lombok features that can make your Java code even cleaner. They cover many different topics, from logging to accessors and from null safety to utility classes. But they all have one thing in common: reducing boilerplate to make code easier to read and more expressive.
Logging Annotations
How many times have you copied a logger definition from one class to another and forgot to change the class name?
public class LogTest {
private static final Logger log =
LoggerFactory.getLogger(NotTheLogTest.class);
public void foo() {
log.info("Info message");
}
}
To ensure that this never happens to you again Lombok has several annotations that allow you to easily define a logger instance in your class. So instead of writing code like above you can use Lombok’s annotations to remove boilerplate and be sure the logger belongs to the right class:
@Slf4j
public class LogTest {
public void foo() {
log.info("Info message");
}
}
Depending on your logging framework of choice, you can use one of the following annotations:
@Log4J
– uses Log4j framework@Log4J2
– uses Log4j2 framework@Slf4J
– uses SLF4J framework@CommonsLog
– uses Apache Commons Logging@JBossLog
– uses JBoss Logging@Log
– uses standard Java util logger@XSlf4j
– uses extended SLF4J logger
Lazy Getters
Another cool Lombok feature is the support for lazy initialization. Lazy initialization is an optimization technique that is used to delay a costly field initialization. To implement it correctly, you need to defend your initialization against race conditions, which results in code that is hard to understand and easy to screw up. With Lombok, you can just annotate a field with @Getter(lazy = true)
.
To use this annotation, we need to define a private and final field and assign a result of a function call to it:
public class DeepThought {
@Getter(lazy = true)
private final String theAnswer = calculateTheUltimateAnswer();
public DeepThought() {
System.out.println("Building DeepThought");
}
// This function won't be called during instance creation
private String calculateTheUltimateAnswer() {
System.out.println("Thinking for 7.5 million years");
return "42";
}
}
If we create an instance of this class, the value won’t be calculated. Instead theAnswer
is only initialized when we access it for the first time. Assume we use the DeepThought
class as follows:
DeepThought deepThought = new DeepThought();
System.out.println("DeepThought is ready");
deepThought.getTheAnswer();
Then we will receive the following output:
Building DeepThought
DeepThought is ready
Thinking for 7.5 million years
As you can see, the value of the ultimate answer is not calculated during the object initialization but only when we access its value for the first time.
Safer synchronized
When Java’s synchronized
keyword is applied on the method level, it synchronizes a particular method using the this
reference. Using synchronized
this way may be convenient, but nothing prevents users of your class from acquiring the same lock and shooting themselves in the foot by messing your carefully designed locking strategy up.
The common pattern to prevent that from happening is to create a private field specifically for locks and synchronize on the lock
object instead:
public class Foo {
private final Object lock = new Object();
public void foo() {
synchronized(lock) {
// ...
}
}
}
But this way, you cannot use the synchronized
keyword on the method level, and this does not make your code clearer.
For this case, Lombok provides the @Synchronized
annotation. It can be used similarly to the synchronized
keyword, but instead of using the this
reference, it creates a private field and synchronizes on it:
public class Foo {
// synchronized on a generated private field
@Synchronized
public void foo() {
// ...
}
}
If you need to synchronize different methods on different locks, you can provide a name of a lock object to the @Synchronized
annotation but in this case you need to define locks yourself:
public class Foo {
// lock to synchronize on
private Object barLock = new Object();
@Synchronized("barLock")
public void bar() {
// ...
}
}
In this case, Lombok will synchronize bar
method on the barLock
object.
Null Checks
Another source of boilerplate in Java are null checks. To prevent fields from being null, you might write code like this:
public class User {
private String name;
private String surname;
public User(String name, String surname) {
if (name == null) {
throw new NullPointerException("name");
}
if (surname == null) {
throw new NullPointerException("surname");
}
this.name = name;
this.surname = surname;
}
public void setName(String name) {
if (name == null) {
throw new NullPointerException("name");
}
this.name = name;
}
public void setSurname(String surname) {
if (surname == null) {
throw new NullPointerException("surname");
}
this.surname = surname;
}
}
To make this hassle-free, you can use Lombok’s @NotNull
annotation. If you mark a field, method, or constructor argument with it Lombok will automatically generate the null-checking code for you.
@Data
@Builder
public class User {
@NonNull
private String name;
@NonNull
private String surname;
private int age;
public void setNameAndSurname(@NonNull String name, @NonNull String surname) {
this.name = name;
this.surname = surname;
}
}
If @NonNull
is applied on a field, Lombok will add a null-check in both a setter and a constructor. Also, you can apply @NonNull
not only on class fields but on method arguments as well.
As a result, every line of the following snippet will raise a NullPointerException
:
User user = new User("John", null, 23);
user.setSurname(null);
user.setNameAndSurname(null, "Doe");
Type Inference with val
It may be quite radical, but Lombok allows you to add a Scala-like construct to your Java code. Instead of typing a name of a type when creating a new local variable, you can simply write val
. Just like in Scala, the variable type will be deducted from the right side of the assignment operator and the variable will be defined as final.
import lombok.val;
val list = new ArrayList<String>();
list.add("foo");
for (val item : list) {
System.out.println(item);
}
If you deem this to be very un-Java-like, you might want to get used to it as it is entirely possible that Java will have a similar keyword in the future.
@SneakyThrows
@SneakyThrows
makes the unthinkable possible: It allows you to throw checked exceptions without using the throws
declaration. Depending on your world-view, this either fixes Java’s worst design decision or opens Pandora’s Box smack in the middle of your code base.
@SneakyThrows
comes in handy if, for example, you need to raise an exception from a method with a very restrictive interface like Runnable
:
public class SneakyRunnable implements Runnable {
@SneakyThrows(InterruptedException.class)
public void run() {
throw new InterruptedException();
}
}
This code compiles and if you execute the run
method it will throw the Exception
instance. There is no need to wrap it in a RuntimeException
as you might otherwise do.
A drawback of this annotation is that you cannot catch a checked exception that is not declared. The following code will not compile:
try {
new SneakyRunnable().run();
} catch (InterruptedException ex) {
// javac: exception java.lang.InterruptedException
// is never thrown in body of corresponding try statement
System.out.println(ex);
}
Experimental Features
In addition to the stable features that we have seen so far, Lombok also has a set of experimental features. If you want to squeeze as much as you can from Lombok feel free to use them but you need to understand the risks. These features may be promoted in one of the upcoming releases, but they can also be removed in future minor versions. API of experimental features can also change and you may have to work on updating your code.
A separate article could be written just about experimental features, however here I’ll cover only features that have a high chance to be promoted to stable with minor or no changes.
Extensions Methods
Almost every Java project has so-called utility classes – classes that contain only static methods. Often, their authors would prefer the methods were part of the interface they relate to. Methods in a StringUtils
class, for example, operate on strings and it would be nice if they could be called directly on String
instances.
public class StringUtils {
// would be nice to call "abc".isNullOrEmpty()
// instead of StringUtils.isNullOrEmpty("abc")
public static boolean isNullOrEmpty(String str) {
return str == null || str.isEmpty();
}
}
Lombok has an annotation for this use case, inspired by other language’s extension methods (for example in Scala). It adds methods from a utility class to an object’s interface. So all we need to do to add isNullOrEmpty
to the String
interface is to pass the class that defines it to the @ExtensionMethod
annotation. Each static method, in this case, is added to the class of its first argument:
@ExtensionMethod({StringUtils.class})
public class App {
public static void main(String[] args) {
String s = null;
String s2 = "str";
s.isNullOrEmpty(); // returns "true"
s2.isNullOrEmpty(); // returns "false";
}
}
We can also use this annotation with built-in utility classes like Arrays
:
@ExtensionMethod({Arrays.class})
public class App {
public static void main(String[] args) {
String[] arr = new String[] {"foo", "bar", "baz"};
List<String> list = arr.asList();
}
}
This will only have an effect inside the annotated class, in this example App
.
Utility Class Constructors
Talking about utility classes… An important thing to remember about them is that we need to communicate that this class should not be instantiated. A common way to do this is to create a private constructor that throws an exception (in case somebody is using reflection to invoke it):
public class StringUtils {
private StringUtils() {
throw new UnsupportedOperationException(
"Utility class should not be instantiated");
}
public static boolean isNullOrEmpty(String str) {
return str == null || str.isEmpty();
}
}
This constructor is distracting and cumbersome. Fortunately, Lombok has an annotation that generates it for us:
@UtilityClass
class StringUtils {
public static boolean isNullOrEmpty(String str) {
return str == null || str.isEmpty();
}
}
Now we can call methods on this utility class as before and we cannot instantiate it:
StringUtils.isNullOrEmpty("str"); // returns "false"
// javac: StringUtils() has private access in lomboktest.StringUtils
new StringUtils();
Flexible @Accessors
This feature does not work on its own but is used to configure how @Getter
and @Setter
annotations generate new methods. It has three flags that configure its behavior:
chain
: Makes setters returnthis
reference instead ofvoid
fluent
: Creates fluent interface. This names all getters and settersname
instead ofgetName
andsetName
. It also setschain
to true unless specified otherwise.prefix
: Some developers prefer to start field names with a prefix like “f”. This annotation allows to drop the specified prefix from getters and setters to avoid method names likefName
orgetFName
.
Here is an example of a class with a fluent interface where all fields have the “f” prefix:
@Accessors(fluent = true, prefix = "f")
@Getter
@Setter
class Person {
private String fName;
private int fAge;
}
And this is how you can use it:
Person person = new Person()
.name("John")
.age(34);
System.out.println(person.name());
Field Defaults
It is not uncommon to see a class where all fields have the same set of modifiers. It’s annoying to read through them and even more annoying to type them again and again:
public class Person {
private final String name;
private final int age;
// constructor and getters
}
To help with this Lombok has an experimental @FieldDefaults
annotation, which defines modifiers that should be added to all fields in a class. The following example makes all fields public and final:
@FieldDefaults(level = AccessLevel.PUBLIC, makeFinal = true)
@AllArgsConstructor
public class Person {
String name;
int age;
}
As a consequence, you can access name
from outside the package:
Person person = new Person("John", 34);
System.out.println(person.name);
If you have few fields that should be defined with different annotations, you can redefine them on the field level:
@FieldDefaults(level = AccessLevel.PUBLIC, makeFinal = true)
@AllArgsConstructor
public class Person {
// Make this field package private
@PackagePrivate String name;
// Make this field non final
@NonFinal int age;
}
Conclusions
Lombok offers a broad range of tools that can save you from thousands of lines of boilerplate and make your applications more concise and succinct. The main issue is that some of your team members may disagree about what features to use and when to use them. Annotations like @Log
are unlikely to cause major disagreements while val
and @SneakyThrows
may have many opponents. Before you start using Lombok, I would suggest coming to an agreement on how are you going to use it.
In any case keep in mind that all Lombok annotations can be easily converted into vanilla Java code using the delombok
command.
If Lombok intrigued you and you want to learn how it works its magic or want to start using it right away, read my Lombok tutorial.