Overview

Today I want to share the Java framework “Immutables” with you. Immutables generate simple, safe and consistent value objects for you. Thanks to Immutables, you don’t need to implement hashcode, equals, toString anymore. After reading this article, you will understand:

  • How to use Immutables in Maven project
  • How to create a value class using Immutables
  • How to create an instance
  • How to modify an instance
  • Support for optional
  • Support for collection
  • How to integrate with Jackson for JSON serialization
  • How to go further on this topic

Let’s get started!

Prerequisites

Declare the following dependency in your Maven project:

<dependency>
  <groupId>org.immutables</groupId>
  <artifactId>value</artifactId>
  <version>2.8.2</version>
  <scope>provided</scope>
</dependency>

In Maven, declaring a dependency as “provided” means this dependency is only for compilation and won’t be required at runtime. This is the case for Immutables because it is only used for generating the immutables classes during compilation.

Create Value Class

Once the dependency is added, you can create your value class now. This can be done by declaring an interface or abstract class with your desired accessor methods. For example, creating a User class with name, emails, and an optional description can be done as follows:

package io.mincong.immutables;

import java.util.Optional;
import java.util.Set;
import org.immutables.value.Value;

@Value.Immutable
public interface User {

  String name();

  Set<String> emails();

  Optional<String> description();

}

Since we declare the annotation @Value.Immutable in the interface, Immutables will recognize this class as value class and generate an immutable implementation using an annotation processor during compilation. The generated class will be located in the same package “io.mincong.immutables” with prefix “Immutable*”, i.e. io.mincong.immutables.ImmutableUser. The naming convention is:

Immutable${MyClass}

Now, you can use it as:

var user =
    ImmutableUser.builder()
        .name("Tom")
        .emails(List.of("tom@foo.com", "tom@bar.com"))
        .description("Welcome to Immutables")
        .build();
// User{name=Tom, emails=[tom@foo.com, tom@bar.com], description=Welcome to Immutables}

By the way, you cannot provide null as reference by default. When giving null to builder, it will raise a null pointer exception:

java.lang.NullPointerException: name

Therefore, once the object is created by Immutables, you know that you are safe to retrieve any field. You don’t need to worry about about null.

Modify An Instance

The objects created by Immutables are immutable, you cannot modify them. The fields are read-only. However, you can create a new object based on the existing one, either using the factory methods “with*” or using a builder.

// Create a new object using method "with{Field}"
var user2 = user.withName("Thomas");
// User{name=Thomas, emails=[tom@foo.com, tom@bar.com], description=Welcome to Immutables}
// Create a new object using builder
var user2 = ImmutableUser.builder().from(user).name("Thomas").build();
// User{name=Thomas, emails=[tom@foo.com, tom@bar.com], description=Welcome to Immutables}

The first approach is handy for changing one or two fields. The second approach is handy for changing more fields.

Benefits

Before going further, let’s discuss what are the benefits of using Immutables we discovered so far. There are several points: generated methods, immutability, and null-safety.

Generated methods. Let’s talk about generated equals, generated hash-code, and generated to-string. Methods equals() and hashCode() is generated by Immutable, so that you don’t have to handle them yourself. It means that whenever a field is added, changed, or deleted, the implementation of equals and hashCode are generated again at the next compilation. It keeps the equals and hashCode consistent and up-to-date. This is the same for toString() method. Also, delegating the implementation to Immutables increase to readability: there is no boilerplate methods stored in your source code.

Immutable. All the fields are immutable, regardless they are primitives, objects, or collections. Immutable objects are always in a consistent state and can be safely shared. They are thread-safe. This is particularly useful when writing high-concurrency applications or storing values in the cache.

Null-safe. Immutables check the mandatory attributes for you and fail the validation during creation-time. So there is no worry at read-time. For nullable objects, Immutables also provides supports for it, e.g. using Optional.

Builder

Now, let’s continue our exploration of Immutables on the builder side. Behind the screen, Immutables processor creates a builder for each value class, such as ImmutableUser.Builder for our value class User. Builder class is very powerful, here are some features that I want to discuss: support for collection, support for optional.

For collection objects, such as Set or List, Immutable builder provides several methods to help you manage them (see code snippet below). Thanks to these methods, it is easy to set the value for a collection in one call or do it incrementally. And having two overloaded methods with interface Iterable<T> and varargs T... makes it possible to fill the values with almost all kinds of collections and array.

Builder#emails(Iterable<String> elements)
Builder#addAllEmails(Iterable<String> elements)
Builder#addEmails(String element)
Builder#addEmails(String... elements)

For optional objects, such as Optional<String> declared in your value class, it creates two overloaded methods for you in the builder, one accepts an optional and the other accepts a normal String:

Builder#description(String description)
Builder#description(Optional<String> description)

I won’t cover more features here. If you were interested, you can go to Immutables’ user guide, there are “strict builder”, “staged builder”, etc.

Jackson Support

In the real-world, working with value classes in Java often means exchanging information with REST APIs and databases. One popular exchange format is JSON. We can see it everywhere: REST APIs, Elastichsearch, MongoDB, … Therefore, it’s important to know how immutables can support it. Here I’m taking Jackson as an example because it is one of the most popular frameworks for JSON serialization in the Java ecosystem.

Overall Jackson does not require any serious code generation to be flexible and highly performant on the JVM. Using the classical Jackson dependencies (annotations, core, databind) and the already-included dependency of Immutables (org.immutables:value:2.8.3), you are ready for the JSON serialization. In your value class, add annotations @JsonSerialize and @JsonDeserialize to delegate the serialization and deserialization to Immutables. If the JSON property is the same as your Java field, you can omit the explicit @JsonProperty. Otherwise, you need to specify it for the field mapping:

 @Value.Immutable
+@JsonSerialize(as = ImmutableAddress.class)
+@JsonDeserialize(as = ImmutableAddress.class)
 public interface Address {

   String address();

   String city();

+  @JsonProperty("zipcode")
   String postalCode();

 }

Then, use it as:

ObjectMapper mapper = new ObjectMapper();
var elysee =
    ImmutableAddress.builder()
        .address("55 Rue du Faubourg Saint-Honoré")
        .city("Paris")
        .postalCode("75008")
        .build();
var json = mapper.writeValueAsString(elysee);
{
  "address": "55 Rue du Faubourg Saint-Honoré",
  "city": "Paris",
  "zipcode": "75008"
}

Note that this is not the only way to configure Immutables for Jackson. Other ways can be reached here in the official documentation about JSON. There, you can also find support for other frameworks for JSON serialization.

Going Further

How to go further from here?

If you want to see the source code of this blog, you can find them in my GitHub project mincong-h/java-examples.

Conclusion

In this article, we saw how to use Immutables in Maven project, including how to create value Immutables annotations and use the implementation generated; how to modify an instance using factory methods “with*” or builder; the main benefits of using Immutables (generated equals, hashCode, toString), immutable and null-safe implementation; optional and collection support in builder class; JSON support with Jackson; and how to go further in this topic. Interested to know more? You can subscribe to the feed of my blog, follow me on Twitter or GitHub. Hope you enjoy this article, see you the next time!

References