What are immutables?
An immutable is a type that can’t be modified after it has been created.
The most common way to define an object in Java is by instantiating a class; Once the class is instantiated, we can modify its properties directly or by calling methods that modify them. Let’s look at an example of a mutable object:
1
2
3
public class MyClass {
public int value;
}
This is a very simple type that we can instantiate and modify:
1
2
MyClass obj = new MyClass();
obj.value = 4;
Immutables differ from these types in that once instantiated, they can’t be modified (Properties can’t be changed and there are no methods to modify them).
A built in example of an immutable type are strings:
1
String msg = "I'm an immutable string";
Once created the string can’t be modified. We can assign a different string to msg
, but the original string can’t be changed (i.e. we can’t change characters in a string, like we do with char arrays).
Why do we want immutables?
Immutables are an idea that comes from functional programming languages and is somewhat new to Java. It has seen increase adoption in the Java world mostly because it makes it simpler to write predictable programs.
Let’s look at a way a program using mutable objects can be unpredictable:
1
2
3
4
MyClass obj = new MyClass();
obj.value = 4;
doSomething(obj);
doSomethingElse(obj);
This might look like a very predictable piece of code, but if we ask what’s the value of obj.value
when doSomethingElse
is called, we might realize, it’s not that simple.
Since obj
is mutable, the call to doSomething
might have changed obj.value
without us knowing, which makes the call to doSomethingElse
unpredictable.
Another scenario where immutables are useful is in multi-threaded software. One of the most common issues in multi-threaded programing are data races. Using immutables makes it impossible for data races to occur because two threads can’t modify the same data, beacause it can’t be modifed at all.
Why not make everything immutable?
In most useful programs things aren’t static; They change over time. If we model a person’s clothes, we might want to change the color of their clothes from one moment to the other. With mutable objects we might do something like this:
1
2
3
4
5
Clothes clothes = new Clothes();
clothes.color = "blue";
// ... Program keeps doing stuff
clothes.color = "green";
If we use immutable objects, we have to do something like this:
1
2
3
4
Clothes clothes = new Clothes("blue");
// ... Program keeps doing stuff
clothes = clothes.copy("green");
Since we can’t modify the object, we create a copy with different values and assign that copy to the variable.
The problem with copying an object is that it’s more resource consuming than just changing the value of a variable. This overhead might be negligible in many cases, but when working on systems where speed is important, it might be counter productive to use immutables.
Immutables in Java
One way to make immutable objects in java is to use classes that have all their properties defined as private and have no methods that modify any of these properties after construction. For example:
1
2
3
4
5
6
7
8
9
public class MyClass {
private int intProperty;
private String stringProperty;
MyClass(int i, String s) {
intProperty = i;
stringProperty = s;
}
}
Once we construct an object of type MyClass
, it can’t be modified.
One thing that we need to be careful with is when our objects contain properties that are themselves mutable. For example:
1
2
3
4
5
6
7
8
9
10
11
public class MyClass {
private int intProperty;
private String stringProperty;
private List<String> listProperty;
MyClass(int i, String s, List<String> l) {
intProperty = i;
stringProperty = s;
listProperty = l;
}
}
This is not an immutable type because listProperty
is mutable.
Immutables library
The Immutables library provides an annotation processor that makes it easy to create immutable objects.
To get started with it, let’s first see how to install the processor in Bazel.
We will need this directory structure
1
2
3
4
5
.
├── BUILD
├── ExamplePackage.java
├── MyClass.java
└── WORKSPACE
In WORKSPACE
we’ll download the maven artifact for Immutables
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
load('@bazel_tools//tools/build_defs/repo:http.bzl', 'http_archive')
RULES_JVM_EXTERNAL_TAG = '4.1'
http_archive(
name = 'rules_jvm_external',
strip_prefix = 'rules_jvm_external-%s' % RULES_JVM_EXTERNAL_TAG,
url = 'https://github.com/bazelbuild/rules_jvm_external/archive/%s.zip' % RULES_JVM_EXTERNAL_TAG,
)
load('@rules_jvm_external//:defs.bzl', 'maven_install')
maven_install(
artifacts = [
'org.immutables:value:2.8.8'
],
repositories = [
'https://repo1.maven.org/maven2',
],
)
The BUILD
file creates the immutables_processor
and adds it as a dependency for our binary:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
load("@rules_java//java:defs.bzl", "java_library", "java_plugin")
java_binary(
name = 'example_package',
srcs = [
'ExamplePackage.java',
'MyClass.java'
],
main_class = 'example.ExamplePackage',
deps = [
':immutables_processor',
'@maven//:org_immutables_value'
]
)
java_plugin(
name = "immutables_processor",
generates_api = True,
processor_class = "org.immutables.processor.ProxyProcessor",
deps = [
"@maven//:org_immutables_value",
],
)
Using Immutables
annotations we create MyClass.java
:
1
2
3
4
5
6
7
8
9
10
11
package example;
import org.immutables.value.Value;
import java.util.List;
@Value.Immutable
public interface MyClass {
int intProperty();
String stringProperty();
List<String> listProperty();
}
The preprocessor will create a class named ImmutableMyClass
that can then be used to build immutables;
1
2
3
4
5
6
7
8
9
10
11
12
13
package example;
import java.util.Arrays;
public class ExamplePackage {
public static void main(String args[]) {
MyClass a = ImmutableMyClass.builder()
.intProperty(45)
.stringProperty("Hello")
.listProperty(Arrays.asList("one", "two"))
.build();
}
}
Besides the concise syntax that Immutables
offers, it also makes sure that our objects are truly immutable. For example, this code will throw a runtime exception:
1
2
3
4
5
6
7
8
9
10
11
public class ExamplePackage {
public static void main(String args[]) {
MyClass a = ImmutableMyClass.builder()
.intProperty(45)
.stringProperty("Hello")
.listProperty(Arrays.asList("one", "two"))
.build();
a.listProperty().add("three");
}
}
The exception happens because the List
used inside the immutable is wrapped as an UnmodifiableCollection, so the add
method is disallowed.
1
2
3
Exception in thread "main" java.lang.UnsupportedOperationException
at java.base/java.util.Collections$UnmodifiableCollection.add(Collections.java:1060)
at example.ExamplePackage.main(ExamplePackage.java:13)
The Immutables guide provides many examples of the different features it provides, so it’s worth exploring it to learn more.
Conclusion
In this article we learned what are immutables, when they are good and how to use them.
The trend I have seen lately (And I think might be a good approach) is that everything that can be made immutable is made immutable to start with. If performance issues arise, the issue is investigated and a mutable type is used if necessary.
design_patterns
java
programming
]