In the previous article, Immutability with Lombok builder pattern, we discovered how we can achieve immutable objects in a very easy and convenient way. In this article, we are going to look at an alternative immutability library called Immutables.
Immutability Library
The alternative immutability library, Immutables, is an open source project which is operating under the Apache License 2.0. It is quite an interesting project which aims to make it easier to create immutable objects by letting users create Java classes with the processing of annotations so that you don’t have to write unnecessary boilerplate code. Sort of like Lombok, but with a more focus on immutable objects as the name suggests.
Preparing your IDE
The processing of annotations generates source classes for us, which depending on your IDE needs to be handled. I ran into some problems here with IntelliJ, so I figured it would be a good idea to share how I set up my IntelliJ to be able to handle the Immutables API.
If you don’t use IntelliJ I recommend you to simply google how to enable it, Immutables should be supported in most of the popular IDE’s.
First of all, we need to enable the autoprocessing of annotations. You can do that by opening the settings for IntelliJ and searching for Annotation Processors, and there you will want to select your module and make sure that the checkbox for Enable annotation processing and Obtain processors from classpath is selected, like the picture below.
Additionally, as I mentioned, the annotations generates source classes for us, so we will need to make sure that the source directory is included marked as a source instead of excluded which it was for me. You can do that by going to the Project Structure menu, and going to modules, and press on generated-sources and make sure that it is marked as a Source, like the picture below.
And that’s it, now we should be ready to get going.
Getting started
It’s easy to get started with either Maven or Gradle. For maven simply add the following dependency.
org.immutables
value
2.5.6
Immutables supports two different ways of generating classes, either by using an interface or by using an abstract class. They are both very similar, and personally, I prefer to stick with interfaces. They will both be processed by annotations to generate boilerplate code for you.
Let’s start with a basic example, where we have 3 required fields and an optional one. Immutables are following good programming praxises added since Java 8 with preventing NullPointerException using java.util.Optional, check extending the builder pattern with null safety for more details. However, if you don’t like to use Optional for some reason, you can annotate an object with @Nullable
. If you don’t wrap the object in an Optional or annotate it as @Nullable
, then it will be treated as a required object and trigger an IllegalStateException during construction if the field is missing.
@Value.Immutable
public interface Person {
String getName();
String getCity();
int getAge();
List getChildren();
Optional getCar();
}
The same could be written as an abstract class if you instead would prefer that.
@Value.Immutable
public abstract class Person {
public abstract String getName();
public abstract String getCity();
public abstract int getAge();
public abstract List getChildren();
public abstract Optional getCar();
}
As mentioned, I prefer to use interfaces as it requires even less code. These classes will automatically generate a class that looks like the following.
@SuppressWarnings({"all"}) @Generated({"Immutables.generator", "Person"}) public final class ImmutablePerson implements Person { private final String name; private final @Nullable String city; private final int age; private final List
children; private final String car; private ImmutablePerson( String name, @Nullable String city, int age, List children, String car) { this.name = name; this.city = city; this.age = age; this.children = children; this.car = car; } /** * @return The value of the {@code name} attribute */ @Override public String getName() { return name; } /** * @return The value of the {@code city} attribute */ @Override public @Nullable String getCity() { return city; } /** * @return The value of the {@code age} attribute */ @Override public int getAge() { return age; } /** * @return The value of the {@code children} attribute */ @Override public List getChildren() { return children; } /** * @return The value of the {@code car} attribute */ @Override public Optional getCar() { return Optional.ofNullable(car); } /** * Copy the current immutable object by setting a value for the {@link Person#getName() name} attribute. * An equals check used to prevent copying of the same value by returning {@code this}. * @param value A new value for name * @return A modified copy of the {@code this} object */ public final ImmutablePerson withName(String value) { if (this.name.equals(value)) return this; String newValue = Objects.requireNonNull(value, "name"); return new ImmutablePerson(newValue, this.city, this.age, this.children, this.car); } /** * Copy the current immutable object by setting a value for the {@link Person#getCity() city} attribute. * An equals check used to prevent copying of the same value by returning {@code this}. * @param value A new value for city (can be {@code null}) * @return A modified copy of the {@code this} object */ public final ImmutablePerson withCity(@Nullable String value) { if (Objects.equals(this.city, value)) return this; return new ImmutablePerson(this.name, value, this.age, this.children, this.car); } /** * Copy the current immutable object by setting a value for the {@link Person#getAge() age} attribute. * A value equality check is used to prevent copying of the same value by returning {@code this}. * @param value A new value for age * @return A modified copy of the {@code this} object */ public final ImmutablePerson withAge(int value) { if (this.age == value) return this; return new ImmutablePerson(this.name, this.city, value, this.children, this.car); } /** * Copy the current immutable object with elements that replace the content of {@link Person#getChildren() children}. * @param elements The elements to set * @return A modified copy of {@code this} object */ public final ImmutablePerson withChildren(String... elements) { List newValue = createUnmodifiableList(false, createSafeList(Arrays.asList(elements), true, false)); return new ImmutablePerson(this.name, this.city, this.age, newValue, this.car); } /** * Copy the current immutable object with elements that replace the content of {@link Person#getChildren() children}. * A shallow reference equality check is used to prevent copying of the same value by returning {@code this}. * @param elements An iterable of children elements to set * @return A modified copy of {@code this} object */ public final ImmutablePerson withChildren(Iterable elements) { if (this.children == elements) return this; List newValue = createUnmodifiableList(false, createSafeList(elements, true, false)); return new ImmutablePerson(this.name, this.city, this.age, newValue, this.car); } /** * Copy the current immutable object by setting a present value for the optional {@link Person#getCar() car} attribute. * @param value The value for car * @return A modified copy of {@code this} object */ public final ImmutablePerson withCar(String value) { String newValue = Objects.requireNonNull(value, "car"); if (Objects.equals(this.car, newValue)) return this; return new ImmutablePerson(this.name, this.city, this.age, this.children, newValue); } /** * Copy the current immutable object by setting an optional value for the {@link Person#getCar() car} attribute. * An equality check is used on inner nullable value to prevent copying of the same value by returning {@code this}. * @param optional A value for car * @return A modified copy of {@code this} object */ public final ImmutablePerson withCar(Optional optional) { String value = optional.orElse(null); if (Objects.equals(this.car, value)) return this; return new ImmutablePerson(this.name, this.city, this.age, this.children, value); } /** * This instance is equal to all instances of {@code ImmutablePerson} that have equal attribute values. * @return {@code true} if {@code this} is equal to {@code another} instance */ @Override public boolean equals(Object another) { if (this == another) return true; return another instanceof ImmutablePerson && equalTo((ImmutablePerson) another); } private boolean equalTo(ImmutablePerson another) { return name.equals(another.name) && Objects.equals(city, another.city) && age == another.age && children.equals(another.children) && Objects.equals(car, another.car); } /** * Computes a hash code from attributes: {@code name}, {@code city}, {@code age}, {@code children}, {@code car}. * @return hashCode value */ @Override public int hashCode() { int h = 5381; h += (h << 5) + name.hashCode(); h += (h << 5) + Objects.hashCode(city); h += (h << 5) + age; h += (h << 5) + children.hashCode(); h += (h << 5) + Objects.hashCode(car); return h; } /** * Prints the immutable value {@code Person} with attribute values. * @return A string representation of the value */ @Override public String toString() { StringBuilder builder = new StringBuilder("Person{"); builder.append("name=").append(name); if (city != null) { builder.append(", "); builder.append("city=").append(city); } builder.append(", "); builder.append("age=").append(age); builder.append(", "); builder.append("children=").append(children); if (car != null) { builder.append(", "); builder.append("car=").append(car); } return builder.append("}").toString(); } /** * Creates an immutable copy of a {@link Person} value. * Uses accessors to get values to initialize the new immutable instance. * If an instance is already immutable, it is returned as is. * @param instance The instance to copy * @return A copied immutable Person instance */ public static ImmutablePerson copyOf(Person instance) { if (instance instanceof ImmutablePerson) { return (ImmutablePerson) instance; } return ImmutablePerson.builder() .from(instance) .build(); } /** * Creates a builder for {@link ImmutablePerson ImmutablePerson}. * @return A new ImmutablePerson builder */ public static ImmutablePerson.Builder builder() { return new ImmutablePerson.Builder(); } /** * Builds instances of type {@link ImmutablePerson ImmutablePerson}. * Initialize attributes and then invoke the {@link #build()} method to create an * immutable instance. * {@code Builder} is not thread-safe and generally should not be stored in a field or collection, * but instead used immediately to create instances. */ public static final class Builder { private static final long INIT_BIT_NAME = 0x1L; private static final long INIT_BIT_AGE = 0x2L; private long initBits = 0x3L; private String name; private String city; private int age; private List
children = new ArrayList(); private String car; private Builder() { } /** * Fill a builder with attribute values from the provided {@code Person} instance. * Regular attribute values will be replaced with those from the given instance. * Absent optional values will not replace present values. * Collection elements and entries will be added, not replaced. * @param instance The instance from which to copy values * @return {@code this} builder for use in a chained invocation */ public final Builder from(Person instance) { Objects.requireNonNull(instance, "instance"); name(instance.getName()); @Nullable String cityValue = instance.getCity(); if (cityValue != null) { city(cityValue); } age(instance.getAge()); addAllChildren(instance.getChildren()); Optional carOptional = instance.getCar(); if (carOptional.isPresent()) { car(carOptional); } return this; } /** * Initializes the value for the {@link Person#getName() name} attribute. * @param name The value for name * @return {@code this} builder for use in a chained invocation */ public final Builder name(String name) { this.name = Objects.requireNonNull(name, "name"); initBits &= ~INIT_BIT_NAME; return this; } /** * Initializes the value for the {@link Person#getCity() city} attribute. * @param city The value for city (can be {@code null}) * @return {@code this} builder for use in a chained invocation */ public final Builder city(@Nullable String city) { this.city = city; return this; } /** * Initializes the value for the {@link Person#getAge() age} attribute. * @param age The value for age * @return {@code this} builder for use in a chained invocation */ public final Builder age(int age) { this.age = age; initBits &= ~INIT_BIT_AGE; return this; } /** * Adds one element to {@link Person#getChildren() children} list. * @param element A children element * @return {@code this} builder for use in a chained invocation */ public final Builder addChildren(String element) { this.children.add(Objects.requireNonNull(element, "children element")); return this; } /** * Adds elements to {@link Person#getChildren() children} list. * @param elements An array of children elements * @return {@code this} builder for use in a chained invocation */ public final Builder addChildren(String... elements) { for (String element : elements) { this.children.add(Objects.requireNonNull(element, "children element")); } return this; } /** * Sets or replaces all elements for {@link Person#getChildren() children} list. * @param elements An iterable of children elements * @return {@code this} builder for use in a chained invocation */ public final Builder children(Iterable elements) { this.children.clear(); return addAllChildren(elements); } /** * Adds elements to {@link Person#getChildren() children} list. * @param elements An iterable of children elements * @return {@code this} builder for use in a chained invocation */ public final Builder addAllChildren(Iterable elements) { for (String element : elements) { this.children.add(Objects.requireNonNull(element, "children element")); } return this; } /** * Initializes the optional value {@link Person#getCar() car} to car. * @param car The value for car * @return {@code this} builder for chained invocation */ public final Builder car(String car) { this.car = Objects.requireNonNull(car, "car"); return this; } /** * Initializes the optional value {@link Person#getCar() car} to car. * @param car The value for car * @return {@code this} builder for use in a chained invocation */ public final Builder car(Optional car) { this.car = car.orElse(null); return this; } /** * Builds a new {@link ImmutablePerson ImmutablePerson}. * @return An immutable instance of Person * @throws java.lang.IllegalStateException if any required attributes are missing */ public ImmutablePerson build() { if (initBits != 0) { throw new IllegalStateException(formatRequiredAttributesMessage()); } return new ImmutablePerson(name, city, age, createUnmodifiableList(true, children), car); } private String formatRequiredAttributesMessage() { List attributes = new ArrayList(); if ((initBits & INIT_BIT_NAME) != 0) attributes.add("name"); if ((initBits & INIT_BIT_AGE) != 0) attributes.add("age"); return "Cannot build Person, some of required attributes are not set " + attributes; } } private static List createSafeList(Iterable iterable, boolean checkNulls, boolean skipNulls) { ArrayList list; if (iterable instanceof Collection) { int size = ((Collection) iterable).size(); if (size == 0) return Collections.emptyList(); list = new ArrayList<>(); } else { list = new ArrayList<>(); } for (T element : iterable) { if (skipNulls && element == null) continue; if (checkNulls) Objects.requireNonNull(element, "element"); list.add(element); } return list; } private static List createUnmodifiableList(boolean clone, List list) { switch(list.size()) { case 0: return Collections.emptyList(); case 1: return Collections.singletonList(list.get(0)); default: if (clone) { return Collections.unmodifiableList(new ArrayList(list)); } else { if (list instanceof ArrayList) { ((ArrayList) list).trimToSize(); } return Collections.unmodifiableList(list); } } } }
Let’s add some tests to verify that the construction works and that the mandatory and optional parameters are considered and works as expected.
@Test
public void testConstruction() {
Person person = ImmutablePerson.builder()
.name("Viktor")
.city("Stockholm")
.age(24)
.build();
assertEquals(person.getName(), "Viktor");
}
@Test (expected = IllegalStateException.class)
public void testConstructionFailDueToMissingRequiredField() {
ImmutablePerson.builder()
.name("Viktor")
.city("Stockholm")
.build();
}
Everything good so far, we have turned around 400+ lines of code into less than 10. That’s a massive amount of boilerplate code removed. Isn’t that amazing? Our object is in fact also completely immutable, meaning that all fields are final. In fact, after building our object there isn’t even any way modify the fields, no setters or adders are provided which means that it is completely thread-safe. However, we are provided with a convenient method for modifying fields, but it will copy the object and all the fields inside. So, for example, if we provide a list to the initial object and then modify the list, it will not be the same list that we modify.
@Test
public void verifyThatObjectIsImmutable() {
List children = new ArrayList<>();
children.add("Viktor");
children.add("Jenny");
children.add("Thea");
Person person1 = ImmutablePerson.builder()
.name("Joakim")
.age(50)
.city("Stockholm")
.children(children)
.car("BMW")
.build();
Person person2 = ImmutablePerson.builder()
.from(person1)
.addChildren("Vera")
.build();
assertEquals(3, children.size());
assertNotEquals(person1, person2);
}
In fact, the list isn’t even modifiable outside of the object that owns the list, attempting to do that will result in an UnsupportedOperationException.
@Test (expected = UnsupportedOperationException.class)
public void verifyListIsNotModifiable() {
List children = new ArrayList<>();
children.add("Viktor");
children.add("Jenny");
children.add("Thea");
Person person1 = ImmutablePerson.builder()
.name("Joakim")
.age(50)
.city("Stockholm")
.children(children)
.car("BMW")
.build();
List children2 = person1.getChildren();
children2.add("Vera");
}
Alternative static constructor
Immutables also makes it possible to have a convenient static constructor of. This can be done by annotating fields with the annotation @Value.Parameter
. However, please make sure that all mandatory fields are provided here, or you won’t even be able to compile the code.
@Test
public void createObjectWithOfConstructor() {
PersonWithOfConstructor personWithOfConstructor = ImmutablePersonWithOfConstructor.of("Viktor", 24);
assertEquals("Viktor", personWithOfConstructor.getName());
}
When using an abstract class instead of an interface it is possible to add a default method by annotating @Value.Default
. Which means that you do what we did above with the @Value.Parameter
and skip some mandatory fields as long as you provide a default method for them.
@Value.Default
public String getName() {
return "Anonymous";
}
Modifiable
Immutables are nice and all, but sometimes you might, for some reason, want to be able to modify the object directly. This is possible by annotating the class with @Value.Modifiable
. It is worth to mention though that the support for it is quite limited as immutables, as the name suggests, heavily favors immutable objects.
Summary
Immutables is an extremely powerful immutability library to build immutable and thread-safe objects with very little boilerplate code required which makes it very convenient. The immutability library reminds quite a lot of Lombok, they both reduce boilerplate code. I would say that Lombok is much more mature. However, it lacks the same good support for immutable objects which is something that immutables excels on. So my advice is to stick to Lombok if you simply wanna reduce boilerplate code, but if you prefer immutables and functional programming in general you should definitely check out Immutables. They have loads of features than I’ve gone through, the features that I brought up is the ones I deemed most important to get intrigued and started with it. You can check out the other features supported by Immutables here.