Immutable Object with ArrayList member variable - why can this variable be changed?
Asked Answered
A

11

25

I have got one class with various member variables. There is a constructor and there are getter-methods, but no setter-methods. In fact, this object should be immutable.

public class Example {
   private ArrayList<String> list; 
}

Now I noticed the following: when I get the variable list with a getter-method, I can add new values and so on - I can change the ArrayList. When I call the next time get() for this variable, the changed ArrayList is returned. How can this be? I didn't set it again, I just worked on it! With a String this behaviour isn't possible. So what is the difference here?

Aspirator answered 26/5, 2011 at 10:39 Comment(0)
A
47

Just because the reference to the list is immutable doesn't mean that the list it refers to is immutable.

Even if list was made final this would be allowed

// changing the object which list refers to
example.getList().add("stuff");

but this would not allowed:

// changing list
example.list = new ArrayList<String>();   // assuming list is public

In order to make the list immutable (prevent also the first line), I suggest you use Collections.unmodifiableList:

public class Example {
    final private ArrayList<String> list;

    Example(ArrayList<String> listArg) {
        list = Collections.unmodifiableList(listArg);
    }
}

(Note that this creates an unmodifiable view of the list. If someone is holding on to the original reference, then the list can still be modified through that.)


With a String this behaviour isnt possible. So what is the difference here?

That is because a String is already immutable (unmodifiable) just as the list would be if you turned it into an unmodifiableList.

Comparison:

              String data structure  | List data structure
           .-------------------------+------------------------------------.
Immutable  | String                  | Collection.unmodifiableList(...)   |
-----------+-------------------------+------------------------------------|
Mutable    | StringBuffer            | ArrayList                          |
           '-------------------------+------------------------------------'
Antho answered 26/5, 2011 at 10:43 Comment(2)
AFAIK Collections.unmodifiableList() returns an immutable WRAPPER for given list. If I am correct, above will not guarantee immutability. A class may instantiate a list, instantiate Example, and can still modify the list that Example is holding by modifying the original list passed to the constructor. Albeit the answer may suffice to address the differences, it may fail to meet strict "immutability" requirements.Eradis
That's correct. Answer updated. You could do Collections.unmodifiableList(new ArrayList<>(listArg)) to make sure no one holds a reference to the underlying mutable list and thus avoid mutability.Antho
S
20

You are returning a reference to the list. And list isn't immutable.


If you do not want your list to be changed return a copy of it:

public List<String> get() {
    return new ArrayList<String>(this.list);
}

Or you can return a unmodifiable list:

public List<String> get() {
    return Collections.unmodifiableList(this.list);
}
Surcingle answered 26/5, 2011 at 10:42 Comment(0)
A
3

The key is to understand that you're not changing the string - you're changing which string references the list contains.

To put it another way: if I take a dollar out of your wallet, and replace it with a dime, I haven't changed either the dollar or the dime - I've just changed the contents of your wallet.

If you want a read-only view on the list, look at Collections.unmodifiableList. That won't stop the list that it's wrapping from changing of course, but it will stop anyone who only has a reference to the unmodifiable list from modifying the contents.

For a truly immutable list, look at Guava's ImmutableList class.

Angelita answered 26/5, 2011 at 10:44 Comment(0)
D
2

As the other answers say the Object you return from the getters is still mutable.

You can turn the List immutable by decorating it using the Collections class:

 list = Collections.unmodifiableList(list);

If you return this to clients they will not be able to add or remove elements to it. However, they can still get elements out of the list - so you have to make sure they're immutable too, if that's what you're after!

Dividivi answered 26/5, 2011 at 10:45 Comment(0)
J
2

