In my last post I discussed some of the benefits of immutable objects as well as how to implement them in Java and other languages. In this post I will introduce unmodifiable objects and discuss how to implement them and some of their uses.

Unmodifiable Objects

Unmodifiable objects are sort of a mix between immutable and mutable objects. Externally, they look like immutable objects because their public interface does not allow modifications. However, they may be internally modified through other means.

I am taking the term “unmodifiable” from the Java Collections Framework1, specifically the Collections.unmodifiableX() methods, so let’s start with an example using one of those.

List<String> mutableList = new ArrayList<>();
mutableList.add("foo");

List<String> unmodifiableList = Collections.unmodifiableList(mutableList);
System.out.println(unmodifiableList.get(0)); // "foo"

mutableList.set(0, "bar");
System.out.println(unmodifiableList.get(0)); // "bar"

unmodifiableList.set(0, "baz"); // Runtime error.

There are a few things to note about this example. First, this example doesn’t do anything useful, so don’t worry if you don’t yet see the purpose of unmodifiable objects. In future sections I will try to provide more practical examples. Second, the unmodifiable list is simply a read-only view of the mutable list; it is only externally unmodifiable. If the underlying list changes, so will the unmodifiable view. This is why get(0) returns different values in each of the print statements.

Lastly, and most importantly, this example will always throw a runtime exception — specifically UnsupportedOperationException — on the last line. This is the primary motivation for this post. The unmodifiable collection looks exactly like a normal collection, which has the advantage of allowing an unmodifiable collection to be used anywhere that a normal collection can be used, but it forces the programmer to keep track of whether or not they are allowed to modify a particular collection or risk a runtime exception.

Implementing Unmodifiable Objects

Unmodifiable Wrapper

One of the simplest ways to implement an unmodifiable object is to just hold a reference to a regular object and delegate to that object for all accessor methods.

class UnmodifiablePoint {
    private final Point point;

    public UnmodifiablePoint(Point point) {
        this.point = point;
    }

    public int getX() {
        return this.point.getX();
    }

    public int getY() {
        return this.point.getY();
    }
}

For mutator methods, you can either simply not provide those methods, as in the above example; or, if you want to implement a common interface, you can throw an UnsupportedOperationException, which is what the collections produced by the Collections.unmodifiableX() methods do.

class UnmodifiablePoint implements Point {
    private final Point point;

    public UnmodifiablePoint(Point point) {
        this.point = point;
    }

    public int getX() {
        return this.point.getX();
    }

    public void setX(int x) {
        throw new UnsupportedOperationException();
    }

    public int getY() {
        return this.point.getY();
    }

    public void setY(int y) {
        throw new UnsupportedOperationException();
    }
}

This approach is convenient because it allows you to use existing types, but it is limited in that it only provides runtime guarantees of unmodifiability. Both of these approaches also require the creation of a new object. In the next section I will describe an alternative that does not require creating a new object while also providing compile-time guarantees that an object is not modified. However, it will most likely require creating the type hierarchy from scratch; you would not be able to extend an existing class or interface.

Unmodifiable and Immutable Object Type Hierarchy

If you recall back to the Rust example in my previous post, the compiler was able to prevent calls to setter methods depending on whether or not a variable was declared as mutable.

let mut p1 = Point::new(1, 2);
p1.set_x(3);

let p2 = Point::new(4, 5);
p2.set_x(6); // Compiler error.

Java doesn’t have anything like Rust’s mut keyword, but we can take advantage of polymorphism to get similar behavior2.

In Java it might look something like this:

MutablePoint p1 = Point.create(1, 2);
p1.setX(3);

Point p2 = Point.create(4, 5)
// Compiler error. `p2` does not have a `setX()` method.
p2.setX(6);

Note that in the Rust example, p2 would be immutable and the compiler can ensure that it is not modified. However, in the Java example, p2 could potentially be modified if there is another reference to the same object as a MutablePoint. If you are sure that there will only ever be one reference to the object, you could effectively treat it as immutable, but the compiler will not be able to help you.

The type hierarchy that would allow this would look like this:

interface Point {
    int getX();
    int getY();
}

interface MutablePoint extends Point {
    void setX(int x);
    void setY(int y);
}

