Practical Java Praxis 65: Use Inheritance or Delegation to Define Immutable Classes
The
-
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:
class MutableCircle { private double radius; public MutableCircle(double r) { radius = r; } public void setRadius(double r) { radius = r; } public double radius() { return radius; } //... }
To provide this class as an immutable class, you can declare an immutable interface that the mutable class implements, as in this example:
interface ImmutableCircle { public double radius(); } class MutableCircle implements ImmutableCircle { private double radius; public MutableCircle(double r) { radius = r; } public void setRadius(double r) { radius = r; } public double radius() { return radius; } //... }
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:
public class Test { public ImmutableCircle createWheel(double r) { return new MutableCircle(r); } public static void main(String args[]) { Test t = new Test(); ImmutableCircle iWheel = t.createWheel(5.0); iWheel.setRadius(7.4); } }
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:
Test.java:12: Method setRadius(double) not found in interface ImmutableCircle. iWheel.setRadius(7.4); ^ 1 error
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:
public class Test { public ImmutableCircle createWheel(double r) { return new MutableCircle(r); } public static void main(String args[]) { Test t = new Test(); ImmutableCircle iWheel = t.createWheel(5.0); System.out.println("Radius of wheel is " + iWheel.radius()); ((MutableCircle)iWheel).setRadius(7.4); System.out.println("Radius of wheel is now " + iWheel.radius()); } }
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:
interface PinNumbers { public String accountOwner(); public int checkingPin(); public int savingsPin(); } class MutablePinNumbers implements PinNumbers { private String acctOwner; private int checkingAcctPin; private int savingsAcctPin; MutablePinNumbers(String owner, int cPin, int sPin) { acctOwner = owner; checkingAcctPin = cPin; savingsAcctPin = sPin; } public void setAccountOwner(String str) { acctOwner = str; } public String accountOwner() { return acctOwner; } public void setCheckingPin(int pin) { checkingAcctPin = pin; } public int checkingPin() { return checkingAcctPin; } public void setSavingsPin(int pin) { savingsAcctPin = pin; } public int savingsPin() { return savingsAcctPin; } } final class ImmutablePinNumbers implements PinNumbers { private String acctOwner; private int checkingAcctPin; private int savingsAcctPin; ImmutablePinNumbers(String owner, int cPin, int sPin) { acctOwner = owner; checkingAcctPin = cPin; savingsAcctPin = sPin; } public String accountOwner() { return acctOwner; } public int checkingPin() { return checkingAcctPin; } public int savingsPin() { return savingsAcctPin; } }
This technique allows a method to specify the following in its signature:
-
The mutable class, if it requires a mutable object
-
The immutable class, if it wants to preserve immutability
-
The neutral interface or base class, if it does not care about immutability
This solution also prevents the casting problem exposed with the immutable interface class. The immutability of these classes cannot be cast away. For example, consider the following code:
public void foo(MutablePinNumbers p) {} public void bar(ImmutablePinNumbers p) {} MutablePinNumbers m = new MutablePinNumbers("person1", 101, 201); ImmutablePinNumbers im = new ImmutablePinNumbers("person2", 102, 202); foo((MutablePinNumbers)im); //Compiler error bar((ImmutablePinNumbers)m); //Compiler error
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:
class MutableCircle { private double radius; public MutableCircle(double r) { radius = r; } public void setRadius(double r) { radius = r; } public double radius() { return radius; } } final class ImmutableCircle { private MutableCircle mCircle; public ImmutableCircle(double r) { mCircle = new MutableCircle(r); } public double radius() { return mCircle.radius(); } }
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. |
Performance penalty. |