Keyboard Shortcuts N Next post
P Previous post
S Save / unsave
R Read aloud
T Toggle theme
/ Focus search
Esc Close panels
🔥
Ready to read...
Java Java and Cryptography Mastery Series — Extended Java Programming Java Tutorial Module 18 - Modern Java Features 8 to 21

Java 10 List.copyOf Set.copyOf Map.copyOf Immutable Copies

Reviewed & accurate
AI Summary

Introduction

Java 10 shipped in March 2018 with the List.copyOf, Set.copyOf, and Map.copyOf methods, delivered through JEP 269 alongside the var keyword. Where Java 9 added List.of, Set.of, and Map.of for constructing immutable collections from individual elements, Java 10 added the missing counterpart: take an existing mutable collection, copy its contents into a fresh immutable collection. The two API surfaces together provide the complete immutable-collection story for Java.

For a Java learner, java 10 immutable copies is the standard way to take a defensive snapshot of a collection before returning it from a method, or before passing it to untrusted code. This post shows the three copyOf families, the null-rejection contract, the difference between copyOf and Collections.unmodifiableXxx, and the behaviour when the input is already immutable.

What is Java 10 Immutable Copies (copyOf)?

The copyOf methods belong to the family of immutable-collection factories in java.util, alongside Java 9 List.of, Set.of, and Map.of. Each takes a single Collection (or Map, for Map.copyOf) and produces a fresh immutable collection of the same contents. The result is always a new instance backed by its own internal data, never a view of the input.

The crucial difference from Collections.unmodifiableList is the snapshot guarantee. Collections.unmodifiableList(coll) returns a view that throws on mutation but is still backed by the input collection; if the input changes, the view changes. List.copyOf(coll) creates a fresh immutable list with no reference to the input; later changes to the input are not reflected in the copy. The copyOf methods are therefore the right tool whenever you need to guarantee that a returned collection will not change after the return call.

How the copyOf Methods Work - Step by Step

Step 1: Pick the family. Use List.copyOf when you want an ordered immutable list of the input elements. Use Set.copyOf when you want an immutable set with duplicates removed. Use Map.copyOf when you want an immutable map with the same entries as the input.

Step 2: Provide the source. The source collection is read once to copy its elements. The source is not retained; the resulting immutable collection is fully independent.

Step 3: Verify the contract. The result throws NullPointerException if the input contains any null element or key, or if the input itself is null. The result throws UnsupportedOperationException on any mutating operation. The result is a fresh instance even when the input is already an immutable collection, although the JDK may optimise to return the same instance when the input is a List.of or Set.of result.

Step 4: Inspect the result type. The runtime class of the result is one of the ImmutableCollections internal classes: ListN, List12, SetN, Set12, or MapN. The user-facing interface is List, Set, or Map, and the result is upcast to that interface.

Step 5: Use the copy. The copied collection is safe to share with untrusted code, return from a method, or store in a static constant. Any attempt to mutate it will throw, and any attempt to add a null later will not be possible because the collection cannot grow.

