You have an ArrayList of orders and you need it sorted, searched, shuffled for a sample, or handed to a legacy API that still wants a synchronized list. None of that is a new collection type. It is static methods on Collections and Arrays.

Collections and Arrays are helpers around the types you already picked — not a fourth List. This post is the methods people actually hit. The Collections hub owns views vs copies and unmodifiable vs immutable; factories own List.of. We use those words and move.

Same checkout records as the rest of the series:

public record LineItem(String sku, int quantity, BigDecimal unitPrice) {}

public record Order(
        String id,
        String customerEmail,
        List<LineItem> items,
        BigDecimal total,
        boolean active) {}

Collections.sort mutates the list. So does List.sort (Java 8), which is the form you want in new code. Both are stable.

List<Order> pack = new ArrayList<>();
pack.add(new Order("o-100", "ada@example.com", List.of(), new BigDecimal("12.00"), true));
pack.add(new Order("o-101", "linus@example.com", List.of(), new BigDecimal("4.50"), true));
pack.add(new Order("o-102", "grace@example.com", List.of(), new BigDecimal("28.50"), true));

pack.sort(Comparator.comparing(Order::total));
// o-101, o-100, o-102 — lowest total first

Collections.sort(pack) needs Order to be Comparable. A comparator is the usual checkout move: sort by total, by id, by email. List.of results throw on sort — unmodifiable. Sort a mutable copy.

binarySearch only means what you think if the list is already sorted in the same order:

pack.sort(Comparator.comparing(Order::id));
int at = Collections.binarySearch(pack, new Order("o-101", "", List.of(), BigDecimal.ZERO, true),
        Comparator.comparing(Order::id));
// index of o-101, or (-(insertion point) - 1) if missing

An unsorted list makes binarySearch undefined — not “throws,” not “returns −1.” You may get a plausible-looking index that is simply wrong. Sort first, same comparator.

MethodMutates?Precondition
list.sort(cmp) / Collections.sortyes, in placelist must support set
Collections.binarySearch(list, key)nolist sorted in that order
Collections.reverse(list)yeslist supports set
Collections.shuffle(list)yeslist supports set
Collections.reverse(pack);           // last becomes first
Collections.shuffle(pack);           // random permutation
Collections.shuffle(pack, new Random(42)); // reproducible shuffle

Shuffle is how you sample packing order without inventing a loop. Reverse is the cheap “oldest first ↔ newest first” when the list is already in one of those orders. Neither allocates a second list.

Frequency, disjoint, nCopies

Three methods that surprise people because they look like they belong on List and do not.

List<String> skus = new ArrayList<>();
skus.add("SKU-MUG");
skus.add("SKU-TEA");
skus.add("SKU-MUG");

int mugs = Collections.frequency(skus, "SKU-MUG"); // 2
boolean overlap = !Collections.disjoint(skus, List.of("SKU-BIN", "SKU-TEA")); // true

frequency uses equals. disjoint is true when the two collections share no element. Pass a Set as one argument when you can — contains then stops being a scan.

nCopies builds an immutable list of length n where every index is the same reference:

LineItem placeholder = new LineItem("SKU-PAD", 0, BigDecimal.ZERO);
List<LineItem> pads = Collections.nCopies(3, placeholder);
System.out.println(pads.size()); // 3
pads.set(0, placeholder);        // UnsupportedOperationException

Need a mutable list of three placeholders? new ArrayList<>(Collections.nCopies(3, placeholder)). Note: if the element is mutable, every index sees the same mutation. nCopies does not clone.

Empty, singleton, checked

Immutable empties and one-element collections are real return values, not “remember to new ArrayList<>().”

List<Order> none = Collections.emptyList();
Set<String> noSkus = Collections.emptySet();
Map<String, Order> noMap = Collections.emptyMap();

List<Order> justOne = Collections.singletonList(order);
Set<String> oneSku = Collections.singleton("SKU-MUG");
Map<String, Order> oneEntry = Collections.singletonMap("o-100", order);

emptyList() is a shared unmodifiable empty. add throws. new ArrayList<>(0) is a distinct mutable list that happens to start with no elements.