interface ImmutablePoint extends Point {}

You then only need to implement a single mutable point and you get an unmodifiable point for free. There is no need to create wrappers or subclasses.

class DefaultPoint implements MutablePoint {
    private int x;
    private int y;

    public DefaultPoint(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public int getX() {
        return x;
    }

    @Override
    public int getY() {
        return y;
    }

    @Override
    public void setX(int x) {
        this.x = x;
    }

    @Override
    public void setY(int y) {
        this.y = y;
    }
}

Implementations of ImmutablePoint would be the same as I described in my previous post.

In the next section I will show an example of how this can be used. But first, there are a few variations to this design that you may want to use depending on your exact use-case. I will explain my reasoning for choosing this particular design as well as some alternatives that you may want to consider.

First, I chose Point as the top of the hierarchy to encourage non-mutability by default, in the same way that variables in Rust are immutable by default and only mutable if you explicitly declare them as such, using the mut keyword. However, it is also reasonable to make ReadablePoint, or something similiarly named, the top of the hierarchy and make Point the type which allows mutation.

Second, since ImmutablePoint has the same methods as Point, you may be tempted to simplify the hierarchy by making ImmutablePoint the top of the hierarchy and have MutablePoint extends ImmutablePoint. This may seem convenient at first, but it violates the Liskov substitution principle, which states that if S is a subtype of T (in this case MutablePoint is a subtype of ImmutablePoint) then any object of type T can be replaced by an object of type S3. This does not hold in this case because we cannot safely replace any immutable point with a mutable point. If the programmer is relying on the immutability of the point for the correctness of the code, then replacing it with a mutable point may result in incorrect behavior. On the other hand, the Point type makes no guarantees about the mutability of the data. If it is being used correctly, the programmer should treat it as if it may be modified, so it is safe to replace it with a mutable point.

Along those same lines, it may seem strange to make ImmutablePoint its own interface when it doesn’t define any additional methods. You can certainly do without it by just treating immutable points as Points, but I think it can be useful to explicitly differentiate between unmodifiable and immutable objects.

Consider the example below.

Point p1 = Point.createImmutable(1, 2);
ImmutablePoint p2 = Point.createImmutable(3, 4);

Both p1 and p2 provide exactly the same methods, and neither allows direct modification, but p2 provides an extra hint that the point will not be modified by some other means. That being said, it should be noted that a poorly or maliciously written implementation of ImmutablePoint could still be modifiable. The type declaration is only a hint to the programmer; the compiler doesn’t know that the value should not change.

Another variation is to replace one or more of the interfaces with abstract or concrete classes. For a simple type like Point, where there is probably not much benefit to having multiple implementations, it might make sense for ImmutablePoint and MutablePoint to be classes rather than interfaces. However, using an interface provides more flexibility, allowing you to provide multiple implementations which optimize for different cases.

Lastly, it is important to note is that this approach is “vulnerable” to casting. By that I mean that a user can simply cast a unmodifiable object to its mutable variant to gain access to its mutator methods.

Point p = Point.create(1, 2);
((MutablePoint) p).setX(3);

This may be acceptable if this class is only used internally, in which case it is probably enough to prevent accidental modification. However, if the API is being used by an untrusted third-party, you should make sure that you are not exposing important internals. If this is a concern, you can use the wrapper approach and implement the Point interface.

class UnmodifiablePoint implements Point {
    private final Point point;

    public UnmodifiablePoint(Point point) {
        this.point = point;
    }

    public int getX() {
        return this.point.getX();
    }

    public int getY() {
        return this.point.getY();
    }
}

Using Unmodifiable Objects

At this point you may still be wondering what exactly is the point of unmodifiable objects in general, and our proposed type hierarchy in particular, if the object is in fact still mutable. Because of their underlying mutability, you cannot use the same assumptions and optimizations as with immutable objects. However, they do share the advantage of making it easier to reason about your code by helping you to control when and where objects can be modified.

Consider the following example, which uses the Point type hierarchy that I described in the previous section.

class Circle {
    private final MutablePoint center;

    Point getCenter() {
        return center;
    }

