coderates
coderates

Reputation: 23

How do I use collections in immutable class safely in Java?

I try to implement immutable class, and I see a rule stating "Perform cloning of objects in the getter methods to return a copy rather than the returning actual object reference".

I understand that when we use immutables there would be no change in copied / cloned collections returned from the getters. When we use custom classes, the change in original collection can be seen also cloned ( shallow copied ) collection return from the getters.

In below code, I could not understand the case :

I created two methods, one for return the original collection as courseList and one for shallow copy of the courselist.

I assigned two version to local references clist1 and clist2.

Then I changed the item in original list. I can see the change original list and copied list also when I reach them through student object. However the change cannot be seen throug the reference I pointed to the cloned course list before ! I think it should also be affected by the change. Why I cant see the change on previously copied version ? This is reference and I think it should be point the same memory area, I also check the result by another example below again. I created a list containing StringBuilder. I appeded new strs to stringbuilder and then I can see the changed previously copied version of the list.

So, the main question, must I use the deep copy in immutable classes always ? Is this a wrong usage ? What would be the safe way to use collections in immutable classes ?

Thanks in advance.

ImmutableStudent.java


public final class ImmutableStudent {

    public ImmutableStudent(String _name, Long _id, String _uni, ArrayList<String> _courseList){
        name = _name;
        id = _id;
        uni = _uni;
        courseList = new ArrayList<>();
        _courseList.forEach( course -> courseList.add(course));
    }

    private final String name;
    private final Long id;
    private final String uni;
    private final ArrayList<String> courseList;


    public String getName() {
        return name;
    }

    public Long getId() {
        return id;
    }

    public String getUni() {
        return uni;
    }


    public List<String> getCourseList() {
        return courseList;
    }

    public List<String> getCourseListClone() {
        return (ArrayList<String>)courseList.clone();
    }
    
}

ImmutableHelper.java

public class ImmutableHelper {

    public static void run(){

        ArrayList<String> courseList = new ArrayList<>();
        courseList.add("Literature");
        courseList.add("Math");

        String name = "Emma";
        Long id = 123456L;
        String uni = "MIT";

        ImmutableStudent student = new ImmutableStudent(name, id, uni, courseList);

        System.out.println(name == student.getName());
        System.out.println(id.equals(student.getId()));
        System.out.println(courseList == student.getCourseList());

        System.out.println("Course List         :" + student.getCourseList());
        System.out.println("Course List Clone   :" + student.getCourseListClone());

        List<String> clist1 = student.getCourseList();
        List<String> clist2 = student.getCourseListClone();

        student.getCourseList().set(1, "Art");

        System.out.println("Course List         :" + student.getCourseList());
        System.out.println("Course List Clone   :" + student.getCourseListClone());

        System.out.println("Previous Course List        :" + clist1);
        System.out.println("Previous Course List Clone  :" + clist2);
        

        // Check shallow copy using collections.clone()


        ArrayList<StringBuilder> bList = new ArrayList<>();

        StringBuilder a = new StringBuilder();
        a.append("1").append("2").append("3");

        StringBuilder b = new StringBuilder();
        b.append("5").append("6").append("7");

        bList.add(a);
        bList.add(b);

        ArrayList<StringBuilder> bListCp = (ArrayList<StringBuilder>)bList.clone();

        System.out.println("Blist   : " + bList);
        System.out.println("BlistCp :" + bListCp);

        a.append(4);

        System.out.println("Blist   : " + bList);
        System.out.println("BlistCp :" + bListCp);

    }
}

The Result

Course List         :[Literature, Math]

Course List Clone   :[Literature, Math]

Course List         :[Literature, Math, Art]

Course List Clone   :[Literature, Math, Art]

Previous Course List        :[Literature, Math, Art]

Previous Course List Clone  :[Literature, Math]

Blist   : [123, 567]

BlistCp :[123, 567]

Blist   : [1234, 567]

BlistCp :[1234, 567]

Upvotes: 2

Views: 1074

Answers (2)

rpsrosario
rpsrosario

Reputation: 111

From the clone() Javadoc:

Returns a shallow copy of this ArrayList instance. (The elements themselves are not copied.)

What this means is that the reference returned by the clone method is actually a reference to a new instance of ArrayList that contains exactly the same elements as the original list. In an example:

// Original list is reference L1 and contains three elements A, B and C
L1 = [ A, B, C ]

// By doing L1.clone you get a reference to a new list L2
L2 = [ A, B, C ]

// When you add a new element to L1 you do not see the change in L2 because
// it is effectively a different list
L1 = [ A, B, C, D ]
L2 = [ A, B, C ]

// However, if you change one of the A, B or C's internal state then that
// will be seen when accessing that object through L2, since the reference
// kept in the lists are the same
L1 = [ A, B', C, D ]
L2 = [ A, B', C ]

For your question:

So, the main question, must I use the deep copy in immutable classes always ? Is this a wrong usage ? What would be the safe way to use collections in immutable classes ?

It depends on what you want to achieve. There are two scenarios:

Scenario A: You want the users of the class to receive an immutable view of the list (meaning no user can modify the list) but you want any changes that happen to the original list to be propagated through the immutable view.

Scenario B: You want all versions of the list to be immutable, even the internally kept one.

Both scenarios can be answered by using Collections.unmodifiableList, which states in the Javadoc:

Returns an unmodifiable view of the specified list. Query operations on the returned list "read through" to the specified list, and attempts to modify the returned list, whether direct or via its iterator, result in an UnsupportedOperationException.

The difference would be on where you use it. For Scenario A you would invoke the method in the getter, so that every caller would receive the unmodifiable view of the list but the class would still keep the modifiable view internally. For Scenario B you would store a reference to the result of calling unmodifiableList internally in the class. Note that on newer versions of Java you can also use List.copyOf to create an immutable list, which you could use for Scenario B.

From your description I believe what you are trying to achieve is scenario A.

Upvotes: 6

Sweeper
Sweeper

Reputation: 271810

Why I cant see the change on previously copied version ?

Precisely because you copied it! It's a copy - a different ArrayList object from the original, that just happens to contain the same elements.

This is reference and I think it should be point the same memory area

That is only true in the case of:

public List<String> getCourseList() {
    return courseList;
}

which is why you see the change on clist1. With clone(), you are creating a new object, and allocating new memory. Sure, you are still returning a reference to an object, but it's not the same reference that courseList stores. It's a reference to the copy.

must I use the deep copy in immutable classes always ?

No, as long as the elements in the collection are immutable. The whole point of making a copy is so that users can't do things like this:

List<String> list = student.getCourseList();
list.add("New Course");

If getCourseList didn't return a copy, the above code would change the student's course list! We certainly don't want that to happen in an immutable class, do we?

If the list elements are immutable as well, then users of your class won't be able to mutate them anyway, so you don't need to copy the list elements.


Of course, all of this copying can be avoided if you just use an immutable list:

private final List<String> courseList;
public ImmutableStudent(String _name, Long _id, String _uni, ArrayList<String> _courseList){
    name = _name;
    id = _id;
    uni = _uni;
    courseList = Collections.unmodifiableList(_courseList)
};

Upvotes: 3

Related Questions