Immutability Library: Immutables

Immutables

In the previous article, Immutability with Lombok builder pattern, we discovered how we can achieve immutable objects in a very easy and convenient way. In this article, we are going to look at an alternative immutability library called Immutables.

Immutability Library

The alternative immutability library, Immutables, is an open source project which is operating under the Apache License 2.0. It is quite an interesting project which aims to make it easier to create immutable objects by letting users create Java classes with the processing of annotations so that you don’t have to write unnecessary boilerplate code. Sort of like Lombok, but with a more focus on immutable objects as the name suggests.

Preparing your IDE

The processing of annotations generates source classes for us, which depending on your IDE needs to be handled. I ran into some problems here with IntelliJ, so I figured it would be a good idea to share how I set up my IntelliJ to be able to handle the Immutables API.

If you don’t use IntelliJ I recommend you to simply google how to enable it, Immutables should be supported in most of the popular IDE’s.

First of all, we need to enable the autoprocessing of annotations. You can do that by opening the settings for IntelliJ and searching for Annotation Processors, and there you will want to select your module and make sure that the checkbox for Enable annotation processing and Obtain processors from classpath is selected, like the picture below.

Enabling annotation processors in IntelliJ to work with the immutability library

Additionally, as I mentioned, the annotations generates source classes for us, so we will need to make sure that the source directory is included marked as a source instead of excluded which it was for me. You can do that by going to the Project Structure menu, and going to modules, and press on generated-sources and make sure that it is marked as a Source, like the picture below.

Marking generated-sources as a source directory

And that’s it, now we should be ready to get going.

Getting started

It’s easy to get started with either Maven or Gradle. For maven simply add the following dependency.

<dependency>
  <groupId>org.immutables</groupId>
  <artifactId>value</artifactId>
  <version>2.5.6</version>
</dependency>

Immutables supports two different ways of generating classes, either by using an interface or by using an abstract class. They are both very similar, and personally, I prefer to stick with interfaces. They will both be processed by annotations to generate boilerplate code for you.

Let’s start with a basic example, where we have 3 required fields and an optional one. Immutables are following good programming praxises added since Java 8 with preventing NullPointerException using java.util.Optional, check extending the builder pattern with null safety for more details. However, if you don’t like to use Optional for some reason, you can annotate an object with @Nullable. If you don’t wrap the object in an Optional or annotate it as @Nullable, then it will be treated as a required object and trigger an IllegalStateException during construction if the field is missing.

@Value.Immutable
public interface Person {

  String getName();
  String getCity();
  int getAge();
  List<String> getChildren();
  Optional<String> getCar();

}

The same could be written as an abstract class if you instead would prefer that.

@Value.Immutable
public abstract class Person {

  public abstract String getName();
  public abstract String getCity();
  public abstract int getAge();
  public abstract List<String> getChildren();
  public abstract Optional<String> getCar();
 
}

As mentioned, I prefer to use interfaces as it requires even less code. These classes will automatically generate a class that looks like the following.


@SuppressWarnings({"all"})
@Generated({"Immutables.generator", "Person"})
public final class ImmutablePerson implements Person {
  private final String name;
  private final @Nullable String city;
  private final int age;
  private final List<String> children;
  private final String car;

  private ImmutablePerson(
      String name,
      @Nullable String city,
      int age,
      List<String> children,
      String car) {
    this.name = name;
    this.city = city;
    this.age = age;
    this.children = children;
    this.car = car;
  }

  /**
   * @return The value of the {@code name} attribute
   */
  @Override
  public String getName() {
    return name;
  }

  /**
   * @return The value of the {@code city} attribute
   */
  @Override
  public @Nullable String getCity() {
    return city;
  }

  /**
   * @return The value of the {@code age} attribute
   */
  @Override
  public int getAge() {
    return age;
  }

  /**
   * @return The value of the {@code children} attribute
   */
  @Override
  public List<String> getChildren() {
    return children;
  }

  /**
   * @return The value of the {@code car} attribute
   */
  @Override
  public Optional<String> getCar() {
    return Optional.ofNullable(car);
  }

  /**
   * Copy the current immutable object by setting a value for the {@link Person#getName() name} attribute.
   * An equals check used to prevent copying of the same value by returning {@code this}.
   * @param value A new value for name
   * @return A modified copy of the {@code this} object
   */
  public final ImmutablePerson withName(String value) {
    if (this.name.equals(value)) return this;
    String newValue = Objects.requireNonNull(value, "name");
    return new ImmutablePerson(newValue, this.city, this.age, this.children, this.car);
  }

