Using immutable objects in Java (and other programming languages as well) is a good thing because immutable objects may be shared safely, are thread-safe and reduce the risk of side effects in your applications.

Nowadays multiple frameworks exist to reduce the need of writing boilerplate code here but there is one special framework whose features I’d like to demonstrate in the following short tutorial.

It hooks into your application using annotation processing and generates type-safe builders, toString, hashCode, equals methods for you, supports lazy attributes, singleton instances, serialization into data-formats like JSON and a lot of other features,too.

immutables using builders 1024x588
Figure 1. Immutables - Using generated Builders

Dependencies

We need to add just one dependency to our project’s pom.xml (using Maven).

In addition we should assure, that our IDE’s Maven integration supports annotation processing – e.g. when using Eclipse IDE, there is the m2eclipse apt Plugin easy installable from the Eclipse Marketplace.

For more detailed information there is a dedicated article in the project’s documentation here.

<dependency>
  <groupId>org.immutables</groupId>
  <artifactId>value</artifactId>
  <version>2.0.7</version>
  <scope>provided</scope>
</dependency>

Example 1: Builder for Interface

In our first example, we’re using specifying an immutable class derived from an interface.

Adding org.immutables.value.Value.Immutable as class level annotation is all we need – afterwards we have a mind-blown immutable class named ImmutableBook with equals, hashCode, toString implemented and a nice builder that we’re going to use in the following example code.

One special fact here is the usage of Java 8′s Optional: Every other parameter is mandatory when using the builder to create a new instance but the optional one’s may be left out. We will demonstrate this behaviour in the following sample code, too.

This is our Book interface:

package com.hascode.tutorial.example1;

import java.util.List;
import java.util.Optional;

import org.immutables.value.Value;

@Value.Immutable
public interface Book {
	String title();

	Optional<String> excerpt();

	Float price();

	List<String> tags();

}

This is our library interface – we’re using it the same way we’re using the book interface.

package com.hascode.tutorial.example1;

import java.util.List;

import org.immutables.value.Value;

@Value.Immutable
public interface Library {
	String name();

	List<Book> books();

	boolean opened();
}

Now our example: We’re using the builder to create two books first. The second book does not need to specify an excerpt because it is optional.

Finally we’re using the Library’s builder to create an immutable library and add the books.

At last, we’re printing the library to demonstrate the generated toString method.

package com.hascode.tutorial.example1;

public class BuilderForInterfaceExample {

	public static void main(final String[] args) {
		Book book1 = ImmutableBook.builder().title("One first book").excerpt("Lorem ipsum dolor sit.").addTags("foo", "bar", "baz").price(12.5F).build();
		Book book2 = ImmutableBook.builder().title("Another book").addTags("xoxo", "trololol").price(20.2F).build();
		Library library = ImmutableLibrary.builder().name("My first library").opened(true).addBooks(book1, book2).build();
		System.out.println(library.toString());
	}
}

We may now run the example using our IDE our Maven from the command line like this:

$ mvn exec:java -Dexec.mainClass=com.hascode.tutorial.example1.BuilderForInterfaceExample
Library{name=My first library, books=[Book{title=One first book, excerpt=Optional[Lorem ipsum dolor sit.], price=12.5, tags=[foo, bar, baz]}, Book{title=Another book, excerpt=Optional.empty, price=20.2, tags=[xoxo, trololol]}], opened=true}

Example 2: Builder for Abstract Class

In the second example we’re doing basically the same but our immutable class is based on an abstract class here.

package com.hascode.tutorial.example2;

import java.util.List;
import java.util.Optional;

import org.immutables.value.Value;

@Value.Immutable
public abstract class AbstractPerson {
	abstract String getName();

	abstract List<String> getHobbies();

	abstract Optional<Integer> getAge();
}

Again we’re using the immutable object’s builder to create a new instance and print it afterwards:

package com.hascode.tutorial.example2;

public class BuilderForAbstractClassExample {

	public static void main(final String[] args) {
		AbstractPerson person = ImmutablePerson.builder().age(22).addHobbies("sports", "travelling").name("Ted").build();
		System.out.println(person.toString());
	}

}

We may now run the example using our IDE our Maven from the command line like this:

$ mvn exec:java -Dexec.mainClass=com.hascode.tutorial.example2.BuilderForAbstractClassExample
Person{name=Ted, hobbies=[sports, travelling], age=Optional[22]}

Example 3: Strict Builder Mode

Another nice feature of Immutables is the builder’s strict mode. This mode forbids a second modification of an object’s field when using the dedicated builder method and throws an exception.

This may be handy sometimes to catch copy-and-paste errors or other errors that may occur due to multiple modifications in one build process.

Adding the annotation @Value.Style allows use to activate the strict-builder-mode and a variety of other features – from the naming of the generated builders to the naming of the setters etc..

package com.hascode.tutorial.example3;

import org.immutables.value.Value;

@Value.Immutable
@Value.Style(strictBuilder = true)
public interface Box {
	double width();

	double height();
}

In the following example, the first box is constructed without a problem but the second one tries to modify the height attribute twice and therefore an IllegalStateException is thrown:

package com.hascode.tutorial.example3;

public class StrictBuilderExample {