CallMutable?NullsTypical use
Collections.emptyList()non/a (empty)“no orders, and there never will be via this reference”
new ArrayList<>(0)yeslater adds may be nullcaller will add
List.of()non/asame job as emptyList, Java 9 factory
singletonList(x)nonull allowedone element, including “explicitly null”
List.of(x)nonull forbiddenone element, factories contract

Return emptyList() when the caller must not add. Return a new ArrayList when they will. Returning the shared empty and then having a caller add is how you get UnsupportedOperationException in a place that “always worked in tests” — the tests used a mutable stub.

checkedList is the runtime generic you wish List<Order> already was when you pass the list to a raw-typed API:

List<Order> orders = new ArrayList<>();
List<Order> checked = Collections.checkedList(orders, Order.class);

@SuppressWarnings("rawtypes")
List raw = checked;
raw.add("not an order");
// ClassCastException — heap pollution caught at the add

Without the wrapper, the bad add succeeds and a later Order cast blows up far from the crime. Use checkedList / checkedSet / checkedMap at the boundary with legacy raw types. Do not wrap every list in the service for sport. Why raw types exist after erasure, and PECS when a helper should accept List<? extends Order>, is Generics.

Unmodifiable wrappers are views

List<Order> live = new ArrayList<>();
live.add(order);
List<Order> read = Collections.unmodifiableList(live);

live.add(another);
System.out.println(read.size()); // 2
read.add(another);               // UnsupportedOperationException

unmodifiableList, unmodifiableSet, unmodifiableMap (and the Java 10 unmodifiableXxx copies of existing wrappers) refuse mutation through the wrapper. They are views. The hub already defined that. At an API boundary where you want a snapshot, List.copyOf is the factory that copies first.

Synchronized wrappers lock the whole collection

List<Order> shared = Collections.synchronizedList(new ArrayList<>());
shared.add(order); // this call is synchronized on the wrapper

Every mutative and query method takes the wrapper’s lock. That is whole-collection synchronized, the same idea as legacy Vector. Iteration is not covered by those per-call locks.

List<Order> shared = Collections.synchronizedList(new ArrayList<>());

synchronized (shared) {
    for (Order o : shared) {
        System.out.println(o.id());
    }
}

You must synchronize on the wrapper while you iterate — iterator, enhanced for, forEach, spliterator. Skip the lock and you can see ConcurrentModificationException or a torn walk. A stream over a synchronized list is not magically safe either.

Prefer java.util.concurrent types for shared data: ConcurrentHashMap for maps, CopyOnWriteArrayList when reads dominate and you want snapshot iterators, or don’t share — confine the ArrayList to one thread. The wrapper is for talking to an API that still demands a synchronized List, not for designing a new shared structure.

synchronizedSet and synchronizedMap have the same iterator rule: lock on the wrapper, not on the backing collection.

Arrays.asList: fixed-size, backed by the array

This is the method people confuse with List.of. It is not a factory in that family.

LineItem mug = new LineItem("SKU-MUG", 2, new BigDecimal("12.00"));
LineItem tea = new LineItem("SKU-TEA", 1, new BigDecimal("4.50"));
LineItem[] array = { mug, tea };

List<LineItem> view = Arrays.asList(array);
view.set(1, mug);            // array[1] is now mug — writes through
System.out.println(array[1].sku()); // SKU-MUG
view.add(tea);               // UnsupportedOperationException
view.set(0, null);           // allowed — asList permits nulls
Arrays.asList(array)List.of(e1, e2)new ArrayList<>(List.of(...))
Sizefixedfixedgrows
setwrites through to the arrayUnsupportedOperationExceptionmutates the list only
add / removethrowsthrowsworks
NullsallowedNullPointerExceptionallowed
Storagethe array you passedown unmodifiable storageown ArrayList storage

Arrays.asList(mug, tea) is the varargs form: the compiler builds an array, then the list wraps it. Changing set still writes that hidden array. There is no separate ArrayList.

Note: Arrays.asList on an int[] is a one-element List<int[]>, not a list of boxed integers. Use IntStream or box first. List.of has the same varargs trap if you pass a primitive array as a single object.