  /**
   * Copy the current immutable object by setting a value for the {@link Person#getCity() city} attribute.
   * An equals check used to prevent copying of the same value by returning {@code this}.
   * @param value A new value for city (can be {@code null})
   * @return A modified copy of the {@code this} object
   */
  public final ImmutablePerson withCity(@Nullable String value) {
    if (Objects.equals(this.city, value)) return this;
    return new ImmutablePerson(this.name, value, this.age, this.children, this.car);
  }

  /**
   * Copy the current immutable object by setting a value for the {@link Person#getAge() age} attribute.
   * A value equality check is used to prevent copying of the same value by returning {@code this}.
   * @param value A new value for age
   * @return A modified copy of the {@code this} object
   */
  public final ImmutablePerson withAge(int value) {
    if (this.age == value) return this;
    return new ImmutablePerson(this.name, this.city, value, this.children, this.car);
  }

  /**
   * Copy the current immutable object with elements that replace the content of {@link Person#getChildren() children}.
   * @param elements The elements to set
   * @return A modified copy of {@code this} object
   */
  public final ImmutablePerson withChildren(String... elements) {
    List<String> newValue = createUnmodifiableList(false, createSafeList(Arrays.asList(elements), true, false));
    return new ImmutablePerson(this.name, this.city, this.age, newValue, this.car);
  }

  /**
   * Copy the current immutable object with elements that replace the content of {@link Person#getChildren() children}.
   * A shallow reference equality check is used to prevent copying of the same value by returning {@code this}.
   * @param elements An iterable of children elements to set
   * @return A modified copy of {@code this} object
   */
  public final ImmutablePerson withChildren(Iterable elements) {
    if (this.children == elements) return this;
    List<String> newValue = createUnmodifiableList(false, createSafeList(elements, true, false));
    return new ImmutablePerson(this.name, this.city, this.age, newValue, this.car);
  }

  /**
   * Copy the current immutable object by setting a present value for the optional {@link Person#getCar() car} attribute.
   * @param value The value for car
   * @return A modified copy of {@code this} object
   */
  public final ImmutablePerson withCar(String value) {
    String newValue = Objects.requireNonNull(value, "car");
    if (Objects.equals(this.car, newValue)) return this;
    return new ImmutablePerson(this.name, this.city, this.age, this.children, newValue);
  }

  /**
   * Copy the current immutable object by setting an optional value for the {@link Person#getCar() car} attribute.
   * An equality check is used on inner nullable value to prevent copying of the same value by returning {@code this}.
   * @param optional A value for car
   * @return A modified copy of {@code this} object
   */
  public final ImmutablePerson withCar(Optional<String> optional) {
    String value = optional.orElse(null);
    if (Objects.equals(this.car, value)) return this;
    return new ImmutablePerson(this.name, this.city, this.age, this.children, value);
  }

  /**
   * This instance is equal to all instances of {@code ImmutablePerson} that have equal attribute values.
   * @return {@code true} if {@code this} is equal to {@code another} instance
   */
  @Override
  public boolean equals(Object another) {
    if (this == another) return true;
    return another instanceof ImmutablePerson
        && equalTo((ImmutablePerson) another);
  }

  private boolean equalTo(ImmutablePerson another) {
    return name.equals(another.name)
        && Objects.equals(city, another.city)
        && age == another.age
        && children.equals(another.children)
        && Objects.equals(car, another.car);
  }

  /**
   * Computes a hash code from attributes: {@code name}, {@code city}, {@code age}, {@code children}, {@code car}.
   * @return hashCode value
   */
  @Override
  public int hashCode() {
    int h = 5381;
    h += (h << 5) + name.hashCode();
    h += (h << 5) + Objects.hashCode(city);
    h += (h << 5) + age;
    h += (h << 5) + children.hashCode();
    h += (h << 5) + Objects.hashCode(car);
    return h;
  }

  /**
   * Prints the immutable value {@code Person} with attribute values.
   * @return A string representation of the value
   */
  @Override
  public String toString() {
    StringBuilder builder = new StringBuilder("Person{");
    builder.append("name=").append(name);
    if (city != null) {
      builder.append(", ");
      builder.append("city=").append(city);
    }
    builder.append(", ");
    builder.append("age=").append(age);
    builder.append(", ");
    builder.append("children=").append(children);
    if (car != null) {
      builder.append(", ");
      builder.append("car=").append(car);
    }
    return builder.append("}").toString();
  }

  /**
   * Creates an immutable copy of a {@link Person} value.
   * Uses accessors to get values to initialize the new immutable instance.
   * If an instance is already immutable, it is returned as is.
   * @param instance The instance to copy
   * @return A copied immutable Person instance
   */
  public static ImmutablePerson copyOf(Person instance) {
    if (instance instanceof ImmutablePerson) {
      return (ImmutablePerson) instance;
    }
    return ImmutablePerson.builder()
        .from(instance)
        .build();
  }