Collections.unmodifiableList() makes list unmidifiable. Which again creates a new final array list and override add, remove, addall and clear method to throw unsupportedoperationexception. Its a hack of Collections class. But at compile time it does not stop you from adding and removing stuff. I would rather go with cloning of that list. Which can help me to keep my existing object immutable and does not cost me creating of new list. Read difference between cloning and new operator(http://www.javatpoint.com/object-cloning). Also will help from crashing my code at runtime.

Jijib answered 12/1, 2015 at 10:42 Comment(0)
P
1

This works for me. We need to have the class as final so that it can not be inherited. keep the list as final (this is just a reference to the list). In the constructor, use unmodifiableList and create a new list and assign a reference to that and not the input.

public final class ImmutableClassWithList {
    
    private  final List<String> list;
    
    public ImmutableClassWithList(ArrayList<String> input) {
        list = Collections.unmodifiableList(new ArrayList<String>(input));
    }
    
    public List<String> getList(){
        
        return list;
    }

}
Piss answered 11/8, 2021 at 5:41 Comment(1)
You may want to accept a generic list as an input, not only an ArrayListVang
V
1

Since Java 10 you can use List.copyOf static method.

public void setList(List<T> list){
  this.list = List.copyOf(list);
}

As per javadoc, the resulting list is immutable, and any changes to the underlying list will not affect the new one.

Vang answered 12/12, 2022 at 17:11 Comment(1)
You will have to think about nulls here. Both as the list and in the list.Universal
A
0

Therefore you should not provide a getter method for the list if you want to protect it from being changed.

Its objects are still staying intact since you didn't have setters. But what you do is remove/add new/different objects to it and this is fine.

Aponeurosis answered 26/5, 2011 at 10:45 Comment(0)
T
0

The list reference is immutable, but not the list. If you want the list itself to be immutable, consider using ImmutableList

Traduce answered 26/5, 2011 at 10:47 Comment(0)
L
0

To get a really immutable list, you will have to make deep copies of the contents of the list. UnmodifiableList would only render the list of references somewhat immutable. Now making a deep copy of the List or array will be tough on memory with the growing size. You can make use of serialization/deserialization and store the deep copy of array/list into a temp file. The setter would not be available as the member varaible needs to be immutable. The getter would serialize the member variable into a file and then desialize it to get a deep copy. Seraialization has an innate nature of going into the depths of an object tree. This would ensure complete immutability at some performance cost though.

package com.home.immutable.serial;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public final class ImmutableBySerial {

    private final int num;
    private final String str;
    private final TestObjSerial[] arr;

    ImmutableBySerial(int num, String str, TestObjSerial[] arr){
        this.num = num;
        this.str = str;
        this.arr = getDeepCloned(arr);
    }

    public int getNum(){
        return num;
    }

    public String getStr(){
        return str;
    }

    public TestObjSerial[] getArr(){
        return getDeepCloned(arr);
    }

    private TestObjSerial[] getDeepCloned(TestObjSerial[] arr){
        FileOutputStream fos = null;
        ObjectOutputStream oos = null;
        FileInputStream fis = null;
        ObjectInputStream ois = null;
        TestObjSerial[] clonedObj = null;
        try {
             fos = new FileOutputStream(new File("temp"));
             oos = new ObjectOutputStream(fos);
             oos.writeObject(arr);
             fis = new FileInputStream(new File("temp"));
             ois = new ObjectInputStream(fis);
             clonedObj = (TestObjSerial[])ois.readObject();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                oos.close();
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return clonedObj;
    }
}
Lacrimator answered 27/5, 2017 at 6:54 Comment(0)
M
0

One another solution is to return copy of the arraylist not the actual array list like this code

import java.util.ArrayList;
import java.util.List;

public final class ImmutableExample {

    private final List<String> strings = new ArrayList<String>();

    public ImmutableExample() {
        strings.add("strin 1");
        strings.add("string 2");
    }

    public List<String> getStrings() {
        List<String> newStrings = new ArrayList<String>();
        strings.forEach(s -> newStrings.add(s));
        return newStrings;
    }

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        ImmutableExample im = new ImmutableExample();
        System.out.println(im.getStrings());
        im.getStrings().add("string 3");
        System.out.println(im.getStrings());

    }
}
Miserable answered 14/11, 2017 at 13:49 Comment(1)
This doesn't add new information. Returning a copy has been explained in this answer: stackoverflow.com/a/6137267 and it even uses a much cleaner version of copying the list.Alimentary

© 2022 - 2024 — McMap. All rights reserved.