How do I protect a List inside a Java record?

A record is a convenient way to represent data, but a record containing a mutable list can still change unexpectedly. In this tutorial, you will protect a reading list from changes made through other references.

Records became a final language feature in Java 16. This tutorial uses Java 17 as its baseline and requires no preview or incubator flags. See Java’s language changes by release.

1. Understand the problem

Consider this record:

record ReadingList(String title, List<String> books) {
}

Java generates its constructor and accessors. However, the generated constructor stores the supplied list reference. A caller holding that list can change its contents afterward.

A final field prevents reassignment of the reference; it does not freeze the referenced object. Java’s record documentation explains the fields and methods generated for records.

2. Protect the list during construction

Use a compact constructor to replace the incoming list with an unmodifiable snapshot:

ReadingList {
    books = List.copyOf(books);
}

Assign to the constructor parameter, books. Java assigns the resulting value to the component field after the constructor body completes.

List.copyOf() preserves element order and prevents later changes to the original list from affecting the result. It rejects null collections and null elements. See the Java 17 List API.

3. Create the complete example

Save this code as RecordListExample.java:

import java.util.ArrayList;
import java.util.List;

public class RecordListExample {
    record ReadingList(String title, List<String> books) {
        ReadingList {
            if (title == null || title.isBlank()) {
                throw new IllegalArgumentException(
                        "Title must not be blank");
            }
            books = List.copyOf(books);
        }
    }

    public static void main(String[] args) {
        List<String> original =
                new ArrayList<>(List.of("Java Basics"));

        ReadingList readingList =
                new ReadingList("Weekend", original);

        original.add("Advanced Java");

        System.out.println("Original: " + original);
        System.out.println("Record: " + readingList.books());

        try {
            readingList.books().add("Another Book");
        } catch (UnsupportedOperationException exception) {
            System.out.println("Record list cannot be modified");
        }
    }
}

The example checks two paths: modifying the original list and attempting a modification through the record’s accessor.

4. Compile and run

Open a terminal in the directory containing the file:

javac --release 17 RecordListExample.java
java RecordListExample

Output:

Original: [Java Basics, Advanced Java]
Record: [Java Basics]
Record list cannot be modified

The original list accepts another book. The record keeps its initial contents, and its returned list rejects additions.

5. Know the limit: elements are not copied

This approach protects the list structure. It does not create separate copies of its elements.

Here, the elements are strings, which are immutable. If the list contained mutable Book objects, callers could still change those objects. Use immutable element types or copy mutable elements when your data model requires deeper protection.

Best practices

  • Copy incoming collections at construction boundaries.
  • Document your null policy; this example rejects null lists and null elements.
  • Prefer immutable element types for value objects.
  • Avoid unnecessary copying in the accessor when the stored list is already protected.
  • Treat the caught exception as a demonstration; normal application code should avoid attempting unsupported modifications.

Summary

A record does not automatically make a referenced list immutable. Using List.copyOf() in its compact constructor protects the list against external structural changes while keeping the record concise.

How do I use new Java 10 methods like List.copyOf(), Set.copyOf(), and Map.copyOf()?

Java 10 introduced the List.copyOf(), Set.copyOf(), and Map.copyOf() methods as convenient ways to create unmodifiable copies of existing collections. These methods are part of the java.util package and provide a simpler way to create immutable collections compared to using older methods like Collections.unmodifiableList().

Here’s how you can use them:


1. List.copyOf()

The List.copyOf() method creates an unmodifiable copy of the provided Collection. The returned list:

  • Is immutable (you cannot add, remove, or modify elements).
  • Rejects null elements (throws a NullPointerException).

Example:

package org.kodejava.util;

import java.util.List;

public class ListCopyExample {
    public static void main(String[] args) {
        // Create a mutable list
        List<String> originalList = List.of("A", "B", "C");

        // Create an unmodifiable copy
        List<String> unmodifiableList = List.copyOf(originalList);

        // Print the copied list
        System.out.println(unmodifiableList);

        // Throws UnsupportedOperationException if modification is attempted
        // unmodifiableList.add("D");

        // Throws NullPointerException if original list has nulls
        // List<String> listWithNull = new ArrayList<>();
        // listWithNull.add(null);
        // List.copyOf(listWithNull);
    }
}

2. Set.copyOf()

The Set.copyOf() method creates an unmodifiable copy of the provided Collection, ensuring that:

  • The returned set contains no duplicate elements.
  • Null elements are not allowed.
  • The original collection can be a List, Set, or any Collection.

Example:

package org.kodejava.util;

import java.util.Set;

public class SetCopyExample {
   public static void main(String[] args) {
      // Create a mutable set
      Set<String> originalSet = Set.of("A", "B", "C");

      // Create an unmodifiable copy
      Set<String> unmodifiableSet = Set.copyOf(originalSet);

      // Print the copied set
      System.out.println(unmodifiableSet);

      // Throws UnsupportedOperationException
      // unmodifiableSet.add("D");
   }
}

3. Map.copyOf()

The Map.copyOf() method creates an unmodifiable copy of the provided map. Similar to List.copyOf() and Set.copyOf():

  • The returned map is immutable.
  • Null keys or values are not allowed.
  • Elements retain the original insertion order (if applicable, e.g., for LinkedHashMap).

Example:

package org.kodejava.util;

import java.util.Map;

public class MapCopyExample {
   public static void main(String[] args) {
      // Create a mutable map
      Map<Integer, String> originalMap = Map.of(1, "One", 2, "Two", 3, "Three");

      // Create an unmodifiable copy
      Map<Integer, String> unmodifiableMap = Map.copyOf(originalMap);

      // Print the copied map
      System.out.println(unmodifiableMap);

      // Throws UnsupportedOperationException
      // unmodifiableMap.put(4, "Four");
   }
}

Notes:

  1. Immutable Behavior:
    • Any attempt to modify the unmodifiable collections (e.g., using add() or put()) throws UnsupportedOperationException.
    • These methods return a new collection, but if the input collection is already immutable and meets the conditions, it may return the original collection (performance optimization).
  2. Handling Nulls:
    • If any input collection contains null elements, these methods will throw a NullPointerException.
  3. Differences from Existing Methods:
    • Unlike Collections.unmodifiableList()/Set()/Map(), these methods create a copy, ensuring that changes to the source collection won’t affect the new collection.
  4. Static Imports:
    • These methods belong to static utility classes (List, Set, and Map) and are invoked directly as static methods.

Summary:

  • Use these methods to get immutable copies of collections.
  • They reject null values by design.
  • Collections become unmodifiable and can’t be changed after creation.

They are great for enhancing immutability and safety of the application!