    // ... other methods to modify the center ...
}

Here, the Circle class exposes its internal MutablePoint, but it does so as an unmodifiable Point. This prevents the consumer of this API from directly manipulating the location of the circle, which could lead to an invalid state, such as moving tthe center to a location that is outside some boundary. By limiting the ways that the center can be manipulated, you can guarantee that the circle will always be in a valid state. Again, this comes with the caveat that the user could cast the returned Point to a MutablePoint.

Of course there are other ways to accomplish something similar. You could instead return a (possibly immutable) copy of the object being exposed. Using the proposed Point type hierarchy, you can do this without changing the Circle interface since an ImmutablePoint would still satisfy the Point return type. This would still prevent unwanted modification, but the returned object now represents the value at exactly the time the method is called, whereas returning the object itself means that the user will have a constantly updated view of the value. Both are valid options, so you will have to decide which makes more sense for your given use case. One thing to keep in mind is that if you return a copy each time, this can put pressure on the garbage collector if the user makes many calls to the method. It may also be costly to create a copy if it is a complex object.

More Complex Unmodifiable Objects

We can extend the ideas demonstrated by the Point type hierarchy to create more complex types and while still making it easy to expose read-only views of internal data.

First, let’s extend our Circle example from the previous section to provide mutable and non-mutable variants.

interface Circle {
    Point getCenter();
}

interface MutableCircle extends Circle {
    @Override MutablePoint getCenter();
}

class DefaultCircle implements MutableCircle {
    private final MutablePoint center;

    @Override
    public MutablePoint getCenter() {
        return center;
    }
}

We can now use these like this:

MutableCircle c1 = Cirlce.create()
// `getCenter()` returns a `MutablePoint`, so `setX()` is available.
c1.getCenter().setX(1);

Circle c2 = Circle.create()
// Compiler error.
// `getCenter()` returns a `Point`, which does not have a `setX()` method.
c2.getCenter().setX(2);

The first thing to note is that I’ve omitted the methods for modifying the center from MutableCircle. However it is still able to provide mutability by returning a MutablePoint. This is a nice way to simplify your interface, but you should keep in mind that the consumer of this API can now modify the center without restriction. If this is a concern you should continue to return a Point and provide separate methods for modifying the center.

You may also have noticed that the implementation is essentially exactly the same as our earlier Circle class. However, because of the way that we’ve defined our interfaces, we are able to treat it as either unmodifiable or mutable depending on which interface we use.

We can extend this idea even further to create more complex types. For example, if you are creating a game you might have something like this4:

interface GameWorld {
    Player getPlayer();
}

interface MutableGameWorld extends GameWorld {
    @Override MutablePlayer getPlayer();
}

interface Player {
    Point getLocation();
    Equipment getEquipment();
}

interface MutablePlayer extends Player {
    @Override MutablePoint getLocation();
    @Override MutableEquipment getEquipment();
    void move(int dx, int dy);
    void setEquipment(Equipment equipment);
}

interface Equipment {
    Weapon getWeapon();
    Armor getArmor();
}

interface MutableEquipment extends Equipment {
    @Override MutableWeapon getWeapon();
    @Override MutableArmor getArmor();
    void setWeapon(Weapon weapon);
    void setArmor(Armor armor);
}

interface Weapon {
    double getBaseDamage();
    double getActualDamage();
}

interface MutableWeapon extends Weapon {
    void setBaseDamage(double damage);
    void setDamageModifier(double modifier);
}

interface Armor {
    double getBaseDefense();
    double getActualDefense();
}

interface MutableArmor extends Armor {
    void setBaseDefense(double defense);
    void setDefenseModifier(double modifier);
}

You can then provide different views of the game world to different parts of your system. For example, the game engine would need a mutable view in order to make updates. However, the renderer only needs access to the current state of the world but should not be able to change it.

class GameEngine {
    private MutableGameWorld world;

    private void initialize() {
        world.getPlayer().getLocation().setX(0);
        world.getPlayer().getLocation().setY(0);
        world.getPlayer().getEquipment().getWeapon().setBaseDamage(15.0);
        world.getPlayer().getEquipment().getArmor().setBaseDefense(10.0);
    }
}

class Renderer {
    private GameWorld world;

