1

This is probably very trivial to many of you, but I could not find the answer to it. Thanks in advance for any help.

In short, I shallow copy a list, then modify one of its elements, but the change is not reflected in the other list, despite almost every resource saying that they are just two references to the same object, and any change in one should be reflected in the other one.

P.S. I have already checked out questions like this one that clarify the differences b/w shallow and deep copy. My question is why despite those theories, sometimes shallow-copy behaves like deep-copy.

    List<Integer> lst1 = new ArrayList<>();
    lst1.add(2);
    lst1.add(6);
    lst1.add(1);
    lst1.add(4);
    lst1.add(9);
    lst1.add(5);

    List<Integer> lst2 = new ArrayList<>(lst1);
    lst1.set(0, 3);

    System.out.println("lst1 = " + lst1);
    System.out.println("lst2 = " + lst2);

result:

lst1 = [3, 6, 1, 4, 9, 5]

lst2 = [2, 6, 1, 4, 9, 5]

Sam
  • 59
  • 4

3 Answers3

2

What you have is a shallow copy, yes, but any copy will show the change you describe. If it weren't a copy at all, it'd show that change, e.g. List<Integer> lst2 = lst1;.

The difference between a shallow and a deep copy is what happens to mutable elements in the array. Here is an example that shows the difference between a shallow and a deep copy.

List<List<Integer>> lst1 = new ArrayList<>();
lst1.add(new ArrayList<>(Arrays.asList(5)));
System.out.println(lst1); // [[5]]
List<List<Integer>> lstShallowCopy = new ArrayList<>(lst1); // shallow copy
List<List<Integer>> lstDeepCopy = lst1.stream()
   .map(ArrayList::new)
   .collect(toList()); // deep copy
List<List<Integer>> lstNotACopy = lst1; // not a copy at all

lst1.get(0).add(6);
lst1.add(Arrays.asList(7));

System.out.println(lst1); // [[5, 6], [7]]
System.out.println(lstShallowCopy); 
  // [[5, 6]]: shows modifications to the original elements, but 
  // doesn't include new elements
System.out.println(lstDeepCopy); // [[5]]
  // shows no modifications at all
System.out.println(lstNotACopy); // [[5, 6], [7]]
  // shows all modifications
Louis Wasserman
  • 191,574
  • 25
  • 345
  • 413
2

There seems to be a confusion as to what deep-copy and shallow-copy means.

A shallow copy is a new list that contains the same objects. You can prove that this is what happens like so:

List<AtomicBoolean> a = Arrays.asList(new AtomicBoolean(true));
List<AtomicBoolean> b = new ArrayList<>(a);
b.get(0).set(false);
a == b // false
a.get(0) == a.get(b) // true

a and b are difference lists, but they contain all the same objects.

A deep copy requires copying all the objects in the list. In this case it would be:

List<AtomicBoolean> b = a.stream().map(value -> new AtomicBoolean(value.get())).collect(toList());

However, note that since int is immutable, there is no difference between a shallow copy and a deep copy.

Also note that a shallow copy is still a different object, and that change its properties does not reflect on the original object. In your case, calling .set changes the copy only.

njzk2
  • 38,969
  • 7
  • 69
  • 107
  • "However, note that since `int` is immutable" this is the key part to why this code doesn't work as you're expecting. If you want to show this behavior in an `int` or `Integer`, you can wrap the primitive in a class. e.g. `class IntWrapper{ public int value;}` – Christopher Schneider Mar 02 '20 at 22:11
  • 2
    @ChristopherSchneider it doesn’t matter whether `Integer` is immutable or not. The OP is calling `set` on one list, which replaces the element in that list, whether immutable or not. This never has an effect on the previously created copy. It’s just impossible to construct a counter-example where the object is modified instead of the list. – Holger Mar 03 '20 at 10:19
  • @Holger Yes, you're right. I did not look at the initial code thoroughly. What prompted my response was what I _think_ they expected, which was that, "Since this is an Integer object, I'm just updating a reference to an integer, which should reflect in both lists." But since they are actually just setting a brand new value at index 0, that is irrelevant. – Christopher Schneider Mar 04 '20 at 14:35
0

The new keyword causes a brand new List to be created, since you are assigning lst2 to a new list. Any time the new keyword is used, a brand new object is created that hasn't before been referenced. Since lst2 is instantiated to a new list, this new list has its own space in memory separate from the previous list.

Separately, if your list contained references to mutable objects, not integers, that would be a different story.

njzk2
  • 38,969
  • 7
  • 69
  • 107
Niiiick
  • 161
  • 1
  • 4