Kusterbeck
Kusterbeck

Reputation: 118

When creating an immutable class, should collections only contain immutable objects?

Currently studying for an exam... ran into this question on a past paper.

Consider the following Student and Tutor classes:

public class Student {
    private String name;
    private String course;

    public Student(String name, String course) {
        this.name = name;
        this.course = course;
    }

    public String getName() {
        return name;
    }

    public String getCourse() {
        return course;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setCourse(String course) {
        this.course = course;
    }
}

Tutor:

public final class Tutor {
    private String name;
    private final Set<Student> tutees;

    public Tutor(String name, Student[] students) {
        this.name = name;
        tutees = new HashSet<Student>();
        for (int i = 0; i < students.length; i++) {
            tutees.add(students[i]);
        }
    }

    public Set<Student> getTutees() {
        return Collections.unmodifiableSet(tutees);
    }

    public String getName() {
        return name;
    }
}

Re-write the Tutor class to make it immutable(without modifying the Student class).

I know that the name field should have the final modifier to ensure thread safety.

If the tutees Set contains mutable Student Objects and the Student class cannot be changed then how do we make the class immutable? Perhaps creating a clone of the Set and upon every call to the getTutees method clear tutees and add the cloned elements to it?

Or is it already immutable despite the collection containing mutable objects?

Upvotes: 3

Views: 1521

Answers (2)

Nathan Hughes
Nathan Hughes

Reputation: 96394

The members of the collection also need to be immutable. Unmodifiable only means that no elements can be added or removed.

Even if the collection is unmodifiable, once code accessing it has a reference to an element belonging to that collection, then if that element is not immutable then it can be changed.

If the getter for the collection returns a defensive copy, making a new Set referencing new Student objects and returning that instead of the original set, then that will make the collection immutable.

public Set<Student> getTutees() {
    Set newSet = new HashSet();
    for (Student tutee : tutees) {
        newSet.add(new Student(tutee.getName(), tutee.getCourse()));
    }
    return newSet;
}

Adding an example here to show that using Collections.unmodifiableSet is not sufficient for this:

import java.util.*;

public class ImmutabilityExample {
    public static void main(String[] args) {
        Student student = new Student("Joe", "Underwater Basketweaving 101");
        Tutor tutor = new Tutor("Bill", new Student[] {student});
        Set<Student> students = tutor.getTutees();
        System.out.println("before=" + students);
        students.iterator().next().setName("Mary");
        System.out.println("" + tutor.getTutees());
    }
}

class Student {
    private String name;
    private String course;

    public Student(String name, String course) {
        this.name = name;
        this.course = course;
    }

    public String getName() {
        return name;
    }

    public String getCourse() {
        return course;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setCourse(String course) {
        this.course = course;
    }
    public String toString() {
        return "Student, name=" + name + ", course=" + course;
    }
}

final class Tutor {
    private String name;
    private final Set<Student> tutees;

    public Tutor(String name, Student[] students) {
        this.name = name;
        tutees = new HashSet<Student>();
        for (int i = 0; i < students.length; i++) {
            tutees.add(students[i]);
        }
    }

    public Set<Student> getTutees() {
        return Collections.unmodifiableSet(tutees);
    }

    public String getName() {
        return name;
    }
}

The output for this is:

before=[Student, name=Joe, course=Underwater Basketweaving 101]
[Student, name=Mary, course=Underwater Basketweaving 101]

Upvotes: 3

Little Santi
Little Santi

Reputation: 8803

I know I am getting too deeper into what it seems to be not more than a homework question, but...

Premises:

  1. Tutor contains instances of Student.
  2. We want Tutor to be immutable. Thus, it implies that the contained instances of Student must be immutable too.
  3. We can't make Student immutable.

So, what are our chances?

  1. We could make Tutor return shallow copies of the Student instancies. However, this implies that the system will offer a strange, unexpected behaviour to the client. Imagine this code:
    Tutor tutor=new Tutor(...);
    Set students=tutor.getTutees();
    Student student=students.iterator().next();
    student.setCourse("1");
    ...
    Set students=tutor.getTutees();
    Student student=students.iterator().next();
    if (!student.getCourse().equals("1"))
    {
        throw new RuntimeException("What the hell??? I already set it before!!!");
    }

IMHO, the immutability is not worth the confussion we induce the client code to.

  1. We could either extend Student and override the setters to turn them off:
    class MyStudent extends Student
    {
        public MyStudent(...)
        {
            super(...);
        }

        @Override
        public void setCourse(String s)
        {
            // Prevent modification silently.
        }

        @Override
        public void setName(String s)
        {
            // Even worse: Prevent modification through runtime exceptions:
            throw new IllegalStateException();
        }
    }

Same objection as before: The client code would still compile OK, but the behaviour would be strange and unexpected according to the standard javabean conventions.

My conclusion: Immutability is a design issue (not a programming issue), because if a class is immutable, it should have no setters at all.

So, altough the problem has some technically valid solutions, it is unsolvable in terms of good practices.

Upvotes: 2

Related Questions