Introduction to the Immutables library

The Immutables library is a very powerful and interesting library, focused on easily creating immutable object classes with few annotations. In this introduction to the Immutables library we’ll show the basic features of this library and how to get the most out of them.

The project

We are going to create a simple maven project with just two dependencies:

  • the immutables library
  • junit5 (for testing purposes)

We won’t use any frameworks in this demo. Just a plain Java Maven project.

The model

Instead of creating a POJO with fields, getters, setters and so on, all we need to do is create an abstract class that defines our domain object’s getter methods. Then we’ll annotate the class with @Value.Immutable.

The annotation will create for us an immutable final class called ImmutableEmployee with a private all-args constructor and a nested builder class. The builder’s methods will match the fields’ names, derived from the getters according with the typical JavaBean naming convention.

Now we can run our unit test from the IDE to test our generated class and see that the test is successful.

Anyway, before running the test your IDE might give you a compilation error like the one in the picture below. The reason is simple: the ImmutableEmployee class will only be generated in the compile phase of the build lifecycle. In other words, we wrote code that includes a not yet existent class.

If you do mvn clean test your test will be green. After that, the compilation error might have gone away. As you can see, there’s now an ImmutableEmployee class in the target/generated-sources folder. If you are still getting the compilation error in your IDE, you need to activate annotation processing. In IntelliJ Idea all you need to do is:

  • open the Settings menu (CTRL+ALT+S)
  • go to submenu Build, Execution, Deployment -> Compiler -> Annotation Processors
  • tick the Enable annotation processing checkbox
  • click OK
    The folder target/generated-sources/annotations should now be recognized by your IDE as a generated sources root folder. If that’s not the case, you can right click the folder, then click Mark Directory as -> Generated Sources Root.

The downside of the Immutables library is that, in order to benefit from code autocompletion in your IDE, you’ll need to recompile your project after creating a class annotated with @Value.Immutable.

Adding a field

Now if we add an id field to our Employee class and we don’t modify our unit test, we might think that the test will still pass.

The id field is not set via builder so it will be null, and our test case contains no assertion requiring that field to be non-null. Anyway our test will fail, since by default all fields are mandatory. When we call the build() method it will throw an IllegalStateException, and the exception message will inform us which required fields are null.

java.lang.IllegalStateException: Cannot build Employee, some of required attributes are not set [id]

    at com.codemadeclear.immutables.demo.model.ImmutableEmployee$Builder.build(ImmutableEmployee.java:283)
    at com.codemadeclear.immutables.demo.model.EmployeeTest.generatedImmutableClassShouldHaveBuilder(EmployeeTest.java:24)
…

This is indeed a very useful feature. When we add a new field to a model class we must update all our unit tests where we create a new instance of that class. If we don’t, the tests will fail.

Nullable and optional fields

To turn a field into a non-mandatory field, i.e. to allow null values for a field, just add a @Nullable annotation on the corresponding getter method. Any annotation will do as long as its simple name is @Nullable. A couple of examples (and the dependency that you might need to add to your pom.xml) are:

  • @org.springframework.lang.Nullable from org.springframework:spring-core
  • @org.jetbrains.annotations.Nullable from org.jetbrains:annotations
  • any custom @Nullable annotation that you can create (see below). This will spare you from adding dependencies to your pom.xml. Notice the RetentionPolicy.CLASS as the annotation will only be required at compile time and not at runtime.

Another possibility is to wrap a field in an Optional. In our example we´ve added a getSalesTarget() method that returns an Optional. In our unit test we don’t assign any value for salesTarget upon instantiation of our Employee and we don’t get any exception.

We can even update our unit test to assert how the creation of an Employee with a null id is now possible.

Now the unit test will pass.

Default values

The annotation @Value.Default placed on a non-abstract method marks that method’s return value as the default value for the relative field. This value will be set for the field upon instantiation of the immutable object if we don’t provide any value at all to the builder. In the following example we are providing a default value for the nationality field of our employee.

Collections

All fields of collection type don’t need an explicit initialization. By default the field will be an empty collection. In our unit test the fringeBenefits field is not added any element upon employee instantiation. Afterwards, when we call the getFringeBenefits() method we get an empty Set.

For each field of type collection, the following fields will be created by Immutables:

  • an addElements(Foo element) method that adds a single element to the collection
  • an addElements(Foo... elements) method that adds a vararg of elements to the collection
  • an addAllElements(Iterable<Foo> elements) method that adds all elements from an Iterable to the collection
  • an elements(Iterable<Foo> elements) method that clears the collection then adds all elements from an Iterable (i.e. a full replacement is performed)

In all cases collection fields cannot contain null elements. If we try to add a null element to a collection field, a NullPointerException will be thrown.

All the collection and map fields will always be immutable. An UnsupportedOperationException will be thrown when we try to add elements to the collection returned by a getter. Since the collection fields are immutable, the getter will return the collection itself. Obviously there’s no need to return a defensive copy.

Conclusion

We’ve made an introduction to the Immutables library and seen its very basic features. With a few annotations we can generate immutable objects and easily customize their behaviour writing a few lines of code. The upsides of using immutable objects are many, with the most significant ones being thread-safety and clarity of use (the fields are all set in one place, less chances of hard-to-detect errors). The only downside is the need to recompile your project any time you need to re-generate your immutable classes after making modifications. This makes the library a little awkward to use, but in my opinion it is not a big deal.

The example project is available for download on GitLab.