Module 3: Immutability and Final
Overview
Immutability is a key concept in software development that refers to the inability to modify an object after it has been created. Immutable objects provide various benefits in terms of code safety, thread safety, and code simplicity.
Learning Objectives
- Implement a method parameter as final to prevent its reassignment
- Implement an instance variable as final to prevent its reassignment
- Given a code snippet, determine whether an object that a final variable references can be modified
- Design and implement an immutable class
- Explain what the final keyword means for a variable
- Explain the effects of declaring an instance variable as final
- Explain the effects of declaring a method parameter as final
- Outline the characteristics of a final class
- Outline how to design an immutable class
- Outline how to implement an immutable class
- Outline the advantages of an immutable class
Creating Immutable Classes
An immutable class is one whose state cannot be changed after instantiation. Here are the key steps to create an immutable class:
- Declare the class as
final
to prevent extension - Make all fields private and final
- Don't provide setter methods
- If the class contains mutable objects:
- Make defensive copies in the constructor
- Make defensive copies in the getter methods
- Ensure that methods don't modify the object's state
public final class ImmutablePerson {
private final String name;
private final int age;
private final List<String> hobbies;
public ImmutablePerson(String name, int age, List<String> hobbies) {
this.name = name;
this.age = age;
// Defensive copy to prevent the reference from being modified externally
this.hobbies = new ArrayList<>(hobbies);
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public List<String> getHobbies() {
// Return a defensive copy to prevent modification
return new ArrayList<>(hobbies);
}
}
Overview
In the Encapsulation lesson, we covered declaring variables (both instance variables and method parameters) and classes final. We also discussed the effects of these declarations and the motivations for using them. These declarations are part of the building blocks needed to create an immutable class where both the state and behavior are unchangeable. This reading will discuss the other steps needed to create an immutable class so you can create a custom one whenever you want.
Immutable objects
We've already used many immutable classes from the Java library, like primitives and wrapper classes, but what made them immutable? The term means not capable or susceptible to change. In Java, this takes on a more precise definition. Constructors of immutable classes make immutable objects, and once they do, the state of the object becomes fixed. In addition, we want the method behavior to be fixed, which is why it is important to prevent overriding the methods through subclassing.
Creating an immutable class
In order to create an immutable class, we'll need to apply the final keyword as discussed from Reading 01 in the right places. We may also get a chance to utilize defensive copying which we learned about in our Encapsulation lesson.
The steps to create an immutable class are:
- Declare the class final to prevent sub-classing. This prevents the overriding of methods.
- Declare the instance variables final to prevent them from being reassigned.
- Do not provide setter methods for instance variables.
- Ensure the instance variables' states can't change once initialized. While there's no simple declaration for this, there are several ways to accomplish it:
- Primary method: Use immutable types where possible, and let Java do the work for you.
- If you can't follow the primary method, it's a little more complicated:
- Ensure your class never provides references to instance variables by utilizing defensive copying. Defensive copying provides access to an object's information by copying it and providing a reference to the copy. We'll show an example of this further down.
- Ensure the class' methods never change the state of its instance variables. There's no way to ensure this outside of careful programming and testing.
Imagine you have written an immutable CustomerOrder class using the steps described above. In Figure 1, we have created an immutable object of type CustomerOrder, which is indicated by the bold box. The object's state and behavior are fixed at instantiation. In Figure 1, note that order is not declared final, so its value could be changed to point to a different CustomerOrder object. If you also decided to declare the variable order final, the effect would make both the reference and object immutable.

Figure 1: Creating a custom immutable class called CustomerOrder.
Immutable class design example
Figure 2 shows the class diagram for the CustomerOrder class we introduced above. The instance variables of type long, String, and BigDecimal are all immutable. However, the variable orderDate is a mutable Date.
The methods include a constructor where we initialize all the instance variables, a getter for customerID, another getter for orderDate (the mutable Date), and a method computeBill() that takes a taxRate and calculates the total. We'll ignore shipping costs and other complications in this example.

Figure 2: Class diagram for the immutable CustomerOrder class.
Immutable class implementation
Figure 3 shows an implementation of the CustomerOrder class. Below are some key specifics to highlight.
- The CustomerOrder class is declared final.
- All the instance variables are declared private and final. Each is initialized in the constructor. The orderDate is initialized by defensively copying the Date provided in the constructor. The orderDate field is assigned a new instance of a Date object using the time field from the passed Date object.
- In the method getOrderDate(), defensive copying is used to create a new instance of a Date object using the time field from the original Date object. The original Date reference is never exposed.
- The customerID, name, and orderAmount instance variables are immutable. This means we don't need to utilize defensive copying in the getter methods.
- There are no setters in this class. The getters, except for orderDate, are omitted below.
- At no point in this class are the instance variables themselves modified.
public final class CustomerOrder {
private final long customerID;
private final String name;
private final BigDecimal orderAmount;
private final Date orderDate;
/**
* Constructor.
* @param customerID - Customer identifier
* @param name - Customer full name
* @param orderAmount - Dollar amount of order
* @param orderDate - Day order was made
*/
public CustomerOrder(long customerID, String name, BigDecimal orderAmount, Date orderDate) {
this.customerID = customerID;
this.name = name;
this.orderAmount = orderAmount;
this.orderDate = new Date(orderDate.getTime());
}
/**
* returns immutable primitive.
* @return customer identifier
*/
public long getCustomerID() {
return customerID;
}
// some simple getters omitted for immutable instance variable Objects
/**
* Returns the Order Date using defensive encapsulation, since Date is mutable.
* @return a new instance of the orderDate;
*/
public Date getOrderDate() {
return new Date(orderDate.getTime());
}
/**
* Compute the total cost of the order.
* Since Double is immutable in Java, a new Double will be returned.
* @param taxRate - the tax rate for the purchase
* @return order cost
*/
public BigDecimal computeBill(BigDecimal taxRate) {
BigDecimal taxRatio = taxRate.add(new BigDecimal("1.0"));
return orderAmount.multiply(taxRatio);
}
}
Figure 3: Immutable CustomerOrder class
Immutable class advantages
Encapsulation: You've been introduced to this concept in a previous lesson. The idea is to protect your object instances from being modified by the outside world. In large software systems, this prevents many types of undesired or unexpected behaviors when software is modified. It also protects your objects from being modified by malicious third parties during execution.
Simplicity: There are less cases to design for and test if the object's state is fixed. Trying to anticipate what outside influences (internal or external to your software system) can do to your mutable objects can be complex and daunting.
Thread safety: You've been introduced to concurrency as well as some associated benefits and risks with using it. It's important that an application performs the same way when one thread or many threads are running and interacting with its objects. A key risk is multiple threads manipulating the state of the same object. Since immutable objects prohibit manipulating state, they prevent this risk. Immutable classes are an essential tool when writing concurrent code.
Understanding the Final Keyword
The final
keyword in Java can be applied to variables, methods, and classes, with different effects:
Final Variables
When applied to a variable, final
means that the variable cannot be reassigned after initialization:
// Final primitive variable
final int MAX_SIZE = 100;
// MAX_SIZE = 200; // This would cause a compilation error
// Final reference variable
final List<String> namesList = new ArrayList<>();
// namesList = new ArrayList<>(); // This would cause a compilation error
namesList.add("John"); // This is allowed - the object's state can be changed
Important distinction: for reference variables, final
prevents reassignment of the reference, but it doesn't make the referenced object immutable.
Final Methods
When applied to a method, final
prevents the method from being overridden in subclasses:
public class Parent {
final void cannotBeOverridden() {
// Method implementation
}
}
public class Child extends Parent {
// This would cause a compilation error
// void cannotBeOverridden() { }
}
Final Classes
When applied to a class, final
prevents the class from being extended:
final class CannotBeExtended {
// Class implementation
}
// This would cause a compilation error
// class Subclass extends CannotBeExtended { }
Introduction to Immutability
Immutability is a key concept in software development that refers to the inability to modify an object after it has been created. Immutable objects provide various benefits in terms of code safety, thread safety, and code simplicity.
The final keyword in Java is one tool that helps enforce immutability when used correctly. In this module, we'll explore how to use final effectively and design immutable classes in Java.
Creating Immutable Classes
An immutable class is one whose state cannot be changed after instantiation. Here are the key steps to create an immutable class:
- Declare the class as
final
to prevent extension - Make all fields private and final
- Don't provide setter methods
- If the class contains mutable objects:
- Make defensive copies in the constructor
- Make defensive copies in the getter methods
- Ensure that methods don't modify the object's state
public final class ImmutablePerson {
private final String name;
private final int age;
private final List<String> hobbies;
public ImmutablePerson(String name, int age, List<String> hobbies) {
this.name = name;
this.age = age;
// Defensive copy to prevent the reference from being modified externally
this.hobbies = new ArrayList<>(hobbies);
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public List<String> getHobbies() {
// Return a defensive copy to prevent modification
return new ArrayList<>(hobbies);
}
}
Benefits of Immutability
Immutability offers several advantages in software development:
Thread Safety
Immutable objects are inherently thread-safe because their state cannot be modified after creation. This eliminates the need for synchronization when sharing these objects between threads.
Simplicity and Predictability
Since immutable objects cannot change, they are easier to reason about and less prone to bugs related to unexpected state changes.
Hashcode Consistency
Immutable objects make excellent keys in HashMaps and elements in HashSets because their hashcode will never change.
Failure Atomicity
Operations on immutable objects cannot leave them in an inconsistent state if they fail midway.
// Example of immutable objects' advantages
// Thread safety
ImmutablePerson person = new ImmutablePerson("Alice", 30, Arrays.asList("Reading", "Hiking"));
// Can be safely shared between threads without synchronization
// HashMap key consistency
Map<ImmutablePerson, String> personDetails = new HashMap<>();
personDetails.put(person, "Employee");
// The person's hashcode won't change, so it can always be retrieved
Common Immutable Classes in Java
Java provides several built-in immutable classes:
- String: Once created, a String object cannot be modified
- Integer, Long, Double: All primitive wrappers are immutable
- BigInteger, BigDecimal: For arbitrary-precision arithmetic
- Collections.unmodifiableXXX: Creates unmodifiable views of collections
String Immutability
String's immutability is a classic example:
String name = "Alice";
String upperName = name.toUpperCase(); // Creates a new String object
System.out.println(name); // Still prints "Alice"
System.out.println(upperName); // Prints "ALICE"
Unmodifiable Collections
Creating immutable collections with Java's Collections utility:
List<String> mutableList = new ArrayList<>();
mutableList.add("One");
mutableList.add("Two");
List<String> immutableList = Collections.unmodifiableList(mutableList);
// immutableList.add("Three"); // This would throw UnsupportedOperationException
// But be careful - the backing collection can still be modified:
mutableList.add("Three"); // This affects immutableList too!
// For true immutability, copy the elements:
List<String> trulyImmutable = Collections.unmodifiableList(new ArrayList<>(mutableList));
Functional Programming and Immutability
Immutability is a core principle of functional programming. Java has introduced several functional-style features that work well with immutable objects:
Stream API
Streams provide a functional approach to processing collections of data:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> upperNames = names.stream()
.map(String::toUpperCase) // Creates new objects rather than modifying
.collect(Collectors.toList());
Optional
Optional is an immutable container that may or may not contain a non-null value:
Optional<String> optional = Optional.of("value");
String result = optional.map(s -> s.toUpperCase())
.orElse("default");
Record Classes (Java 16+)
Records provide a concise way to create immutable data classes:
public record Person(String name, int age, List<String> hobbies) {
// Constructor to perform defensive copying
public Person {
hobbies = List.copyOf(hobbies); // Creates an immutable copy
}
// No need to define equals, hashCode, toString, or accessors - they're generated
}
Key Topics
Final Keyword
Learn how to use the final keyword in different contexts.
- Final variables
- Final methods
- Final classes
Immutable Classes
Understand how to create fully immutable classes.
- Design principles
- Defensive copying
- Handling collections
Benefits of Immutability
Why immutability matters in modern software development.
- Thread safety
- Caching
- Security
Functional Programming
How immutability connects to functional programming techniques.
- Pure functions
- Side-effect-free code
- Java Stream API