mike231
mike231

Reputation: 31

Java, Making a class Immutable

I came across this exercise online where I have two classes and I'm supposed to make the Tutor class immutable. However, the only thing I can think of is adding final to name field. When it comes to the constructor, I don't think I need to change the initialisation of the name variable as String is immutable. I'm not sure how to approach the collection and how to make this part of the constructor immutable. According to the exercise, I'm not supposed to change the Student class (which I can see is mutable)

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;
    }
}

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;
    }
}

Upvotes: 1

Views: 1901

Answers (5)

Arvind Kumar Avinash
Arvind Kumar Avinash

Reputation: 79075

Java SE 16

You can use JEP 395: Records feature, introduced as part of Java SE 16, to create an immutable class without requiring much ceremony.

If you have already gone through the above link, you must have figured out that you can do it simply as

record Tutor(String name, Set<Student> tutees) { }

What you get in turn are:

  1. A final class Tutor.
  2. A canonical constructor whose signature is the same as the header, Tutor(String name, Set<Student> tutees).
  3. private final fields, name and tutees and their corresponding public accessor method with the same name and return type.
  4. Automatically created equals, hashCode and toString methods.

Demo:

Student.java

record Student(String name, String course) { }

Tutor.java

import java.util.Set;

record Tutor(String name, Set<Student> tutees) { }

Main.java

import java.util.Set;

class Main {
    public static void main(String[] args) {
        Set<Student> cscStudents = Set.of(
                                            new Student("Harry", "Java-8"),
                                            new Student("Tina", "Java-9"),
                                            new Student("Andy", "Java-11")
                                        );

        Set<Student> scienceStudents = Set.of(
                                            new Student("Tony", "Phy"),
                                            new Student("Kerry", "Chem"),
                                            new Student("John", "Bio")
                                        );

        Tutor t1 = new Tutor("Mark", cscStudents);
        Tutor t2 = new Tutor("Robin", scienceStudents);
        Tutor t3 = new Tutor("Mark", Set.of(
                                            new Student("Andy", "Java-11"),
                                            new Student("Harry", "Java-8"),
                                            new Student("Tina", "Java-9")
                                        )
                            );

        System.out.println(t1);
        System.out.println();

        System.out.println(t1.tutees());
        System.out.println();

        System.out.println("Students of " + t1.name() + ":");
        t1.tutees()
            .stream()
            .forEach( t -> System.out.println(t.name()) );
        System.out.println();

        System.out.println(t1.equals(t2));
        System.out.println(t1.equals(t3));
    }
}

Output:

Tutor[name=Mark, tutees=[Student[name=Andy, course=Java-11], Student[name=Harry, course=Java-8], Student[name=Tina, course=Java-9]]]

[Student[name=Andy, course=Java-11], Student[name=Harry, course=Java-8], Student[name=Tina, course=Java-9]]

Students of Mark:
Andy
Harry
Tina

false
true

Upvotes: 0

davidxxx
davidxxx

Reputation: 131346

The Tutor class presents many aspects promoting its immutability :

  • the class is final
  • the Set<Student> is protected against the modifications
  • no method allowing to change directly the state of the class

However, the defensive copy of the constructor is not complete.
It also has to copy the Students elements of the array passed. Otherwise the client of the constructor may change any instance of them and make so the Tutor instance mutable such as :

Student[] students = ...;
Tutor tutor = new Tutor(name, students);
students[0].setName("new Name!"); // break the immutability of Tutor

You should write something like :

public Tutor(String name, Student[] students){
  this.name = name;
  tutees = new HashSet<Student>();
  for (Student student : students){   
      Student copy = new Student(student.getName(), 
                                    student.getCourse());
      tutees.add(copy);
   }     
}

Additionally note that the Set returned by getTutees() is unmodifiable but elements contained in are as Student is mutable. So to make Tutor immutable you also have to create a copy of the Student elements as you return getTutees() such as :

public Set<Student> getTutees(){
   Set<Student> students = new HashSet<>();
   for (Student student : tutees){
      Student copy = new Student(student.getName(), 
                                    student.getCourse());
      students.add(copy);
   }     
   return Collections.unmodifiableSet(students);
}

As you may notice, getting the immutability in these conditions (an instance that we wish immutable but that contains a collection referencing mutable instances) requires to write more code (to read/to maintain/to test) and to perform more processing (so slower to execute).
If Student was an immutable class, the original getTutees() and the original constructor would be enough.

Upvotes: 4

Ken.C
Ken.C

Reputation: 41

Do you really need to return the Set of Students? If you really need that you can hide that by using an interface that provides only getters, something like

interface IStudent {
    public String getName();
    public String getCourse(); 
}

class Student : implements IStudent { ...} 

and in your Tutor you return Set<IStudent>

Upvotes: 1

Greg Gardner
Greg Gardner

Reputation: 190

Proper way is to make an object immutable is to:

  1. Declare the object final
  2. Do not provide setter methods
  3. Make all fields private
  4. Make mutable fields final
  5. Use deep copy in the constructor
  6. Clone objects in getter methods, so you don't return actual reference.

Upvotes: 2

Soham
Soham

Reputation: 1

To make the Tutor class immutable, you should use the "final" modifier on all the fields inside a Tutor, not on the Tutor's class definition.

Upvotes: 0

Related Questions