Making Objects Immutable

Introduction to Immutable Objects

This blog post describes what immutable objects are in object-oriented programming. After reading this post, you should be able to implement immutable objects, and understand how they can be used to prevent unintended side effects in our code.

What are Immutable Objects?

An immutable object is an object whose state cannot be modified after it’s constructed. The first example most students encounter in Java programming is a String object. Consider the following piece of code:

String worldStr = "Hello world";
worldStr.toUpperCase();
System.out.println(worldStr);

While we might expect this code to output “HELLO WORLD“, it actually outputs “Hello world“. That’s because the String.toUpperCase() method doesn’t modify the state of the worldStr object. Instead, it returns a new String object with the contents of worldStr in uppercase. The state of the worldStr object remains unmodified.

In fact, no method in the String class modifies a String object. Objects of the String class are immutable.

The following code demonstrates how the method String.toUpperCase() creates a new object instead of modifying an existing one:

String worldStr = "Hello world";
String upperStr = worldStr.toUpperCase();
System.out.println(upperStr);
System.out.println(worldStr);

The output of this code is “HELLO WORLD“, followed by “Hello world“.

Typical Getters and Setters

Many students have the intuition to add getters and setters for all of the private variables in a class. Consider the following Point class, representing a two-dimensional point in space, with getter and setter methods for the X and Y coordinates:

public class Point {
    private int x;
    private int y;

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

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

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

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

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

    @Override
    public String toString() {
        return "Point (" + this.x +
                ", " + this.y + ")";
    }
}

The class allows us to create a Point, get and set its location in space, and get a representation of it as a String (which allows us to print a Point object directly to System.out).

Objects of this Point class are mutable — that is, their state (the location in space they represent) can change after they’re constructed. Sometimes, this type of mutable functionality is necessary; but, it should be avoided when it isn’t needed, because it can create unintended side effects in our code.

Side Effects with Mutable Objects

Let’s consider the following program which outputs the same Point, mirrored into the four quadrants of the plane:

public class MirrorPrinter {
    public static void main(String[] args) {
        Point pnt = new Point(2, 3);
        printMirrored(pnt);
    }

    public static void printMirrored(Point p) {
        System.out.println(p);
        p.setX(-p.getX());
        System.out.println(p);
        p.setY(-p.getY());
        System.out.println(p);
        p.setX(-p.getX());
        System.out.println(p);
    }
}

This code creates a new Point at coordinates (2, 3), then prints it out in all four quadrants. It outputs:

Point (2, 3)
Point (-2, 3)
Point (-2, -3)
Point (2, -3)

The code seems to behave as expected. However, there’s caveat. Consider the following MirrorPrinter.main(String[]) method with two additional lines of code:

public static void main(String[] args) {
    Point pnt = new Point(2, 3);
    printMirrored(pnt);
    System.out.println("---");
    System.out.println(pnt);
}

We might reasonably expect that the code will output the Point mirrored across the four quadrants, then additionally output the original value of pnt. That is, we might expect the code to output:

Point (2, 3)
Point (-2, 3)
Point (-2, -3)
Point (2, -3)
---
Point (2, 3)

However, what the code actually outputs (note the final line of output) is the following:

Point (2, 3)
Point (-2, 3)
Point (-2, -3)
Point (2, -3)
---
Point (2, -3)

That’s because the MirrorPrinter.printMirrored(Point) method has a side effect. The Point object passed to the method is modified, prior to it being used again in MirrorPrinter.main(String[]).

Making the Point Class Immutable

Instead of having a mutable class, let’s create instance methods for Point objects that return new Point objects as a result, just like how the String.toUpperCase() method returns a new String. By doing so, we can prevent methods like MirrorPrinter.printMirrored(Point) from modifying the Point object passed to it.

As a bonus, by writing instance methods that return new Point objects, we have the opportunity to better encapsulate the actual operations we want to run on Point objects.

The two operations that we want to perform on Point objects are to flip them across the X-axis and across the Y-axis. Let’s rewrite the Point class by adding Point.flipX() and Point.flipY() methods that both return new Point objects:

public class Point {

    [...]

    public Point flipX() {
        return new Point(-this.x, this.y);
    }

    public Point flipY() {
        return new Point(this.x, -this.y);
    }
}

To make the class immutable, remove any methods that modify the state of the Point object. In this case, remove the two setter methods, Point.setX(int) and Point.setY(int).

As an exercise, I recommend that students remove the setter methods from the first version of the Point class we wrote above, then add the Point.flipX() and Point.flipY() methods to form a complete implementation of the class.

Rewriting the printMirrored Method

Using the new Point.flipX() and Point.flipY() methods, we can rewrite the MirrorPrinter.printMirrored(Point) method with this now-immutable Point class:

public static void printMirrored(Point p) {
    System.out.println(p);
    System.out.println(p.flipX());
    System.out.println(p.flipX().flipY());
    System.out.println(p.flipY());
}

Note, in the third line of the method, that we run the Point.flipY() method directly on the result from Point.flipX(). That’s because Point.flipX() returns a new Point object which can be operated on directly.

Because Point objects are now immutable, we’re guaranteed that MirrorPrinter.printMirrored(Point) doesn’t modify the Point object that’s passed to it. As such, the MirrorPrinter.main(String[]) method now behaves as expected. The following code:

public static void main(String[] args) {
    Point pnt = new Point(2, 3);
    printMirrored(pnt);
    System.out.println("---");
    System.out.println(pnt);
}

now outputs:

Point (2, 3)
Point (-2, 3)
Point (-2, -3)
Point (2, -3)
---
Point (2, 3)

Summary

Immutable objects are objects whose state can’t be modified after they’re constructed. Making a class immutable involves removing any setter methods, as well as any other methods that modify the state of an object. Instead of modifying the state of an object, look for ways instead to create a new object with the desired state.

Making classes immutable in our code can prevent unintended side effects on objects when we’re running computations with them. I recommend students get into the habit of writing immutable classes wherever possible, as it’ll generally make writing correct code easier.

For more tips, and to arrange for personalized tutoring for yourself or your study group, check out Vancouver Computer Science Tutoring.