Creating Immutable Classes in Java
An introduction to immutable classes in Java, such as a String class.
Join the DZone community and get the full member experience.
Join For FreeWhat is an immutable object?
Once an object is created and initialized, it cannot be modified. We can call accessor methods (e.g. getter methods), copy the objects, or pass the objects around — but no method should allow modifying the state of the object. Wrapper classes (such as Integer and Float) and String classes are well-known examples of classes that are immutable.
Let us now discuss the String class. String is immutable; once you create a String object, you cannot modify it. How about methods such as trim that remove leading and trailing whitespace characters — do such methods modify the state of the String object? No. If there are any leading or trailing whitespace characters, the trim method removes them and returns a new String object instead of modifying that String object.
Defining Immutable Classes
Keep the following aspects in mind for creating your own immutable objects:
- Make the fields final and initialize them in the constructor. For primitive types, the field values are final, there is no possibility of changing the state after it is initialized. For reference types, you cannot change the reference.
- For reference types that are mutable, you need to take of some more aspects to ensure immutability. Why? Even if you make the mutable reference type final it is possible that the members may refer to objects created outside the class or may be referred by others. In this case:
- Make sure that the methods don’t change the contents inside those mutable objects.
- Don’t share the references outside the classes — for example, as a return value from methods in that class. If the references to fields that are mutable are accessible from code outside the class, they can end up modifying the contents of the object.
- If you must return a reference, return the deep copy of the object (so that the original contents remain intact even if the contents inside the returned object is changed).
- Provide only accessor methods (i.e., getter methods) but don’t provide mutator methods (i.e., setter methods)
- In case changes must be made to the contents of the object, create a new immutable object with the necessary changes and return to that reference.
- Declare the class final. Why? If the class is inheritable, methods in its derived class can override them and modify the fields.
Let us now review the String class to understand how these aspects are taken care of in its implementation:
All its fields are made private. The String constructors initialize the fields.
- There are methods such as trim, concat, and substring that need to change the contents of the String object. To ensure immutability, such methods return new String objects with modified contents.
- The String class is final, so you cannot extend it and override its methods.
Here is a circle class that is immutable. For brevity, this example shows only the relevant methods for illustrating how to define an immutable class:
//ImmutableCircle.java
// Point is a mutable class
class Point {
private int xPos, yPos;
public Point(int x, int y) {
xPos = x;
yPos = y;
}
public String toString() {
return "x = " + xPos + ", y = " + yPos;
}
int getX() { return xPos; }
int getY() { return yPos; }
}
// ImmutableCircle is an immutable class – the state of its objects
// cannot be modified once the object is created
public final class ImmutableCircle {
private final Point center;
private final int radius;
public ImmutableCircle(int x, int y, int r) {
center = new Point(x, y);
radius = r;
}
public String toString() {
return "center: " + center + " and radius = " + radius;
}
public int getRadius() {
return radius;
}
public Point getCenter() {
// return a copy of the object to avoid
// the value of center changed from code outside the class
return new Point(center.getX(), center.getY());
}
public static void main(String []s) {
System.out.println(new ImmutableCircle(10, 10, 20));
}
// other members are elided ...
}
This program prints:
center: x = 10, y = 10 and radius = 20
Note the following aspects in the definition of the ImmutableCircle class:
- The class is declared final to prevent inheritance and overriding of its methods.
- The class has only final data members and they are private.
- Because center is a mutable field, the getter method getCenter() returns a copy of the Point object.
Immutable objects also have certain drawbacks. To ensure immutability, methods in immutable classes may end-up creating numerous copies of the objects. For instance, every time getCenter() is called on the ImmutableCircle class, this method creates a copy of the Point object and returns it. For this reason, we may need to define a mutable version of the class as well, for example, a mutable Circle class.
The String class is useful in most scenarios, if we call methods such as trim, concat, or substring in a loop, these methods are likely to create numerous (temporary) String objects. Fortunately, Java provides StringBuffer and StringBuilder classes that are mutable. They provide functionality similar to String, but you can mutate the contents within the objects. Hence, depending on the context, we can choose to use String class or one of StringBuffer or StringBuilder classes.
Source code presented in this article can be downloaded here.
Published at DZone with permission of Hari Kiran G. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments