ArrayList fields in class constructors

Viewed 544

I'm currently learning beginner level Java and have a problem with an ArrayList when used in a constructor as a parameter.

When I have to initialize an ArrayList in the class constructor, I usually write something like this (for the sake of this example, let's assume that I want to create an ArrayList of integers as the class field).

public class Example {

    private ArrayList<Integer> myList;

    public Example(ArrayList<Integer> myInts){
        this.myList = myInts;
    }
       
}

However, when I see people do the same thing in tutorials or textbooks, they write the following code:

public class Example {

    private ArrayList<Integer> myList;

    public Example(int myInts){
        this.myList = new ArrayList<>();
        addIntegers(myInts);
    }
 
    public void addIntegers(int myInts){
        this.myList.add(myInts);
    }
      
}

Is there a difference between the two examples? I assume mine is the wrong way to do it, but actually running both versions gives me the same results (to my limited understanding, that is), so I'm struggling to grasp what sets these two variants apart.

1 Answers

There is a difference, yes. In the code presented, one would call the constructor like so:

ArrayList<Integer> values = new ArrayList<>(List.of(1, 2, 3, 4));
Example example = new Example(values);

After object construction, the calling side still has access to values, i.e. the internal data structure used by example. By manipulating this data structure, the calling side could bring example in an unexpected state and cause issues.

To prevent such problems, we normally do not directly use reference-types passed in from the outside for internal states, but generate copies of them. This, in essence, is what the second example does. If we still want to pass a List as parameter to the constructor, we can copy the list:

public class Example {

    private ArrayList<Integer> myList;

    public Example(Collection<Integer> myInts) {
        this.myList = new ArrayList<>(Objects.requireNonNull(myInts));
    }

    public Example(Integer... myInts) {
        this(Arrays.asList(Objects.requireNonNull(myInts)));
    }

    public Example(int... myInts) {
        this(Arrays.stream(Objects.requireNonNull(myInts))
                .boxed()
                .collect(Collectors.toList()));
    }           
}

Now if the calling side mutates the List passed to the constructor, the internal datastructure within the Example-instance is not affected since it operates on a copy of the original list.

Remark: If the list-type is mutable, then copying the list is in general not sufficient; we would have to deep-copy the list (i.e. create a copy of each list-entry). This is - in general - not possible in Java.

Related