Last time, we took a first look at the Immutables library, focusing on the basics of how to generate and use an immutable object. Here, we’ll take a look at how we can use some useful features to make our immutables a bit nicer to work with!
Basic Usage
In part 1, we used a toy example of an image editing app to highlight where our code would benefit from immutability. Using Immutables, we ended up with this Color
class, which is used to generate an ImmutableColor
class:
@Value.Immutable
public abstract class Color {
public abstract String rgb();
public Color lighten() {
return ImmutableColor.builder()
.rgb(/* math */)
.build();
}
}
This works perfectly fine, but we’d like to have some more robust and easier-to-use models.
Factory Methods
First, it’s a little annoying to actually construct our Color
objects. That’s because we’re forced to use a builder—builders are nice when there’s a lot of arguments or configuration, but we only have a single RGB value.
We can generate a static factory method as an alternative way to create ImmutableColor
instances. To do this, we tag the attributes that we care about with a @Value.Parameter
annotation:
@Value.Immutable
public abstract class Color {
@Value.Parameter
public abstract String rgb();
// ...
}
This tells the annotation processor to create a static factory method, named of
, on the generated class. We now have a second way to create an ImmutableColor
instance:
Color black = ImmutableColor.of("000000");
We can use our new factory method inside lighten
:
@Value.Immutable
public abstract class Color {
public abstract String rgb();
public Color lighten() {
return ImmutableColor.of(/* math */);
}
}
Derived Attributes
Right now, our colors are represented as RGB values. There are alternate representations as well, such as HSV—hue, saturation, value. HSV is a bit nicer to work with for color manipulation, so let’s represent our color in both RGB and HSV.
To do this we apply some math to our RGB value and get our three HSV values.
@Value.Immutable
public abstract class Color {
@Value.Parameter
public abstract String rgb();
public List<Double> hsv() {
return /* math */
}
// ...
}
We can now represent our colors as HSV values! Let’s now make our hsv
method an attribute.
Last time, we said that abstract, zero-argument methods with a non-void return type (such as rgb
) are attributes. We can also have attributes that are non-abstract methods. However, these non-abstract methods require an annotation to distinguish them from methods that aren’t intended to be attributes (such as toString
).
In this case, we always want to compute the HSV value for our color—we want a derived attribute. We can make one with the @Value.Derived
annotation. Each time we create an ImmutableColor
instance, that instance will compute and memoize its hsv
value.
@Value.Immutable
public abstract class Color {
@Value.Parameter
public abstract String rgb();
@Value.Derived
public List<Double> hsv() {
return /* math */
}
// ...
}
Lazy Attributes
We’ve now defined three Color
methods:
rgb
, required to create anImmutableColor
instancehsv
, computed once fromrgb
and stored upon creationlighten
, computes a color that is 10% lighter
hsv
is always the same value for a Color
instance, so it’s natural to turn it into an attribute. lighten
is also always the same value for a Color
instance, so let’s turn that into an attribute as well!
Unlike hsv
, however, we don’t want to compute and memoize lighten
each time we create a Color
.
One, we won’t call lighten
very often, so it’s not necessary to precompute it.
Two, we’ll actually end up in an infinite loop! Because lighten
returns a Color
, the computed value will also need to need to compute and memoize its own lighten
value, and so on.
Instead of a derived attribute, we want a lazy attribute. This is done with the @Value.Lazy
annotation. The method will compute its value when it gets called for the first time, then memoize the result for future calls. Lazy attributes are great for expensive computations.
@Value.Immutable
public abstract class Color {
@Value.Parameter
public abstract String rgb();
@Value.Derived
public List<Double> hsv() {
return /* math */
}
@Value.Lazy
public Color lighten() {
return ImmutableColor.of(/* math */);
}
}
As seen here, attributes can also be collections (and arrays). Immutables supports List
, Map
, and Set
, as well as a few more types. By default, collections are backed by unmodifiable collections. If you’re using Guava, they’ll be backed by Guava’s immutable collections instead.
Default Attributes
Let’s add an alpha channel to our colors. Normally, we want our colors to have an alpha value of 1—fully opaque. However, when desired, we should be able to create colors with other alpha values. To do this, we make alpha a default attribute.
@Value.Immutable
public abstract class Color {
@Value.Parameter
public abstract String rgb();
@Value.Derived
public List<Double> hsv() {
return /* math */
}
@Value.Default
public double alpha() {
return 1;
}
@Value.Lazy
public Color lighten() {
return ImmutableColor.of(/* math */);
}
}
Copying Immutables
Sometimes, we want to create a copy of an immutable object with a few attributes changed. For example, given an arbitrary color (with an arbitrary alpha value), we might want to determine its base, fully-opaque color.
We could do this by creating an entirely new object from scratch. This isn’t so bad for our Color
class, but it could get quite tedious if we had a lot of attributes to copy. This code would also become brittle if we add additional attributes to Color
in the future.
// This is brittle!
Color color = // an arbitrary color
Color opaqueColor =
ImmutableColor.builder()
.rgb(color.rgb())
.alpha(1)
.build();
Instead, builders have a from
method that copies all values from our desired object. We can then change any attributes that we want.
Color color = // an arbitrary color
Color opaqueColor =
ImmutableColor.builder()
.from(color)
.alpha(1)
.build();
A final way of copying immutables is via wither methods. For each settable attribute, our generated immutable objects (e.g. ImmutableColor
) have a with*
method. Coupled with copyOf
, we have another way of copying our immutables.
Color color = // an arbitrary color
Color opaqueColor =
ImmutableColor.copyOf(color).withAlpha(1);
These wither methods attempt to copy with structural sharing.
We’ve now taken a look at a few basic features of the Immutables library, but there are lots more. In part 3, we’ll take a look at some more advanced features that allow us to streamline our immutables even more!