	public static void main(final String[] args) {
		Box box1 = ImmutableBox.builder().width(12).height(20).build(); // works

		// throws illegal-state exception, height has been already set!
		Box box2 = ImmutableBox.builder().width(12).height(20).height(21).build();
	}

}

We may now run the example using our IDE our Maven from the command line like this and should receive the following exception:

$ mvn exec:java -Dexec.mainClass=com.hascode.tutorial.example3.StrictBuilderExample
Caused by: java.lang.IllegalStateException: Builder of Box is strict, attribute is already set: height
        at com.hascode.tutorial.example3.ImmutableBox$Builder.checkNotIsSet(ImmutableBox.java:190)
        at com.hascode.tutorial.example3.ImmutableBox$Builder.height(ImmutableBox.java:165)
        at com.hascode.tutorial.example3.StrictBuilderExample.main(StrictBuilderExample.java:9)
        ... 6 more

Example 4: Constructor Methods

Another way to create our immutable objects is using constructor methods / static factory methods. We may specify parameters for our this constructor method by adding the annotation @Value.Parameter to the dedicated fields.

package com.hascode.tutorial.example4;

import org.immutables.value.Value;

@Value.Immutable
public interface Link {
	public enum Protocol {
		HTTP, HTTPS
	};

	@Value.Parameter
	String url();

	@Value.Parameter
	Protocol protocol();
}

Now we’re able to create new instances using the constructor method____- in this case: ImmutableLink.of()

package com.hascode.tutorial.example4;

import com.hascode.tutorial.example4.Link.Protocol;

public class ConstructorMethodExample {

	public static void main(final String[] args) {
		Link link1 = ImmutableLink.of("www.hascode.com", Protocol.HTTP);
		Link link2 = ImmutableLink.builder().url("www.hascode.com").protocol(Protocol.HTTP).build();
		System.out.println("link1 equals link2: " + link1.equals(link2));
	}
}

We may now run the example using our IDE our Maven from the command line like this:

$ mvn exec:java -Dexec.mainClass=com.hascode.tutorial.example4.ConstructorMethodExample
link1 equals link2: true

Example 5: Lazy Attribute Evaluation

Another feature of interest is the possibility of lazy evaluation and memoization of a method.

In the following example, we’re collecting a list of strings – the lazy evaluated method counts the amount of total characters from all these strings.

package com.hascode.tutorial.example5;

import java.util.List;

import org.immutables.value.Value;

@Value.Immutable
public abstract class TagCloud {
	abstract List<String> tags();

	@Value.Lazy
	public int charsCollected() {
		System.out.println("charCollected() called");
		return tags().stream().reduce("", (n, p) -> n + p).length();
	};
}

This is our example code:

package com.hascode.tutorial.example5;

public class LazyAttributesExample {
	public static void main(final String[] args) {
		TagCloud cloud = ImmutableTagCloud.builder().addTags("foo", "bar", "baz", "bleh", "xoxo").build();
		System.out.println("chars collected: " + cloud.charsCollected());
		System.out.println("chars collected: " + cloud.charsCollected());
	}
}

We may now run the example using our IDE our Maven from the command line like this. As we can see, the lazy-attribute method is called only once.

$ mvn exec:java -Dexec.mainClass=com.hascode.tutorial.example5.LazyAttributesExample
charCollected() called
chars collected: 17
chars collected: 17

Example 6: Check Methods

Finally, we’re able to validate our build process by specifying a check method to verify the state of our object.

In the following example, constructing a new instance of Human fails if the name’s length is less than 3.

We’re applying the validation method by adding the annotation @Value.Check.

package com.hascode.tutorial.example6;

import org.immutables.value.Value;

@Value.Immutable
public abstract class Human {
	abstract String name();

	@Value.Check
	protected void validate() {
		if (name().length() < 3) {
			throw new IllegalArgumentException("name is too short");
		}
	}
}

In the following example code, we’re creating two Humans – the first one is created without a problem – the second one fails due to the name-length constraint.

package com.hascode.tutorial.example6;

public class CheckMethodExample {
	public static void main(final String[] args) {
		Human human1 = ImmutableHuman.builder().name("Ted").build();
		System.out.println(human1);

		// fails with iae
		Human human2 = ImmutableHuman.builder().name("Te").build();
	}

}

We may now run the example using our IDE our Maven from the command line like this and receive an error because our pre-check fails:

$ mvn exec:java -Dexec.mainClass=com.hascode.tutorial.example6.CheckMethodExample
Caused by: java.lang.IllegalArgumentException: name is too short
        at com.hascode.tutorial.example6.Human.validate(Human.java:12)
        at com.hascode.tutorial.example6.ImmutableHuman.validate(ImmutableHuman.java:88)
        at com.hascode.tutorial.example6.ImmutableHuman.access$1(ImmutableHuman.java:87)
        at com.hascode.tutorial.example6.ImmutableHuman$Builder.build(ImmutableHuman.java:160)
        at com.hascode.tutorial.example6.CheckMethodExample.main(CheckMethodExample.java:10)
        ... 6 more

Tutorial Sources

Please feel free to download the tutorial sources from my GitHub repository, fork it there or clone it using Git:

git clone https://github.com/hascode/immutables-examples.git