  /**
   * Creates a builder for {@link ImmutablePerson ImmutablePerson}.
   * @return A new ImmutablePerson builder
   */
  public static ImmutablePerson.Builder builder() {
    return new ImmutablePerson.Builder();
  }

  /**
   * Builds instances of type {@link ImmutablePerson ImmutablePerson}.
   * Initialize attributes and then invoke the {@link #build()} method to create an
   * immutable instance.
   * {@code Builder} is not thread-safe and generally should not be stored in a field or collection,
   * but instead used immediately to create instances.
   */
  public static final class Builder {
    private static final long INIT_BIT_NAME = 0x1L;
    private static final long INIT_BIT_AGE = 0x2L;
    private long initBits = 0x3L;

    private String name;
    private String city;
    private int age;
    private List<String> children = new ArrayList();
    private String car;

    private Builder() {
    }

    /**
     * Fill a builder with attribute values from the provided {@code Person} instance.
     * Regular attribute values will be replaced with those from the given instance.
     * Absent optional values will not replace present values.
     * Collection elements and entries will be added, not replaced.
     * @param instance The instance from which to copy values
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder from(Person instance) {
      Objects.requireNonNull(instance, "instance");
      name(instance.getName());
      @Nullable String cityValue = instance.getCity();
      if (cityValue != null) {
        city(cityValue);
      }
      age(instance.getAge());
      addAllChildren(instance.getChildren());
      Optional carOptional = instance.getCar();
      if (carOptional.isPresent()) {
        car(carOptional);
      }
      return this;
    }

    /**
     * Initializes the value for the {@link Person#getName() name} attribute.
     * @param name The value for name 
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder name(String name) {
      this.name = Objects.requireNonNull(name, "name");
      initBits &= ~INIT_BIT_NAME;
      return this;
    }

    /**
     * Initializes the value for the {@link Person#getCity() city} attribute.
     * @param city The value for city (can be {@code null})
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder city(@Nullable String city) {
      this.city = city;
      return this;
    }

    /**
     * Initializes the value for the {@link Person#getAge() age} attribute.
     * @param age The value for age 
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder age(int age) {
      this.age = age;
      initBits &= ~INIT_BIT_AGE;
      return this;
    }

    /**
     * Adds one element to {@link Person#getChildren() children} list.
     * @param element A children element
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder addChildren(String element) {
      this.children.add(Objects.requireNonNull(element, "children element"));
      return this;
    }

    /**
     * Adds elements to {@link Person#getChildren() children} list.
     * @param elements An array of children elements
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder addChildren(String... elements) {
      for (String element : elements) {
        this.children.add(Objects.requireNonNull(element, "children element"));
      }
      return this;
    }

    /**
     * Sets or replaces all elements for {@link Person#getChildren() children} list.
     * @param elements An iterable of children elements
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder children(Iterable<String> elements) {
      this.children.clear();
      return addAllChildren(elements);
    }

    /**
     * Adds elements to {@link Person#getChildren() children} list.
     * @param elements An iterable of children elements
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder addAllChildren(Iterable<String> elements) {
      for (String element : elements) {
        this.children.add(Objects.requireNonNull(element, "children element"));
      }
      return this;
    }

    /**
     * Initializes the optional value {@link Person#getCar() car} to car.
     * @param car The value for car
     * @return {@code this} builder for chained invocation
     */
    public final Builder car(String car) {
      this.car = Objects.requireNonNull(car, "car");
      return this;
    }

    /**
     * Initializes the optional value {@link Person#getCar() car} to car.
     * @param car The value for car
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder car(Optional car) {
      this.car = car.orElse(null);
      return this;
    }

    /**
     * Builds a new {@link ImmutablePerson ImmutablePerson}.
     * @return An immutable instance of Person
     * @throws java.lang.IllegalStateException if any required attributes are missing
     */
    public ImmutablePerson build() {
      if (initBits != 0) {
        throw new IllegalStateException(formatRequiredAttributesMessage());
      }
      return new ImmutablePerson(name, city, age, createUnmodifiableList(true, children), car);
    }

    private String formatRequiredAttributesMessage() {
      List attributes = new ArrayList();
      if ((initBits & INIT_BIT_NAME) != 0) attributes.add("name");
      if ((initBits & INIT_BIT_AGE) != 0) attributes.add("age");
      return "Cannot build Person, some of required attributes are not set " + attributes;
    }
  }