Java Program for the copyOf Methods

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class ImmutableCopiesDemo {

    // Returns a defensive immutable copy of the input list.
    static List<String> snapshotNames(List<String> names) {
        return List.copyOf(names);
    }

    public static void main(String[] args) {

        // (1) Basic List.copyOf from a mutable ArrayList
        var source = new ArrayList<String>(List.of("alpha", "beta", "gamma"));
        var copy = List.copyOf(source);
        System.out.println("(1) copy = " + copy + "  class=" + copy.getClass().getSimpleName());

        // (2) Source mutation does not affect the copy
        source.add("delta");
        source.set(0, "MUTATED");
        System.out.println("(2) source = " + source + "  copy = " + copy);

        // (3) Set.copyOf from a mutable HashSet, duplicates collapse
        var dupes = new HashSet<String>(List.of("x", "x", "y", "y", "z"));
        var setCopy = Set.copyOf(dupes);
        System.out.println("(3) setCopy size = " + setCopy.size() + "  contents = " + setCopy);

        // (4) Map.copyOf from a mutable HashMap
        var map = new HashMap<String, Integer>();
        map.put("a", 1);
        map.put("b", 2);
        map.put("c", 3);
        var mapCopy = Map.copyOf(map);
        System.out.println("(4) mapCopy = " + mapCopy + "  class=" + mapCopy.getClass().getSimpleName());

        // (5) Null element in source throws NullPointerException
        var withNull = new ArrayList<String>();
        withNull.add("first");
        withNull.add(null);
        try {
            List.copyOf(withNull);
        } catch (NullPointerException ex) {
            System.out.println("(5) null in source rejected: "
                    + ex.getClass().getSimpleName());
        }

        // (6) Null source itself throws NullPointerException
        try {
            List.copyOf(null);
        } catch (NullPointerException ex) {
            System.out.println("(6) null source rejected: "
                    + ex.getClass().getSimpleName());
        }

        // (7) Mutations on the copy throw UnsupportedOperationException
        try {
            copy.add("epsilon");
        } catch (UnsupportedOperationException ex) {
            System.out.println("(7) add rejected: "
                    + ex.getClass().getSimpleName());
        }
        try {
            copy.set(0, "zero");
        } catch (UnsupportedOperationException ex) {
            System.out.println("    set rejected: "
                    + ex.getClass().getSimpleName());
        }
        try {
            copy.remove(0);
        } catch (UnsupportedOperationException ex) {
            System.out.println("    remove rejected: "
                    + ex.getClass().getSimpleName());
        }

        // (8) copyOf on an already-immutable collection
        var ofResult = List.of("p", "q", "r");
        var copyOfImmutable = List.copyOf(ofResult);
        System.out.println("(8) identity same? " + (ofResult == copyOfImmutable));

        // (9) copyOf vs Collections.unmodifiableList (live view semantics)
        var base = new ArrayList<String>(List.of("1", "2"));
        var unmod = java.util.Collections.unmodifiableList(base);
        var frozen = List.copyOf(base);
        base.add("3");
        System.out.println("(9) unmod = " + unmod + "  frozen = " + frozen);

        // (10) Realistic defensive copy in a return value
        var original = new ArrayList<String>(List.of("alice", "bob"));
        var snapshot = snapshotNames(original);
        original.add("eve");
        System.out.println("(10) original = " + original
                + "  snapshot = " + snapshot);
    }
}

Code Explanation - Line by Line

The snapshotNames method takes a List<String> and returns List.copyOf(names). This is the canonical defensive-copy pattern: the method's caller cannot mutate the returned list, and any mutations the caller makes to the original list after the call are not reflected in the snapshot. The method is one line, which is the appeal of copyOf over the longer Collections.unmodifiableList(new ArrayList<>(names)) pattern.

Section (1) creates a mutable ArrayList with three strings, then calls List.copyOf to produce an immutable snapshot. The runtime class of the snapshot is ListN (or List12, depending on element count), which is one of the ImmutableCollections internal classes. Section (2) is the key demonstration: mutating the source by adding "delta" and replacing "alpha" with "MUTATED" leaves the copy unchanged. The copy's contents are alpha, beta, gamma, exactly what they were at the moment of the copy.

Section (3) shows Set.copyOf from a HashSet. The HashSet was constructed from a list with duplicate elements, but the Set constructor collapses duplicates, so the HashSet already contains only three distinct elements. Set.copyOf produces an immutable set with the same three elements. The iteration order is unspecified, like Set.of.

Section (4) shows Map.copyOf from a HashMap with three entries. The returned map is immutable and has the same key-value pairs. Section (5) shows that List.copyOf throws NullPointerException when the input contains a null element. This matches the null-rejection contract of List.of: the immutable collections never hold nulls, so copyOf must reject any input that contains a null.

Section (6) shows that the source collection itself cannot be null. List.copyOf(null) throws NullPointerException. Section (7) shows the three mutation operations (add, set, remove) all throwing UnsupportedOperationException. The underlying immutable list classes do not override the mutator methods, so the default implementations throw.

Section (8) is a subtle optimisation point. When you call List.copyOf on an already-immutable collection from List.of, the JDK is allowed to return the same instance because the contents cannot change. The == check in the demo confirms this in current JDK versions. This optimisation is not guaranteed by the contract, but it is observable and useful to know.

Section (9) is the contrast with Collections.unmodifiableList. The unmodifiable list is a live view: when you mutate the backing ArrayList, the view changes too. List.copyOf is a snapshot: when you mutate the backing ArrayList, the snapshot is unaffected. The output shows that unmod = [1, 2, 3] but frozen = [1, 2].

Section (10) is the realistic scenario: the snapshotNames method is called with an ArrayList, and the caller mutates the original after the call. The snapshot returned by the method is unchanged, which is exactly what defensive copy means.

