开发者

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

开发者 https://www.devze.com 2023-03-08 17:37 出处:网络
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.

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开发者_C百科, 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?


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                          |
           '-------------------------+------------------------------------'


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


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.


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!


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.


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.


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


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


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());

    }
}


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

}


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.

0

精彩评论

暂无评论...
验证码 换一张
取 消