    private void render() {
        drawPlayerAt(world.getPlayer().getLocation());

        // Compiler error.
        // `GameWorld.getPlayer()` returns a `Player`.
        // `Player.getLocation()` returns a `Point`.
        // `Point` does not have a `setX()` method.
        world.getPlayer().getLocation().setX(1);
    }
}

Limitation on Generics

This pattern can more or less be extended indefinitely, but things get a little messy when generics are involved.

First let’s define a simple generic List type.

interface List<E> {
    E get(int i);
}

interface MutableList<E> extends List<E> {
    void add(E e);
}

This is fine so far, but let’s see what happens when we try to use this.

interface Polygon {
    List<Point> getVertices();
}

interface MutablePolygon extends Polygon {
    @Override MutableList<Point> getVertices();
}

In order to provide mutability, we override getVertices() and return a mutable list of unmodifiable points. This only allows you to add or remove vertices, but you cannot modify existing vertices. If you want to be able to modify existing verices, you would need to return an unmodifiable or mutable list of mutable points. However, the compiler won’t actually let us do this.

interface MutablePolygon extends Polygon {
    @Override List<MutablePoint> getVertices();
}
// OR
interface MutablePolygon extends Polygon {
    @Override MutableList<MutablePoint> getVertices();
}

This will fail to compile because the new return types are not subtypes of List<Point>. The reason for this is a bit complicated, so I won’t go into any more detail other than to say that it has to do with covariance and contravariance.

If you prefer one of these options, you can modify the Polygon interface.

interface Polygon {
    List<? extends Point> getVertices();
}

interface MutablePolygon extends Polygon {
    @Override List<MutablePoint> getVertices();
}
// OR
interface MutablePolygon extends Polygon {
    @Override MutableList<MutablePoint> getVertices();
}

This will now compile, but the usage can get a little ugly.

Polygon p = Polygon.create();

// So far so good ...
Point v0 = p.getVertices().get(0);

// ... but, if we try to assign the list to a variable, we get a compiler
// error because List<Point> does not satisfy List<? extends Point>
List<Point> vs = p.getVertices(); // Compiler error.

// If we specify <? extends Point>, it compiles but looks a little messy.
List<? extends Point> vs = p.getVertices();
Point v1 = p.getVertices().get(1)

This pattern should still work for even more complicated generic types, such as those with multiple or nested type parameters. However, it will get even more messy as your type becomes more complex. For example, let’s say you have a method that returns Map<Point, List<Point>>. In order to use this pattern, you would actually need to specify this as Map<? extends Point, ? extends List<? extends Point>>.

Conclusion

On the surface, immutable and unmodifiable objects look quite similar. Their interfaces are the same and they both disallow modification making it easier to reason about your code. Immutable objects accomplish this by disallowing all modifications which ensures that the data cannot be changed unexpectedly. In contrast, unmodifiable objects only provide a read-only view of the data. The data cannot be modified through this view, but it can be changed if there are other references to the data. This doesn’t give you as much guarantees about your data as immutable objects, but it still allows you to control when your data can be modified by either providing direct access to the mutable data or providing an unmodifiable view of the data.

In this post I showed how you can create an unmodifiable object by creating a simple wrapper around a mutable object. I then showed how you can use polymorphism as another way to provide mutable and read-only views of the same object without relying on wrapper classes. I then showed how this idea can be extended to more complex data type as well as limitations of this approach when dealing with generics.

  1. I don’t know where the term originated, but based on a quick search, most of the references to unmodifiable objects seem to be related to Java. They are sometimes also referred to as “read-only”, such as in the .NET Framework, which provides a ReadOnlyCollection class. 

  2. To be clear, I am only claiming to emulate the ability for the compiler to prevent calls to certain methods, which is a very small portion of the guarantees that Rust provides regarding mutability. 

  3. A less formal, but much simpler way to think of this is to consider whether the phrase “S is a type of T” makes sense. For example, it makes sense to say that a “mutable point is a type of point,” but it does not make sense to say that a “mutable point is a type of immutable point.” However, keep in mind that the simplification means that it may not always be an accurate substitution for the formal definition. 

  4. Note that I am not a game developer and this is soley intended as a demonstration of the type hierarchy. Please do not take this as game design advice.