Need a real independent mutable list from an array?

List<LineItem> copy = new ArrayList<>(Arrays.asList(array));
copy.add(tea); // fine — copy is a true ArrayList
array[0] = tea; // copy is unaffected

Or List.copyOf(Arrays.asList(array)) when the result should stay unmodifiable and null-free.

Arrays.sort and Arrays.binarySearch are the array-side twins of list.sort and Collections.binarySearch. Same precondition: search only after a sort in the same order. They mutate / read the array, not a List. Reach for them when you still have an array; wrap with asList only when the next API wants a List.

Copy helpers you actually use

Collections.copy(dest, src) overwrites dest in place; dest must be at least as long as src or you get IndexOutOfBoundsException. It is not new ArrayList<>(src).

List<Order> dest = new ArrayList<>(Collections.nCopies(pack.size(), dummy));
Collections.copy(dest, pack);

Most application code wants the constructor copy or List.copyOf, not Collections.copy. Keep copy in mind for the “overwrite an existing list of the same size” job — buffers, recycled rows — and skip it as a synonym for clone.

Arrays.copyOf and System.arraycopy stay on the array side. They are how you grow or slice the backing store; they are not collection factories.

Interview lens

Interviewers poke the wrapper myths and the asList / search preconditions.

QuestionHonest answer
Does synchronizedList make iteration thread-safe?No. Lock on the wrapper for the whole iteration. Prefer concurrent types or confinement.
Is unmodifiableList a snapshot?No. It is a view. Mutate the original, the wrapper shows it.
Why does Arrays.asList(...).add(x) throw?Fixed-size array view. set is allowed; structural add/remove is not.
binarySearch on an unsorted list?Undefined result. Sort with the same order first.
emptyList() vs new ArrayList<>(0)?Shared immutable empty vs a mutable list you can grow.
nCopies(3, item) — three objects?Three slots, one reference. Mutate the object, every index sees it.

Wrong answer: “Collections.synchronizedList makes iteration thread-safe automatically.” The javadoc’s synchronized (list) around the iterator is mandatory. Forgetting it is the bug.

What to draw: a box around the list labeled “lock per call,” and a second box around the for loop labeled “you lock this.” Concurrent maps do not work that way — that is the next reason to leave wrappers behind.

Cheat sheet

Sort / search    list.sort(cmp)     binarySearch requires sorted
Rearrange        reverse, shuffle   in place, needs set
Query            frequency, disjoint
Build            nCopies(n, x)      singleton / singletonList / singletonMap
Empty            emptyList/Set/Map  immutable, shared
Checked          checkedList(list, Foo.class)   raw-type boundary
Wrap             unmodifiableXxx    VIEW
                 synchronizedXxx    whole-collection lock; lock while iterating

Arrays.asList    fixed-size, backed by array, set writes through, add throws
                 nulls allowed — not List.of

emptyList()      cannot add
new ArrayList<>(0)   can add, distinct instance

Prefer j.u.c     ConcurrentHashMap, CopyOnWriteArrayList — not synchronizedXxx

Do:

  • Sort, then binarySearch with the same comparator.
  • Return emptyList() / singletonList when the result must stay size-fixed.
  • Synchronize on the synchronized wrapper for every iteration, or don’t use the wrapper.

Don’t:

  • Assume synchronizedList makes a for-each safe.
  • Call add on Arrays.asList or on emptyList().
  • Use unmodifiableList when you meant List.copyOf.
  • Treat Collections as a collection type you new.

Wrap-up

Collections and Arrays are the toolbox around List, Set, and Map: sort and search, shuffle and reverse, empty and singleton, checked and unmodifiable views, synchronized wrappers, and asList. None of them replaces ArrayList. The traps that show up in reviews are consistent — binarySearch without a sort, asList.add, emptyList returned to a caller who adds, and a synchronized wrapper whose iterator still needs your lock.

Those synchronized wrappers are the modern cousin of types that were born locked. The next post is why Vector and Hashtable still compile, and why you still do not choose them.

Next optional step in the series The whole-table locks that predate the framework — and what to type instead. Legacy Collections: Vector, Stack, Hashtable, and Why They Still Compile