You can use a number of techniques to define immutable classes in Java. In this article, Java expert Peter Haggar explains the immutable interface, the common interface, and immutable delegation class.

From the author of

From the author of

The first article in this series outlined some advantages of immutable
objects and how to design and implement your own. This article discusses three
additional techniques that you can use to define immutable classes. Each has
its own advantages and disadvantages. The techniques discussed are listed here:

Immutable interface

Common interface or base class

Immutable delegation class

Immutable Interface

Assume that you have an existing mutable class, MutableCircle, that
represents a circle. Because of the thread-safety advantages of an immutable
object, you want to let other code access an object of this class as an immutable
object. The original MutableCircle class looks like this:

Because the immutable interface exposes only the nonmutating methods of the
underlying class, access to the object through the interface type preserves
immutability. This allows you to use the immutable interface to prevent mutation.
For example, the following code returns a reference to the MutableCircle
object through the ImmutableCircle interface type, thereby properly preventing
this code from compiling:

Note that the createWheel method returns a reference to an ImmutableCircle
object. Objects of type ImmutableCircle can access only methods defined
in the ImmutableCircle interface. In this case, the only method available
is the nonmutating radius method. Attempts to access the methods of MutableCircle
from an ImmutableCircle object reference are flagged by the compiler.
Compiling the previous code results in the following error message:

This is what you want to happen with code written in this way. This design,
however, has a flaw. It works until the users of this class realize how to get
around the immutability constraints you have established with the interface.
Consider the following code, which breaks these immutability constraints:

This code not only compiles cleanly, but it also generates the following output:

Radius of wheel is 5.0
Radius of wheel is now 7.4

The output shows that the supposedly immutable ImmutableCircle object
has been altered. With this approach, however, users of the ImmutableCircle
class can easily expunge its immutability with a simple cast. Remember, an interface
declares a reference type. Therefore, an object reference of type ImmutableCircle
can be cast to its derived type of MutableCircle. An object reference
cast to a MutableCircle then can access the methods of this class and
break immutability.

Because the programmer must extend the effort to code the cast, you might think
that this serves as enough of a deterrent. Nevertheless, the mutability constraints
can be breached.

Common Interface or Base Class

Preventing breaches of immutability requires another approach. One is to use
one common interface or base class and two derived classes. These are organized
as follows:

An interface or abstract base class that contains the immutable methods
that are common for its derived classes

A derived class that provides a mutable implementation

A derived class that provides an immutable implementation

For example, you might design an interface and two derived classes like this:

Method foo takes an object reference of MutablePinNumbers as
a parameter. Therefore, it can access the mutating methods of the MutablePinNumbers
class. By contrast, method bar takes an object reference of type ImmutablePinNumbers
as a parameter. Therefore, it cannot change the object referred to by parameter
p. The object remains immutable for the duration of this method. If code
tries to cast between these two types, the compiler generates an error.

This implementation ensures that the immutability constraints cannot be breached
by a simple cast.

Immutable Delegation Class

Another approach uses an immutable delegation class. This class contains only
immutable methods and delegates these calls to the mutable object that it contains.
For example, returning to the circle classes, the delegation technique looks
like this:

The ImmutableCircle class uses layering, or the "has-a" relationship,
with the MutableCircle class. When you create an ImmutableCircle
object, you also create a MutableCircle object. Users of the ImmutableCircle
object, however, cannot access the underlying MutableCircle object. They
can access only the immutable methods provided in the ImmutableCircle
class. Unlike the earlier immutable interface example, the user of these classes
cannot cast between them.

This solution is particularly useful when you are unable to modify an existing
mutable class. For example, the class might be part of a library you are using,
and you do not have access to the source code to use the other techniques. In
this case, you can use the layering approach.

However, this solution has a downside. Coding the delegation model requires
more work to implement and more effort to understand and maintain. In addition,
a performance penalty is associated with each delegated method call. Consider
these factors before deciding which technique to use.

Table 1 lists the advantages and disadvantages of the techniques to provide
immutable objects.

Table 1

Immutability Techniques

Technique

Advantages

Disadvantages

Immutable interface

Easy and straightforward.
No performance penalty.

Can be breached

Common interface or base class

Cannot be breached.
Clean way to separate mutable objects from immutable objects.

Extra classes to implement.
Deeper class hierarchy.

Immutable delegation class

Cannot be breached.
Useful when you cannot change the source of an existing mutable class.