Skip to content

Classes - Mutability (course slides)

Java. Covers: reassigning vs mutating class instances, references vs value types, mutability with lists, shared mutable state.

Reassigning instances of classes

java
public class Person {
    String name;
    int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

// Elsewhere:
Person friend = new Person("John", 23);
friend = new Person("Lisa", 22);

Reassigning a variable (friend = new Person(...)) just points it at a brand new instance; it doesn't touch the old instance at all.

java
Person person = new Person("John", 20);
Person otherPerson = person;
person = new Person("Lisa", 25);
System.out.println(otherPerson.name); // "John"

Compare with primitives — same reassignment behavior:

java
int number = 5;
int otherNumber = number;
number = 10;
System.out.println(otherNumber); // 5

Modifying instances of classes

java
Person person = new Person("John", 20);
Person otherPerson = person;
person.age = 21;
System.out.println(otherPerson.age); // 21

With classes, variables are references (pointers) to the instance, not the instance itself. person and otherPerson here point at the same object, so mutating it through one variable is visible through the other.

Numbers are copied when calling a function

java
public static void addOneToNumber(int number) {
    number = number + 1;
}

public static void main(String[] args) {
    int studentCount = 5;
    addOneToNumber(studentCount);
    System.out.println(studentCount); // 5, unchanged
}

We can't modify an int/String in place inside a function — we have to return the new value and reassign it:

java
public static int addOne(int number) {
    return number + 1;
}

public static void main(String[] args) {
    int count = 5;
    count = addOne(count);
    System.out.println(count); // 6
}

Class instances are NOT copied when calling a function

java
public static void addOneToAge(Person human) {
    human.age = human.age + 1;
}

public static void main(String[] args) {
    Person person = new Person("John", 20);
    addOneToAge(person);
    System.out.println(person.age); // 21
}

The reference is passed into the method, so mutating a field through it is visible to the caller after the call returns — unlike with primitives.

Mutability with lists

java
List<String> names = new ArrayList<>(List.of("John", "Jack", "Joe"));
names.add("Lisa"); // works, this list is mutable

List<String> names2 = List.of("John", "Jack", "Joe");
names2.add("Lisa"); // crashes: UnsupportedOperationException, List.of() is immutable
  • List.of(...) creates an immutable list — .add()/.remove() crash the program.
  • For now, prefer mutable lists (new ArrayList<>(...)) when you need to be able to add/ remove; we'll later see why immutable lists are often preferable.

Shared mutable state — the car race example

Goal: download a list of race participants (already ordered by who finished first), display the list, and congratulate the winner (participants.get(0)).

New requirement: the displayed list should be alphabetical (to keep the race outcome a surprise), but the winner-lookup still needs the original finish order.

Naive fix — sort in place before displaying:

java
public static void displayParticipants(List<String> participants) {
    Collections.sort(participants);
    System.out.println(participants);
}

This also reorders the underlying list itself, since participants is a reference to the same list object used later for congratulateWinner. Result: the wrong person gets congratulated, because participants.get(0) is now alphabetically-first, not first-to-finish.

Attempted fix — make the source list immutable (List.of(...)) so this kind of accidental mutation is caught: now Collections.sort(participants) crashes outright, since List.of() lists can't be sorted in place either.

Real fix — create a mutable copy just for the operation that needs to mutate:

java
public static void displayParticipants(List<String> participants) {
    List<String> mutableList = new ArrayList<>(participants);
    Collections.sort(mutableList);
    System.out.println(mutableList);
}

Now the original list (and the winner lookup that depends on its order) is untouched.

Avoiding mutability

  • Prefer immutable classes/data structures where practical.
  • Sharing mutable state (multiple references to the same mutable object, mutated from different places) is a common source of subtle bugs — exactly what happened above.
  • A hard crash (from an immutable structure) is often preferable to silently displaying wrong data — it points straight at the bug instead of hiding it.
  • Sometimes mutability really is needed — that's fine, just don't default to it.