Immutable Objects in Java using Builder Pattern with Functional Interface

Manna Mahmud
System Weakness
Published in
4 min readAug 1, 2023

--

Why Immutable Objects Are Important

Immutable objects in Java are objects whose states cannot be changed once created. This attribute of immutability brings about several benefits:

Thread-Safety: Immutable objects are inherently thread-safe, as their state cannot change after creation. This makes them a good fit for multi-threaded environments where synchronization and thread safety are essential.

Simplicity and Clarity: Immutable objects are simpler to design, implement, and use. Since they cannot change state, you do not have to worry about their state changes over time. This makes the code clear and easier to reason about.

Hashing: Immutable objects are excellent for use as keys in HashMap or elements in HashSet as their hashcode remains constant.

Security Risks in Deserialization and RCE: Deserialization and Remote Code Execution (RCE) vulnerabilities in Java are often linked to mutable objects. Deserialization transforms a byte stream back into an object. If this process occurs without proper validation, it can lead to RCE. Mutable objects, which can alter their state post-creation, can be exploited during deserialization to change a program’s control flow or invoke arbitrary functions. This risk is exemplified by the Apache Commons Collections Deserialization Vulnerability (CVE-2015–4852), where mutable objects were manipulated during deserialization, leading to RCE. Immutable objects, which cannot change their state after creation, are inherently resistant to such manipulation, emphasizing the importance of immutability when handling potentially untrusted input.

How to Make Java Objects Immutable Using Functional Interface and Builder Pattern

To illustrate how to create complex immutable objects using a Builder pattern and a functional interface, let’s imagine we’re creating a House object. This house contains several other objects including a MainDoor, Terrace, Lobby, MainBedroom, GuestRoom, each with its own properties. We want to create these objects in an immutable manner and integrate them into the House object.

Setting Up the Project

The entire code for this article is hosted on GitHub and can be accessed here. It’s a Maven-based project developed in Java 17. Please clone this repository and follow along as we discuss the various classes and their functionalities.

Defining the Functional Interface

We start with the definition of a Customizer functional interface. This interface is used in our builder classes to provide a mechanism for clients to customize the objects being built.

@FunctionalInterface
public interface Customizer<T> {
T customize(T t);
}

Defining the MainDoor Class

The MainDoor class is an example of how each component of the House is built. The class is declared as final, and all its fields are private final, making it an immutable class. It employs the Builder pattern to construct an instance, which is returned via a build() method.

public final class MainDoor {
private final String doorType;
private final String doorColor;
private final String doorLockType;

// Constructor is private to control the creation of instances.
private MainDoor(Builder builder) {
this.doorType = builder.doorType;
this.doorColor = builder.doorColor;
this.doorLockType = builder.doorLockType;
}

// Getters for all fields, no setters provided to maintain immutability.
public String getDoorType() {
return doorType;
}

public String getDoorColor() {
return doorColor;
}

public String getDoorLockType() {
return doorLockType;
}

// The Builder class
public static class Builder {
private String doorType;
private String doorColor;
private String doorLockType;

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

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

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

public MainDoor build() {
return new MainDoor(this);
}
}
}

Defining the House

The House class uses a Builder to encapsulate the construction logic. Each component is built using a Customizer and incorporated into the House object. This design allows the client to use lambda expressions to customize each component of the house during construction.

public final class House {
private final MainDoor mainDoor;
//... other components

// Private constructor to control instance creation.
private House(Builder builder) {
this.mainDoor = builder.mainDoor;
//... other components
}

// Getters for all components, no setters to maintain immutability.
public MainDoor getMainDoor() {
return mainDoor;
}
//... other getters

public static class Builder {
private MainDoor mainDoor;
//... other components

public Builder mainDoor(Customizer<MainDoor.Builder> customizer) {
this.mainDoor = customizer.customize(new MainDoor.Builder()).build();
return this;
}
//... other builder methods

public House build() {
return new House(this);
}
}
}

Building the House

The House object can be built using lambda expressions to customize each component. This approach is intuitive and provides excellent flexibility in object creation.

House house = new House.Builder()
.mainDoor(md -> md.doorType("Wooden Door"))
.terrace(t -> t.view("Garden View").size(300).hasGarden(true))
.lobby(l -> l.style("Modern").area(500).hasFurniture(true))
.mainBedroom(mb -> mb.wallColor("White").bedType("King Size").hasAirConditioner(true))
.guestRoom(gr -> gr.wallColor("Cream").bedType("Queen Size").hasAirConditioner(false))
.build();

Thats it! A robust technique for creating immutable objects in Java. By combining the Builder pattern with a Functional Interface, we gain significant control over object creation.

If you want to play a bit more, go and check the repo.

Note: At least for large configuration objects better to follow this.

Disclaimer: The views reflected in this article are the author’s views and do not necessarily reflect the views of any past or present employer of the author.

--

--