  private static  List createSafeList(Iterable iterable, boolean checkNulls, boolean skipNulls) {
    ArrayList<String> list;
    if (iterable instanceof Collection) {
      int size = ((Collection) iterable).size();
      if (size == 0) return Collections.emptyList();
      list = new ArrayList<>();
    } else {
      list = new ArrayList<>();
    }
    for (T element : iterable) {
      if (skipNulls && element == null) continue;
      if (checkNulls) Objects.requireNonNull(element, "element");
      list.add(element);
    }
    return list;
  }

  private static  List createUnmodifiableList(boolean clone, List<String> list) {
    switch(list.size()) {
    case 0: return Collections.emptyList();
    case 1: return Collections.singletonList(list.get(0));
    default:
      if (clone) {
        return Collections.unmodifiableList(new ArrayList(list));
      } else {
        if (list instanceof ArrayList) {
          ((ArrayList) list).trimToSize();
        }
        return Collections.unmodifiableList(list);
      }
    }
  }
}
 

Let’s add some tests to verify that the construction works and that the mandatory and optional parameters are considered and works as expected.

@Test
public void testConstruction() {
  Person person = ImmutablePerson.builder()
      .name("Viktor")
      .city("Stockholm")
      .age(24)
      .build();

  assertEquals(person.getName(), "Viktor");
}

@Test (expected = IllegalStateException.class)
public void testConstructionFailDueToMissingRequiredField() {
  ImmutablePerson.builder()
      .name("Viktor")
      .city("Stockholm")
      .build();
}

Everything good so far, we have turned around 400+ lines of code into less than 10. That’s a massive amount of boilerplate code removed. Isn’t that amazing? Our object is in fact also completely immutable, meaning that all fields are final. In fact, after building our object there isn’t even any way modify the fields, no setters or adders are provided which means that it is completely thread-safe. However, we are provided with a convenient method for modifying fields, but it will copy the object and all the fields inside. So, for example, if we provide a list to the initial object and then modify the list, it will not be the same list that we modify.

@Test
public void verifyThatObjectIsImmutable() {
  List<String> children = new ArrayList<>();
  children.add("Viktor");
  children.add("Jenny");
  children.add("Thea");

  Person person1 = ImmutablePerson.builder()
      .name("Joakim")
      .age(50)
      .city("Stockholm")
      .children(children)
      .car("BMW")
      .build();

  Person person2 = ImmutablePerson.builder()
      .from(person1)
      .addChildren("Vera")
      .build();

  assertEquals(3, children.size());
  assertNotEquals(person1, person2);
}

In fact, the list isn’t even modifiable outside of the object that owns the list, attempting to do that will result in an UnsupportedOperationException.

@Test (expected = UnsupportedOperationException.class)
  public void verifyListIsNotModifiable() {
  List<String> children = new ArrayList<>();
  children.add("Viktor");
  children.add("Jenny");
  children.add("Thea");

  Person person1 = ImmutablePerson.builder()
      .name("Joakim")
      .age(50)
      .city("Stockholm")
      .children(children)
      .car("BMW")
      .build();

  List children2 = person1.getChildren();
  children2.add("Vera");
}

Alternative static constructor

Immutables also makes it possible to have a convenient static constructor of. This can be done by annotating fields with the annotation @Value.Parameter. However, please make sure that all mandatory fields are provided here, or you won’t even be able to compile the code.

@Test
public void createObjectWithOfConstructor() {
  PersonWithOfConstructor personWithOfConstructor = ImmutablePersonWithOfConstructor.of("Viktor", 24);
  assertEquals("Viktor", personWithOfConstructor.getName());
}

When using an abstract class instead of an interface it is possible to add a default method by annotating @Value.Default. Which means that you do what we did above with the @Value.Parameter and skip some mandatory fields as long as you provide a default method for them.

@Value.Default
public String getName() {
  return "Anonymous";
}

Modifiable

Immutables are nice and all, but sometimes you might, for some reason, want to be able to modify the object directly. This is possible by annotating the class with @Value.Modifiable. It is worth to mention though that the support for it is quite limited as immutables, as the name suggests, heavily favors immutable objects.

Summary

Immutables is an extremely powerful immutability library to build immutable and thread-safe objects with very little boilerplate code required which makes it very convenient. The immutability library reminds quite a lot of Lombok, they both reduce boilerplate code. I would say that Lombok is much more mature. However, it lacks the same good support for immutable objects which is something that immutables excels on. So my advice is to stick to Lombok if you simply wanna reduce boilerplate code, but if you prefer immutables and functional programming in general you should definitely check out Immutables. They have loads of features than I’ve gone through, the features that I brought up is the ones I deemed most important to get intrigued and started with it. You can check out the other features supported by Immutables here.

You may also like

Leave a Reply

Your email address will not be published. Required fields are marked *