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?
- Read the official get-started documentation of Immutables
https://immutables.github.io/getstarted.html - Read the official user guide to understand more concepts, features, and
patterns
https://immutables.github.io/immutable.html - Read the official JSON guide about JSON serialization with different
frameworks, such as Jackson, Gson
https://immutables.github.io/json.html - Compare different frameworks for value class generation
in Java ecosystem: Lombok, Auto Value, Immutables in DZone by Dustin Marx
https://dzone.com/articles/lombok-autovalue-and-immutables - Explore more projects based on Java annotation processing or find related
resource in general (video, blog posts, concepts)
https://github.com/gunnarmorling/awesome-annotation-processing
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
- Immutables authors, “Immutables”, Immutables, 2020. https://immutables.github.io