A collection view exposes a backing collection through another interface or access policy; a snapshot copies membership but may still share the element objects.
Java collection views: live wrappers, snapshots and shallow copies
Java 8+. The program uses only JDK classes and runs without a framework.
Ask which changes the caller should observe
A dispatch service assembles a list of route codes and hands it to a reader. Collections.unmodifiableList(source) prevents edits through that returned wrapper, but source still owns a writable handle. If the service adds another route later, the reader sees it through the live wrapper.
The program returns both a live unmodifiable view and an unmodifiable snapshot built from a new ArrayList. After the source changes, their sizes differ. Neither return value lets the reader call add successfully. That tells us two separate things: whether edits are permitted and whether future source membership changes remain visible.
String elements are immutable in this example, so a membership snapshot is sufficient for its contract. Replace the strings with mutable Route objects and both collections still refer to those objects unless a separate element-copy policy exists. A read-only list of mutable records is not an immutable object graph.
Range views retain a relationship
subList represents a range of its backing list rather than an independent list. Editing the range through supported operations changes the backing list. Structural edits through a different handle can make subsequent use of the range invalid under the list contract. Do not retain a subList across uncontrolled mutations.
A small view can also keep its backing list reachable. For a small long-lived selection from a large temporary list, copying the selected membership may avoid retaining unnecessary storage. That is an ownership decision, not a universal instruction to copy every collection on every call. Read ArrayList and iterator mutation rules together.
Working program
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class RouteSnapshots {
public static void main(String[] args) {
List<String> routes = new ArrayList<>(Arrays.asList("North", "South"));
List<String> live = Collections.unmodifiableList(routes);
List<String> snapshot = Collections.unmodifiableList(new ArrayList<>(routes));
routes.add("West");
System.out.println("live=" + live.size());
System.out.println("snapshot=" + snapshot.size());
try {
live.add("East");
} catch (UnsupportedOperationException rejected) {
System.out.println("Reader cannot add");
}
}
}Output
live=3
snapshot=2
Reader cannot addCost and failure boundaries
The wrapper adds a small object without copying n elements, while the ArrayList snapshot copies n references in O(n) time and O(n) additional storage. Those references preserve the elements’ identity. Deep-copy cost depends on the reachable objects and the application’s ownership model.
A concurrent source needs a safe snapshot procedure. Copying a list while another thread mutates it does not create an atomic snapshot merely because the result becomes unmodifiable. Coordinate the copy or choose a collection with the required traversal semantics. Tests should mutate both source membership and a mutable element to expose the difference.
Common Mistakes
- Do not describe an unmodifiable live wrapper as an immutable snapshot.
- Do not assume copying membership copies every element.
- Do not keep a range view alive across unrelated structural edits.
Connect the contracts
Compare the boundary explained in List operations with the assumptions made by this program.
Continue with the new boundary checks
Continue with Java LinkedList subList: a live window, not a snapshot.