Sample Input and Output

(1) copy = [alpha, beta, gamma]  class=ListN
(2) source = [MUTATED, beta, gamma, delta]  copy = [alpha, beta, gamma]
(3) setCopy size = 3  contents = [z, y, x]
(4) mapCopy = {a=1, b=2, c=3}  class=MapN
(5) null in source rejected: NullPointerException
(6) null source rejected: NullPointerException
(7) add rejected: UnsupportedOperationException
    set rejected: UnsupportedOperationException
    remove rejected: UnsupportedOperationException
(8) identity same? true
(9) unmod = [1, 2, 3]  frozen = [1, 2]
(10) original = [alice, bob, eve]  snapshot = [alice, bob]

The output confirms every contract point. The copy preserves the original contents at the moment of the call; mutating the source afterwards does not change the copy. Nulls in the source or the source itself cause NullPointerExceptions. Every mutating operation throws UnsupportedOperationException. The optimisation of returning the same instance for an already-immutable input is observed. The contrast between unmodifiableList and copyOf shows the live-view vs snapshot difference. The realistic defensive-copy scenario shows the snapshot is independent of the original.

Common Mistakes to Avoid

The first mistake is using Collections.unmodifiableList when you really want a snapshot. The unmodifiable wrapper is a view, so mutations to the backing list are visible through the wrapper. If you need the collection to remain unchanged forever, use List.copyOf. The second mistake is calling copyOf on a collection that contains nulls; the call throws NullPointerException immediately, which can be a surprise if your input came from an untrusted source.

The third mistake is using copyOf on a Map and expecting the iteration order to match the source. Map.copyOf uses an unspecified iteration order, just like Map.of. If you need the source's iteration order, use Stream.collect(Collectors.toMap(...)) with a LinkedHashMap supplier. The fourth mistake is assuming the copy will be a deep copy. copyOf copies the references to the elements, not the elements themselves; if the elements are mutable objects, the copy is still effectively mutable through those objects. For a true deep copy, you need a manual loop that clones each element.

The fifth mistake is calling copyOf on every list-return statement without measuring the cost. Each copyOf call allocates a new array of the same size as the source, which is O(n) in time and space. If your code already has an immutable list from List.of or a previous copyOf call, calling copyOf again is wasted work; the current implementation optimises this by returning the same instance, but you should not rely on that. The sixth mistake is using copyOf when you need a mutable copy; in that case use new ArrayList<>(source) which produces a mutable copy.

Real-World Applications

The copyOf methods are used throughout the Java standard library. java.util.List.copyOf is used in Stream.toList in Java 16 to produce an unmodifiable list of the stream elements; java.util.Map.copyOf is used in the implementation of Process.info and ProcessHandle.info to produce immutable snapshots of the OS-level process state. The Java 11 HttpClient uses List.copyOf internally to make defensive copies of header lists.

In the wider ecosystem, Spring Framework 5+ uses List.copyOf in the @Configuration class post-processor to produce immutable bean-name lists; JUnit 5 uses Set.copyOf in @TestInstance to produce immutable extension sets; Apache Commons Lang uses Map.copyOf in the LookupSymbolTable to expose immutable configuration views. The Java records feature, which became standard in Java 16, is often paired with copyOf in record constructors to ensure that the record's List fields are immutable. Studying java 10 immutable copies is therefore the prerequisite for writing safe APIs that expose collections without risk of caller mutation. The java 10 immutable copies API is the natural partner to the Java 9 List.of, Set.of, and Map.of factories, completing the immutable-collection story for modern Java.

Conclusion

We showed List.copyOf, Set.copyOf, and Map.copyOf with their snapshot semantics, null-rejection contract, and the contrast with Collections.unmodifiableXxx. The copyOf methods are the standard way to take a defensive snapshot of a collection before returning it from a method, and they are the natural partner to the Java 9 List.of, Set.of, and Map.of factories for constructing immutable collections. The natural next topics to study are the Java 11 HttpClient for HTTP/2 and WebSocket, which returns immutable collections internally, and the Java 11 String methods that pair with immutable collections for text processing.

Test Your Knowledge
How did you find this?

Comments

Join the discussion! Sign in with your Google or Blogger account, or comment as Anonymous - no account needed. For quick questions, also reach me on Telegram @cytestch.

Comments