Collections in your API

Collections · lesson 37 of 42 · 4 min read

Pass collections across a boundary without leaking your state or returning null.

Open this lesson in the learning hub

Key points

  • Never return null for "nothing found". Return List.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.unmodifiableList is 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.