Skip to main content
The Immutable

Why would we need immutable classes? Aren’t classes using setters and getters, i.e. POJOs, enough?

With the upcoming of even more distributed systems and multithreaded programming it’s getting harder and harder to reason about the state of a software system. POJOs or mutable classes may be shared between different threads or actors of a software system. They may change the state of the class and due to the non deterministic nature of distributed systems one can not predict the state of a class that was changed by different actors. This is commonly known as a data race condition.

Immutable classes or immutable data in general take one variable out of the reasoning about the state. The state of immutable data is known after creation and will never change. As it never changes, immutable classes are also inherently threadsafe and may be shared between threads.

How do we make a Java class immutable?

  • All fields should be final and a class should only provide getters.
  • All return values of accessor methods must be immutable as well.
  • A special case are own classes that are members of another class. These must be immutable as well to guarantee total immutability.
An example of a immutable class

class Product {

private final String name;
private final ImmutableList images;
private final Price price;

Product(String name, ImmutableList images, Price price) {
this.name = name;
this.images = images;
this.price = price;
}

public String getName() {
return name;
}

public ImmutableList getImages() {
return images;
}

public Price getPrice() {
return price;
}

@Override
public boolean equals(Object o) {

}

@Override
public int hashCode() {

}
}

Here all fields are final and the images list is a guava ImmutableList. The class Price should be immutable as well to guarantee the immutability of the class. Otherwise the state reference obtained by getPrice may be changed and the immutability of this object is jeopardized.

The Builder

Using the implementation above there is only one way to construct an instance of `Product`; using the constructor. Depending on the number of fields the constructor arguments may become unwieldy. Some of them may be optional as well and we have a nice cascade of telescoping constructors. An option to circumvent these „problems“ is using a Builder.

According to wikipedia

the Builder is a design pattern designed to provide a flexible solution to various object creation problems in object-oriented programming. The intent of the Builder design pattern is to separate the construction of a complex object from its representation.

Here we use it to hide the unwieldy number and optional constructor parameters.

The simplest builder has a corresponding method for every field that temporarily saves the designated value in a field and creates the actual object when calling a designated method often called build or create. An example is noted down below:

class ProductBuilder {

private String name;
private Price price;
private List images = new ArrayList<>();

public ProductBuilder name(String name) {
this.name = name;
return this;
}

public ProductBuilder images(List images) {
this.images = images;
return this;
}

public ProductBuilder price(Price price) {
this.price = price;
return this;
}

public Product build() {
return new Product(name, ImmutableList.copyOf(images), price);
}
}

Every initializer method returns itself so calls may be chained fluently. Default values may be set on field initialization. Mutable structures may be used to allow cumulative addition of list values or similar values. Also one could put some initialization logic or guards into an initializer method.

When the builder is in place it is good practice to add a static method builder() and a static copy(Pojo p) method to the original pojo. The `builder` method creates a new empty instance of the Builder class. Whereas the `copy` method takes an already populated instance of the immutable class and returns a builder that is preinitialized with the values of the existing instance. Parts of it may then be overwritten See the repository for implementations.

One disadvantage of this builder implementations is that they are not threadsafe. Different threads using the same builder instance may run into a data race. So it must not be shared between different threads. But as we can shortly see it is easier to use than an immutable builder.

Immutable version of a builder

A builder may also be made immutable and inherits the same advantages as an immutable object. The builder will be threadsafe and one can reason about the state if used in multiple threads.

To make a builder immutable the initializer methods must return a new instance of the builder with the new values. See below for an example:

class ImmutableProductBuilder {

private String name;
private Price price;
private ImmutableList images;

ImmutableProductBuilder() {
this(„“, ImmutableList.of(), null);
}

private ImmutableProductBuilder(String name, ImmutableList images, Price price) {
this.name = name;
this.images = images;
this.price = price;
}

public ImmutableProductBuilder name(String name) {
return new ImmutableProductBuilder(name, images, price);
}

public ImmutableProductBuilder images(List images) {
return new ImmutableProductBuilder(name, ImmutableList.copyOf(images), price);
}

public ImmutableProductBuilder price(Price price) {
return new ImmutableProductBuilder(name, images, price);
}

public Product build() {
return new Product(name, price, images);
}
}

The advantage is that these kind of builders are also threadsafe. But you must always assure to assign the return value of an initializer method. That is why the mutable version of the builder is easier to use. Once a value is set for an instance it is set. Whereas in the immutable builder case the change is lost when the return value is not assigned.

And Jackson

Now we have nice immutable classes that may only be created using a builder so we always know what state they are in. What happens if we try to send these objects via HTTP/JSON to somebody else?

Serializing such an object to json using the Jackson library should work out of the box, as long as the getter follow the JavaBean conventions and are prefixed with get. If they don’t, every getter needs to contain an annotation @JsonProperty so jackson can identify them as a json property. Any other jackson annotation that controls serialization may be used here as well.

Deserializing does not work out of the box as the setters are missing. There are two ways to enable succesful deserialization of these objects. Annotating the immutable classes constructor with @JsonCreator and every parameter with @JsonProperty. This is error prone as when adding new properties is easily forgotten. Also it violates the assumption that we have only one mean to construct an object and it is possible to bypass logic we added to the builder when using the constructor directly. Especially default values, nullability checks and others.

Fortunately Jackson already addresses this problem and provides us with annotations that allow to use a builder instead of the constructor for deserializing an object from json.

The immutable classes must be annotated with @JsonDeserialize(builder = ProductBuilder.class) and the builder class must be declared as a pojo builder and be annotated with @JsonPOJOBuilder(). The creator method of the builder and prefixes for the initializer methods may be specified as parameters. See JsonPOJOBuilder javadoc for details. By default the prefix is with and the constructor methods called build. So in the code below we need to set the prefix to empty „“ as we don’t use a prefix.

@JsonDeserialize(builder = ProductBuilder.class)
class Product {

}

@JsonPOJOBuilder(withPrefix = „“)
class ProductBuilder {

}

The json properties must be named the same as the builder initializer methods without the prefix. So an method named withPrice will produce a json property price. As long as you don’t use @JsonProperty to rename the json property.

Conclusion

This concludes our little disgression in immutables classes and their serialization / deserialization into and from json. I hope I could show you that it is not necessary to weaken immutability guarantees just because your serialization library assumes all objects are java beans. At least for json there are means to enforce immutability.

The techniques outlined here can be taken one step further by using some library that generates the boilerplate builder for you. One is Immutables. Immutables generates immutable classes and builder using interfaces or abstract classes. For serializing / deserializing these generated classes to / from json the same annotations may be used. See the Immutables tutorial for details.

See our github repository for the full source code that also includes an example using Immutables.

content by Florian Schulz

Interesse geweckt? Dann lass uns sprechen.
Folge uns auf