Java
Article

Beyond POJOs – Ten More Ways to Reduce Boilerplate with Lombok

By Ivan Mushketyk

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:

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 return this reference instead of void
  • fluent: Creates fluent interface. This names all getters and setters name instead of getName and setName. It also sets chain 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 like fName or getFName.

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.

Recommended
Sponsors
Get the latest in Java, once a week, for free.