Functional Programming With Java: Immutability

 · 10 min
Zoltan Tasi on Unsplash

As laid out in the first piece, immutability is one of the core concepts of functional programming. Real functional programming languages support it by design, at a language-level. But in Java and most non-functional languages, we need to design and implement it ourselves, at code-level.

The fundamental idea behind immutability is simple: If we want to change a data structure, we need to create a new copy of it with the changes, instead of mutating the original data structure.


Why Immutability Matters

Why should we take the extra steps to change a value? Because with immutable data structures, we gain a lot of advantages:

  • Data won’t change behind our backs.
  • Once verified, it will be valid indefinitely.
  • No hidden side effects.
  • No half-initialized objects, moving through different methods until it’s complete. This will decouple methods, hopefully making them into pure functions.
  • Thread-safety: no more race conditions. If a data structure never changes, we can safely use it in multiple threads.
  • Better cacheability.
  • Possible optimization techniques, like referential transparency and memoization.

State of Java Immutability

As of writing this article, Java isn’t a language with immutability by design. But it provides us with all the pieces needed to create immutable data structures ourselves.

Built-in immutable types

There are various immutable types available in the JDK. Here are a few that you have probably already encountered:

The “final” keyword

The keyword final` is used to define a variable that’s only assignable once. It might sound like immutability at first, but it really isn’t in a broader sense.

Instead of creating an immutable data structure, only the reference to it will be unchangeable. This only ensures that a variable is always pointing to the same point in memory. It says nothing about the memory content itself.

Java 14

In March 2020, Java 14 will be released with a preview of records: “data holders” for shallowly immutable data.

Hopefully, we can replace many JavaBean types with records. Although there are multiple restrictions compared to traditional classes. They will make records safer to use, but we might have to rethink how we build our data structures.

Java 14 is far into the future for many developers, myself included. I’m using Java 8 and, hopefully soon, 9. So let’s check out other options to achieve immutability today.


How to Become Immutable

There are effectively two ways of creating immutable data structures without Java 14 records: doing it ourselves, or using a third-party framework.

DIY immutables

Think of a typical JavaBean:

java
public class User {

    private String email;

    public User() {
        super();
    }

    public User(String email) {
        super();
        this.email = email;
    }

    public String getEmail() {
        return this.email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

JavaBeans are designed with getters and setters, so they can be used in various scenarios. Many frameworks, like ORM or GUI designers, are reflection-based. They analyze classes to identify getters and setters, not using fields via reflection directly, and are therefore mostly incompatible with immutable designs.

Another pitfall can be side effects. Setters might do more than just set a single value — they might track a dirty state or set multiple values. This isn’t best practice, but it happens all the time.

This particular JavaBean-design isn’t a must-have or fixed rule. It’s a convention that we often use thanks to habit-driven development, and not because we actually need that particular design.

Let’s break with traditional bean-design and make it immutable:

java
public class User {

    private final String email;

    public User(String email) {
        this.email = email;
    }

    public String getEmail() {
        return this.email;
    }
}

That was really simple and we got shorter, more concise code. Now our data structure won’t change unexpectedly once it’s initialized.

Unless we add any more fields with a mutable type…

Every field of an immutable data structure has to be immutable. And every field of the types of it has to be immutable, too. With a single mutable type, maybe even deeply nested, all the benefits of being immutable will be destroyed.

Collections are also problematic types. Unmodifiable collections have existed since Java 7.

With Java 9, easy-to-use factory methods were added. But, just like final, it only means the collection itself is unmodifiable, not the contained objects. So be sure to only save already immutable data structures in them.

Builder pattern

We ensured all our fields are immutable, no matter how deeply nested they are. But we still have a problem: how to build the data structure.

All fields must be set on initialization, so we need a constructor with all fields. What if we don’t need all the fields to be set? Should we provide multiple constructors or static builder-methods? And how do we ensure that all required fields are set and the resulting object is valid?

By using the builder pattern.

We need an additional class encapsulating the complex process of building an immutable data structure. By separating the creation process from representation, we gain fine control over the data structure assembly process, and can even add validation. It also means we introduce a mutable builder, so we can have an immutable data structure.

Assume a more complex user type:

java
public class User {

    // required fields
    private final String email;
    private final String password;

    // optional fields
    private final boolean active;
    private final Optional<LocalDateTime> lastLogin;

    public User(String email,
                String password,
                boolean active,
                LocalDateTime lastLogin) {
        this.email = email;
        this.password = password;
        this.active = active;
        this.lastLogin = Optional.ofNullable(lastLogin);
    }

    public String getEmail() {
        return this.email;
    }

    public String getPassword() {
        return this.password;
    }

    public String isActive() {
        return this.active;
    }

    public Optional<LocalDateTime> getLastLogin() {
        return this.lastLogin;
    }
}

The fields active and lastLogin are optional — by default a user isn’t active until explicitly stated and has never logged in. Either we provide the arguments every time we create a user, or we add additional constructors to match the different combinations of arguments.

But the more complex the type gets, the more constructors we would need. Instead, we create a builder:

java
public class UserBuilder {

    private final String email;
    private final String password;
    private boolean active;
    private LocalDateTime lastLogin;

    // Only one constructor with the required fields.
    // This way we won't forget to set them.
    public UserBuilder(String email,
                       String password) {
        this.email = email;
        this.password = password;
    }

    public UserBuilder active(boolean active) {
        this.active = active;
        return this;
    }

    public UserBuilder lastLogin(LocalDateTime lastLogin) {
        this.lastLogin = lastLogin;
        return this;
    }

    public User build() {
        return new User(this.email,
                        this.password,
                        this.active,
                        Optional.ofNullable(this.lastLogin));
    }
}

Now we can build a user with a fluent API:

java
User user = new UserBuilder("john@doe.com",
                            "pa$$w0rd").active(true)
                                       .build();

Or we can build it in multiple steps:

java
// start building
UserBuilder builder = new UserBuilder("john@doe.com", "pa$$w0rd");
builder.active(true);

// ...do some other stuff

// set remaining fields
builder.lastLogin(lastLoginVariable);

// create the actual user
User user = builder.build();

To make our builder even better, we can add validation to the methods or constructor — e.g. null-checks, or email-/password-validation — and throw an appropriate exception:

java
public class UserBuilder {

    // ...

    // Only one constructor with the required fields.
    // This way we won't forget to set them.
    public UserBuilder(String email,
                       String password) {
        if (email == null) {
            throw new NullPointerException();
        }
        this.email = email;

        if (password == null || password.length > 6) {
            throw new NullPointerException();
        }
        this.password = password;
    }

    // ...
}

It’s good practice to increase encapsulation by putting the builder class directly into the corresponding type as a static nested class. This way we can always use the type name Builder:

java
public class User {

    public static final class Builder {
        // ...
    }

    // ...
}

We successfully created an immutable type and a corresponding builder. But it’s a lot of boilerplate and it can be cumbersome to do that every time we introduce a new type. Every piece of code we write can introduce bugs.

That’s why using a third-party framework can be a real relief, saving typing all the code and providing a lot of convenience-methods and validation.


Third-Party Frameworks

Instead of writing all the code ourselves, we can employ third-party frameworks, to concentrate on designing and modeling the data structures. The generated code is less error-prone and the resulting data structures can be more concise.

Immutables

As the project “Immutables” describes itself:

Java annotation processors to generate simple, safe and consistent value objects. Do not repeat yourself, try Immutables, the most comprehensive tool in this field!

Creating an immutable type is as simple as creating an abstract type, either an abstract class or an interface, and adding the right annotations:

java
import org.immutables.value.Value;

@Value.Immutable
public interface User {

    String getEmail();
    String getPassword();
  
    @Value.Default
    default boolean isActive() {
        return false;
    }

    Optional<LocalDateTime> getLastLogin();
}

The annotation processor generates the actual implementation behind the scenes, including:

  • Serialization support.
  • Builder class.
  • Requirement validation.
  • Convenience methods for copying, etc.
  • equals, hashCode, and toString

Let’s create an immutable user with the provided builder:

java
User user = ImmutableUser.builder()
                         .email("john@doe.com")
                         .password("pa$$w0rd")
                         .lastLogin(LocalDateTime.now())
                         .build();

Thanks to using the @Value.Default annotation and the Optional type, we automatically get validation on calling build(). If not all the requirements are met, an IllegalStateException is thrown.

Another convenience that the framework provides is adding helper methods for creating a copy of an immutable without the need to re-set all the values:

java
User changedPW = ImmutableUser.copyOf(user)
                              .withPassword("n3wP4ssw07d);

Or we can get a new builder to work with:

java
User changedPW = ImmutableUser.builder()
                              .from(user)
                              .password("n3wP4ssw07d")
                              .active(true)
                              .build();

This is just the tip of the iceberg — the feature list is comprehensive:

Project Lombok

Project Lombok is a comprehensive tool trying to reduce the amount of the usual Java boilerplate code, like getters and setters, null checks, equals/hashCode, or toString.

And of course immutables:

java
@Value
public class User {
    private String email;
    private String password;
    private boolean active;
    private Optional<LocalDateTime> lastLogin;
}

The annotation @Value is equal to using these quite self-explanatory annotations:

The only one missing is @Builder, which adds User.builder() to our example.

The project is a great tool for reducing boilerplate. But for building flexible immutable data structures, I would recommend Immutables. Both projects have different goals, and I think it’s a given that a project named Immutables has better features regarding immutability.


Conclusion

Immutability is a good idea for many kinds of software projects, not just languages without built-in support. Even for data storage, it can provide advantages, like the version control system Git, for example. It uses immutable commits to ensure integrity.

But that doesn’t mean every problem is solvable with immutable data structures. Mutable state isn’t a bad thing in itself. We just have to be sure when to use it and be aware of the pitfalls of changing state.

One disadvantage, at least at first, is adapting your codebase to the new data structures. Changing them isn’t a drop-in replacement. But it helps us to create a more predictable data flow, with easily observable state changes.

Our real-world example

We decided to start using immutables to simplify session management. Detecting a dirty session can be hard with deeply nested non-immutable data structures. And persisting every session with every request creates a lot of unnecessary overhead.

Thanks to our new session data structure, which only contains immutable types as fields, it’s much simpler to detect a changed session: If a field is updated, the session is dirty. No more changes to nested objects behind our backs.

But we didn’t stop at the session management. We’ve started to replace more and more types with immutables, to eliminate many subtle bugs, like invalid state or race conditions.

If a data structure doesn’t change too much over its lifetime, we try to make it immutable.


A Functional Approach to Java Cover Image
Interested in using functional concepts and techniques in your Java code?
Check out my book!
Available in English, Polish, Korean, and soon, Chinese.

Resources