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.