Immutable objects are objects that don't change. You make them, then you can't change them. Instead, if you want to change an immutable object, you must clone it and change the clone while you are creating it.
A Java immutable object must have all its fields be internal, private final
fields. It must not implement any setters. It needs a constructor that takes a value for every single field.
Immutable objects come in handy in multi-threaded environments and in streams. It is great to rely on objects not changing mid-stream. Bugs caused by a thread changing another thread's object are often subtle and are very, very hard to track down. Immutable objects stop these whole class of problems in their tracks.
You don't have to take my word for it - see what experts around the web say.
Contents
- Making an Immutable Object
- Common gotchas
- Primitives
- Collections
- Arrays
- Objects
- How to Change an Immutable Object
- Where to put Builders?
- Handling Bad Data in Immutable Objects
- Wrapup
Gotchas!
Lists, arrays, maps, sets and other non-immutable objects can be surprising. A private final
object with no setter is fixed to the object it was initially assigned, but the values inside that object aren't fixed (unless the object is immutable).
That means you might have an ImmutableShoppingList myShoppingList = new ImmutableShoppingList(new String[] {"apples","anchovies","pasta"})
and expect that the shopping list will always have "apples", "anchovies" and "pasta".
Someone could call myShoppingList.getList()[0] = "candy bars";
and change your list to be "candy bars", "anchovies" and "pasta", which is unhealthy and clearly not what you want.
Primitives
Good news! Primitives are immutable, so you don't have to do anything special.
Collections
Good news! java.util.Collections provides a number of convenience methods that make converting a Collection to an UnmodifiableCollection a snap.
Check out:
Collections.unmodifiableCollection
Collections.unmodifiableList
Collections.unmodifiableMap
Collections.unmodifiableNavigableMap
Collections.unmodifiableNavigableSet
Collections.unmodifiableSet
Collections.unmodifiableSortedMap
Collections.unmodifiableSortedSet
I suggest you store the fields as generic Collections (List, rather than ArrayList), and make the unmodifiable in the constructor, like so:
public class ImmutableShoppingList {
private final List<String> list;
public ImmutableShoppingList(List<String> list){
this.list = Collections.unmodifiableList(list);
}
public List<String> getList(){
return list;
}
}
This allows you to use IDE code generation to make the getters, which is nice, and contains all the input modifiers in one place, which is also nice.
Bad news! If you hang onto the reference to the collection when you create the collection, you can still modify it, even if you store it as an unmodifiable collection internally. Here's an example:
List<String> originalList = new ArrayList<>();
theList.add("apple");
ImmutableShoppingList blah = new ImmutableShoppingList(originalList);
originalList.add("candy bar");
The supposedly immutable shopping list started with an apple, and had a candy bar added to it after creation. What can we do about this?
Clone the list!
public class ImmutableShoppingList {
private final List<String> list;
public ImmutableShoppingList(List<String> list){
List<String> tmpListOfHolding = new ArrayList<>();
tmpListOfHolding.addAll(list);
this.list = Collections.unmodifiableList(tmpListOfHolding);
}
public String[] getList(){
return (String[]) list.toArray();
}
}
When we create the immutable object, we deep clone the collection, which severs the connection to the original reference. Now when we run the "sneak a candy bar in" example, "candy bar" gets added to originalList
, but not the ImmutableShoppingList
.
Arrays
Bad news! Java doesn't have any convenient methods to prevent arrays from being modified. Your best bet is to either hide the original array and always return a clone, or to not use arrays in the underlying implementation and instead convert a Collection object to an array.
I prefer to stick with Collections, but if you must have an array in your object's api, this is the approach I would take:
public class ImmutableShoppingList {
private final List<String> list;
public ImmutableShoppingList(String[] list){
this.list = Collections.unmodifiableList(Arrays.asList(list));
}
public String[] getList(){
return (String[]) list.toArray();
}
}
Objects
Object fields can be easy. If the sub-objects are also immutable, good news! You don't have to do anything special.
If the sub-objects are not immutable, they are a lot like a collection. You need to deep clone them, or the original reference can change your supposedly immutable data out from under your feet.
Often, you end up working with pre-existing mutable objects, either in your codebase, or in libraries. In this case, I like to create an immutable object wrapper class that extends the mutable class. I find a static getInstance(MutableObject obj)
method can be helpful, but a constructor ImmutableObject(MutableObject obj)
is also a useful thing to have.
What About When I Want To Change An Immutable Object?
It happens to everyone. You need an an object, but you don't know everything about the object. You can't quite commit to an immutable object.
In this case, I reach for the builder pattern.
The builder pattern creates a temporary object with the same fields as the desired object. It has getters and setters for all the fields. It also has a build()
method that creates the desired object
Imagine a small immutable object:
class ImmutableDog {
private final String name;
private final int weight
public ImmutableDog(String name, int weight){
this.name = name;
this.weight = weight;
}
public String getName(){
return this.name;
}
public int getWeight(){
return this.weight;
}
}
Here's what the builder would look like:
class ImmutableDogBuilder {
private String name;
private int weight;
public ImmutableDogBuilder(){}
public ImmutableDog build(){
return new ImmutableDog(name, weight);
}
public ImmutableDogBuilder setName(String name){
this.name = name;
return this;
}
public ImmutableDogBuilder setWeight(int weight){
this.weight = weight;
return this;
}
public String getName(){
return this.name;
}
public int getWeight(){
return this.weight;
}
}
Note the setters I really like this pattern of returning this
on each setters in builder classes, because it creates a very fluent api. You could use this ImmutableDogBuilder
like this:
ImmutableDogBuilder dogBuilder = new ImmutableDogBuilder().setName("Rover").setWeight(25);
You can imagine in classes with more fields that this compacts your code a lot.
Where To Put The Builder?
There are two schools of thought here.
On the one hand, you can create a separate class for the builder. This is easy, it is very conventional, and your IDE will probably group the classes together, because they probably have similar names.
On the other hand, you can embed the builder class in the immutable object class as a public static inner class.
I prefer to embed builder classes in immutable objects, because I view the builder as a helper for the immutable object, and not a standalone thing. It keeps them together and tightly coupled.
What About Immutable Objects With Bad Data?
It happens to everybody, especially if you accept input. Bad data!
Bad data is no good, but immutable bad data seems especially wrong - you can't even fix it!
I use two approaches to prevent bad data from getting turned into immutable objects.
My primary approach is a suite of business rules that test for sane, permissible data. The business rules look at builders, and if the builder passes, I deem it ok to create the immutable object.
My secondary approach is to embed a small amount of business logic in the immutable object. I don't allow required fields to be null, and any nullable field is an Optional
.
For example:
class ImmutableDog {
private final String name;
private final Optional<int> weight
public ImmutableDog(String name, Optional<int> weight){
Objects.requireNonNull(name);
this.name = name;
this.weight = weight;
}
public String getName(){
return this.name;
}
public Optional<int> getWeight(){
return this.weight;
}
}
This is an ImmutableDog
that requires a name, but does not require a weight. It's important to know what to call a dog, but it's not strictly necessary to know that Fluffy weighs 15 lbs.
Objects.requireNonNull
will immediately throw a NullPointerException
if a name is not provided. This prevents the creation of a nonsensical immutable object. It also allows users of the immutable object (such as streams or functions) to skip handling nulls. There are no nulls here.
Using Optional<int>
makes consumers of ImmutableDog
immediately aware that they may have to handle a null. Providing the Optional
api gives downstream users an easy, functional way of handling nulls.
Wrapup
Immutable objects require some special care and handling, but their utility is worth it.
The main principles to keep in mind are:
- Clone arrays, Collections and Objects internal to your immutable object
- Use builders when you need a mutable object
- Use Optional to indicate nullable fields in your object's api
- Fail fast on bad data - Objects.requireNonNull can help
Go forth, and stop mutating