Collections in your API
Pass collections across a boundary without leaking your state or returning null.
Open this lesson in the learning hubKey points
- Never return
nullfor "nothing found". ReturnList.of()and the caller loops with no check at all. - Returning the field itself hands out a live handle to your state: the caller can clear it.
List.copyOf(items)is a snapshot;Collections.unmodifiableListis a read-only window that still moves.- Copy on the way in too, or the list you were handed can change behind your back later.
- Take the widest type you can:
Collection<? extends T>for a parameter, something concrete for a return. - Document the promise: is what you return a copy or a view, sorted or unordered, modifiable or not?
Example
import java.util.*;
public class Main {
static final class Basket {
private final List<String> items = new ArrayList<>();
Basket(Collection<? extends String> initial) {
items.addAll(initial); // copy in, never store the argument
}
List<String> leaky() { return items; }
List<String> snapshot() { return List.copyOf(items); }
List<String> readOnly() { return Collections.unmodifiableList(items); }
void add(String s) { items.add(s); }
}
static List<String> findMatches(List<String> src, String prefix) {
List<String> hits = new ArrayList<>();
for (String s : src) {
if (s.startsWith(prefix)) hits.add(s);
}
return hits; // empty list, never null
}
public static void main(String[] args) {
List<String> caller = new ArrayList<>(List.of("apple", "fig"));
Basket basket = new Basket(caller);
caller.add("smuggled");
System.out.println("copied on the way in : " + basket.snapshot() + " (later caller add ignored)");
basket.leaky().add("junk");
System.out.println("leaky() let junk in : " + basket.snapshot());
try {
basket.readOnly().add("nope");
} catch (UnsupportedOperationException e) {
System.out.println("readOnly().add : UnsupportedOperationException");
}
List<String> held = basket.snapshot();
List<String> window = basket.readOnly();
basket.add("plum");
System.out.println("snapshot after add : " + held);
System.out.println("read-only after add : " + window + " (a view, so it moved)");
System.out.println("no matches : " + findMatches(caller, "zz") + " (empty list, not null)");
for (String s : findMatches(caller, "zz")) {
System.out.println("this never runs " + s);
}
System.out.println("caller loops safely : no null check anywhere");
}
}
Copy on the way in, copy on the way out, and return an empty collection rather than null.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Collections course, and every lesson in it is listed on the Collections contents page.