Are the Java 10 copyOf factory methods suitable for defensive copies in constructors

Viewed 204

When implementing an immutable class that has an aggregate initialized by a Collection given to the constructor, the constructor must make a defensive copy of the Collection, to prevent the caller subsequently mutating the aggregate.

Before Java 10, that would result in code like this:

class Thing
{
   private final List<Widget> widgets;

   public Thing(List<Widget> widgets) {
      this.widgets = Collections.unmodifiableList​(new ArrayList<>(widgets));
   }

   public List<Widget> getWidgets() {
      return widgets;
   }
}

Java 10 added List.copyOf() and friends, which create an unmodifiable copy of the given Collection. That suggests for Java 10+ we can instead write code like this:

class Thing
{
   private final List<Widget> widgets;

   public Thing(List<Widget> widgets) {
      this.widgets = List.copyOf(widgets);
   }

   public List<Widget> getWidgets() {
      return widgets;
   }
}

Am I correct?

1 Answers

The design intent and the implementation of List.copyOf and the other copyOf methods is to do exactly as you would like, and it should be suitable for making defensive copies. The idea is that List.copyOf always makes a copy, unless it can prove that not making a copy is safe.

Additional context from the List.copyOf specification is:

Returns an unmodifiable List containing the elements of the given Collection, in its iteration order. The given Collection must not be null, and it must not contain any null elements. If the given Collection is subsequently modified, the returned List will not reflect such modifications.

Implementation Note: If the given Collection is an unmodifiable List, calling copyOf will generally not create a copy.

The key assertion is the last sentence in the first paragraph, which says that if the argument is subsequently modified, that modification doesn't occur in the returned list. That is, the returned list is conceptually always a copy.

Note also that "unmodifiable List" is a link to a particular section of the List specification where it describes the List instances returned by List.of, List.copyOf, and stream's Collectors.toUnmodifiableList.

The instance returned by Collections.unmodifiableList is not an "unmodifiable List" in this definition. Rather, it's an unmodifiable view. Such views explicitly permit modification of the backing collection, if one has a reference to the backing collection, and if that collection is itself modifiable.

Yes, the distinction between "unmodifiable list" and "unmodifiable view" is confusing, but it's what we've settled on. Sorry. An alternative at one point was "immutable". But this is inaccurate, as those instances can in fact be mutable, so we've avoided use of "immutable" for these cases.

In any case, the actual behavior reflects this specification:

var list1 = new ArrayList<String>(Arrays.asList("a", "b", "c"))
var list2 = Collections.unmodifiableList(list1)
var list3 = List.copyOf(list2)
var list4 = List.copyOf(list3)
list2 == list3
==> false
list3 == list4
==> true
list1.add("d")
list2
==> [a, b, c, d]
list3
==> [a, b, c]

In other words, List.copyOf does make a defensive copy of the unmodifiable view, and this behavior is required by the specification.

Related