Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

The Power of Immutability in Java

DZone's Guide to

The Power of Immutability in Java

Take a look at the relationship between concurrency and immutability, as well as some tips for overloaded constructors, the Builder Pattern, and other techniques to help.

· Java Zone ·
Free Resource

Get the Edge with a Professional Java IDE. 30-day free trial.

A few years back, I was trying my hand with Scala. Though I fell in love with Scala, I knew bringing it to projects I work on was not going to be an easy undertaking. But there were some concepts in Scala that I decided to apply in Java. One of the concepts I experimented with was immutability. Immutable objects are not a new concept for Java, though it is more fashionable to use POJOs (Plain Old Java Objects), which are inherently mutable in nature.

In this article, I will share my experiences on the immutability of domain modeling and other related topics to effectively embrace immutability.

Concurrency Simplified With Immutability

One of the core reasons I wanted to experiment with immutability was the simplicity it brought to multi-threaded applications. Here is why.

A common problem with implementing a concurrent system is locking shared objects — or rather forgetting to lock a shared mutable object. Since version 5, Java has provided various data structures that are multi-threading safe, such as ConcurrentHashMap.

But as we know, objects within these data structures are not thread safe. Most developers who work on concurrent systems are aware it and take precautions. But then, as time passes by and the shape of the team changes, it is hard to keep track of all precautions that were taken previously. Most people point to TDD as an answer to avoid issues, but mitigating concurrency-related issues through testing is no easy task.

Now, if we replace mutable objects and the need for locks and safeguards with immutable objects, objects start to become more fluid and can be passed around the system/threads without safeguards and locks. This makes the system less prone to issues like deadlocks, inconsistent state, etc.

Let's take a look at some POJO and Immutable examples:

public class PojoCat {
    private String name;
    private Owner owner;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Owner getOwner() {
        return owner;
    }

    public void setOwner(Owner owner) {
        this.owner = owner;
    }
}


public final class ImmutableCat {

    private final String name;
    private final Owner owner;

    public ImmutableCat(String name, Owner owner) {
        this.name = name;
        this.owner = owner;
    }

    public String getName() {
        return name;
    }

    public Owner getOwner() {
        return owner;
    }

  public ImmutableCat changeOwner(Owner newOwner) {
      return new ImmutableCat(this.name, newOwner);
    }
}


Now let's say we need to change the owner of the Cat. In PojoCat we would use setOwner and change the owner, whereas in ImmutableCat, we would use changeOwner, which would generate a completely new instance.

In a multi-threaded environment, if an object is mutated while it is being accessed by different threads, there is no guarantee of what inconsistencies will happen and what will break. Whereas if the object is immutable, threads processing the object would be operating on the same version/instance or a different version/instance of the object. Any new change should trickle through the system in a deterministic fashion, keeping things consistent.

Dealing With Complete and Valid Objects

As immutable objects cannot be modified, it is important to pay attention to how the object can be built with a complete set of information. This also gives us the opportunity to ensure the information provided is complete during construction, i.e. apply validations and assertions to guarantee the completeness of the information. The logic to derive or default some attributes can also be done during the construction stage. This makes understanding the domain object and completeness of it much easier. 

Next, we will look at the construction options for the objects and examples of simple validations.

Overloaded Constructors vs. the Builder Pattern

One of the first hurdles faced using immutable objects is the ugliness that comes with overloaded constructors. It is not only hard to remember the sequence of the parameters but also, if the number of attributes increases, so will the constructor parameters.

This not only breaks the code quality checks but also forces refactoring (method signature change) on the addition of every new attribute, making the code changeset larger than it should be. Overloading the constructors becomes inevitable, sometimes impossible, and sometimes error-prone due to the risk of putting null in the wrong place.

The builder pattern is much better-suited for constructing immutable objects. Extensions can be made without the need to break existing code, and setters (or withXXs) can be chained to create a nice DSL effect. The builder pattern also provides the opportunity to delay validations, derivation, and defaulting to the very end, i.e. the build method, which means attributes can be set in any order and it would not matter.

Here is an article that explains it very well, and there are plenty of IDE plugins that make it easier to autogenerate.

import java.util.Objects;

public final class ImmutableCat {

    private final String name;
    private final Owner owner;
    private final Colour colour;


    private ImmutableCat(Builder builder) {
        this.name = builder.name;
        this.owner = builder.owner;
        this.colour = builder.colour;
    }

    public String getName() {
        return name;
    }

    public Owner getOwner() {
        return owner;
    }

    public Colour getColour() {
        return colour;
    }

    public static final class Builder {
        private String name;
        private Owner owner;
        private Colour colour;

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

        public Builder withOwner(Owner owner) {
            this.owner = owner;
            return this;
        }

        public Builder withColour(Colour colour) {
            this.colour = colour;
            return this;
        }

        public ImmutableCat build() {
          //Validate
            Objects.requireNonNull(name, "Cat should have a name");
            Objects.requireNonNull(colour, "Cat should have a colour");

          //default
            if(owner == null) {
                this.owner = Owner.Unknown;
            }

            return new ImmutableCat(this);
        }
    }
}


One limitation the builder pattern brings is that it doesn't work very well with object inheritance. For instance, if we had AbstractAnimal, Cat, and Dog, either the builder of Cat and Dog would need to have the same "withAttribX" repetitions for AbstractAnimal's attributes, or they would need to inherit from the builder of AbstractAnimal, which cannot build and hence cannot validate.

Next, let's look at inheritance vs. composition to see if this issue can be avoided.

Inheritance vs. Composition

The following are a couple of articles that explain the differences and benefits of one over the other.

From my point of view, I found it much easier to use interfaces to describe IS-A relationships, so I can implement AbstractFactory design patterns. From a code reuse standpoint, I prefer composition, i.e. HAS-A modeling.

When it comes to the builder pattern, every composition is an independent object that can be constructed with completeness and validated. For instance, Object Desktop, which has an object Memory, only needs to validate the extra attributes in object Desktop, as object Memory should be validated and built independently.

As mentioned in the above articles, composition benefits from better memory utilization. It is also possible to construct various instances of Desktop with the same instance of Memory if they have the same spec. This is particularly useful when dealing with a large amount of data that shares common elements and has to be kept in memory.

So what is the downside? Well, the nesting of composition needs to be kept in check, otherwise, in order to reach an attribute, there will be a lot of nested calls. 

Experimental: Further Memory Optimization

As we know, Strings are immutable and Java can optimize heap utilization of Strings by maintaining a constant pool. A similar concept can be applied to immutable objects. For instance, if ObjectA and ObjectB are immutable and equal, one can replace the other and, therefore, only one of them would need to be maintained on the heap while the other can be GCed instantly (assuming equals/hashcode has been implemented correctly). As there is no support provided in Java for immutable objects, this can be achieved by using simple caching techniques, like using WeakMaps.

Typically, this would be useful in systems where a lot of objects are cached or held in memory.

Disclaimer: This is an experimental approach, so please use it with caution and only when necessary.

Will It Always Work?

Well, now the question is, "Can immutability be used everywhere?" To which my answer would be, "It depends."

  • It depends on how the domain is modeled. If mutation is done upon the change of every attribute, then immutability will backfire by creating lots of garbage for collection.

  • It depends on what the system does. For instance, enrichment, CRUD operations, state management, etc.

  • It depends on how concurrent the system is and what trade-offs/risks are suitable.

  • It depends on if memory optimizations (see the section Further Memory Optimization) can achieve a higher benefit than the extra garbage a system might generate.

I hope to hear other points of view and experiences

Get the Java IDE that understands code & makes developing enjoyable. Level up your code with IntelliJ IDEA. Download the free trial.

Topics:
java ,domain model ,best practices ,concurrency ,immutability ,tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}