Collection Utility Classes

The Collections and Arrays classes provide a toolbox of static helper methods — sorting, searching, reversing, and wrapping — that save you from hand-writing common collection algorithms.

java.util.Collections and java.util.Arrays are final classes made entirely of static utility methods. Collections operates on Collection/List implementations; Arrays operates on raw arrays. Together they cover sorting, searching, filling, copying, and wrapping collections in special-behavior views.

What They Are

Both classes are non-instantiable (private constructors) and exist purely to hold static methods, much like Math. They are algorithm libraries that work over the standard collection and array types rather than being types you construct.

import java.util.*;

List<Integer> nums = new ArrayList<>(List.of(5, 3, 1, 4, 2));
Collections.sort(nums);
System.out.println(nums); // [1, 2, 3, 4, 5]

int[] arr = {5, 3, 1, 4, 2};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5]

Sorting and Reversing

Collections.sort(list) sorts in place using natural ordering (elements must implement Comparable), or Collections.sort(list, comparator) for custom ordering. Collections.reverse(list) reverses element order in place. Arrays.sort(array) does the analogous thing for arrays, with an overload accepting a Comparator for object arrays (primitive arrays can't take a Comparator since primitives have a fixed natural order).

List<String> names = new ArrayList<>(List.of("Bob", "alice", "Carl"));
Collections.sort(names, String.CASE_INSENSITIVE_ORDER);
System.out.println(names); // [alice, Bob, Carl]

Collections.reverse(names);
System.out.println(names); // [Carl, Bob, alice]

Binary Search

Collections.binarySearch(list, key) and Arrays.binarySearch(array, key) locate an element in O(log n) time — but only work correctly on data that is already sorted in the same order used for the search. If the element is found, its index is returned; if not, a negative value is returned encoding the insertion point (-(insertionPoint) - 1).

Calling binarySearch on unsorted data gives undefined/incorrect results — it does not throw an error, it just silently returns a meaningless answer.

List<Integer> sorted = List.of(1, 3, 5, 7, 9);
int idx = Collections.binarySearch(sorted, 7);
System.out.println(idx); // 3

int missing = Collections.binarySearch(sorted, 4);
System.out.println(missing); // negative: -(insertion point) - 1 = -3

Frequency, Min, and Max

Collections.frequency(collection, element) counts occurrences. Collections.max(collection) / Collections.min(collection) find extremes by natural ordering, with Comparator overloads for custom ordering.

List<Integer> nums = List.of(4, 2, 4, 4, 1, 4, 3);
System.out.println(Collections.frequency(nums, 4)); // 4
System.out.println(Collections.max(nums)); // 4
System.out.println(Collections.min(nums)); // 1

Other Collections Utility Methods

Collections.shuffle(list) randomizes order; Collections.emptyList()/emptySet()/emptyMap() give shared immutable empty instances; Collections.singletonList(x) creates a single-element immutable list; Collections.nCopies(n, x) creates an immutable list of n repeated references to x; Collections.swap(list, i, j) swaps two elements.

List<Integer> deck = new ArrayList<>(List.of(1, 2, 3, 4, 5));
Collections.shuffle(deck);

List<String> repeated = Collections.nCopies(3, "x");
System.out.println(repeated); // [x, x, x]

List<Integer> single = Collections.singletonList(42);

Synchronized and Unmodifiable Wrappers

Collections also provides wrapper factories: synchronizedList/Set/Map(...) add lock-based thread safety around every method call (all callers must still manually synchronize when iterating, since iteration is a multi-step operation), and unmodifiableList/Set/Map(...) create read-only views (see the dedicated Immutable & Unmodifiable Collections topic for details).

List<String> syncList = Collections.synchronizedList(new ArrayList<>());
synchronized (syncList) { // manual lock required for safe iteration
    for (String s : syncList) {
        System.out.println(s);
    }
}

Arrays: Conversion and Comparison

Arrays.asList(...) bridges an array to the List interface (fixed-size, writes through). Arrays.equals(a, b) compares array contents (not just references, unlike ==). Arrays.deepEquals/Arrays.deepToString handle nested arrays correctly.

int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
System.out.println(a == b);            // false - different array objects
System.out.println(Arrays.equals(a, b)); // true - same contents

int[][] nested1 = {{1, 2}, {3, 4}};
int[][] nested2 = {{1, 2}, {3, 4}};
System.out.println(Arrays.deepEquals(nested1, nested2)); // true

Arrays: Fill and Copy

Arrays.fill(array, value) sets every element to value. Arrays.copyOf(array, newLength) creates a resized copy (padding with default values or truncating). Arrays.copyOfRange(array, from, to) copies a sub-range.

int[] arr = new int[5];
Arrays.fill(arr, 7);
System.out.println(Arrays.toString(arr)); // [7, 7, 7, 7, 7]

int[] extended = Arrays.copyOf(arr, 8);
System.out.println(Arrays.toString(extended)); // [7, 7, 7, 7, 7, 0, 0, 0]

int[] sub = Arrays.copyOfRange(arr, 1, 3);
System.out.println(Arrays.toString(sub)); // [7, 7]

Practical Usage

These utilities show up constantly: sorting a list of custom objects before display, deduplicating with frequency, quickly building test fixtures with nCopies, or converting between arrays and lists at API boundaries.

record Employee(String name, double salary) {}

List<Employee> staff = new ArrayList<>(List.of(
    new Employee("Dana", 75000),
    new Employee("Sam", 62000),
    new Employee("Lee", 90000)
));
Collections.sort(staff, Comparator.comparingDouble(Employee::salary).reversed());
System.out.println(staff.get(0).name()); // Lee - highest paid

Edge Cases

Collections.sort and Collections.max/min throw ClassCastException if elements don't implement Comparable and no Comparator is given. Wrapping an immutable list with Collections.sort throws UnsupportedOperationException, since sort mutates in place.

Arrays.asList on a primitive array (e.g. int[]) does NOT produce a List<Integer> — it produces a List<int[]> with a single element, because primitives can't be generic type arguments. Use Arrays.stream(intArray).boxed().toList() instead when you need a boxed list.

int[] primitives = {1, 2, 3};
List<int[]> oops = Arrays.asList(primitives); // list of ONE int[] element
System.out.println(oops.size()); // 1, not 3

Complete Example

import java.util.*;

public class UtilityDemo {
    public static void main(String[] args) {
        List<Integer> scores = new ArrayList<>(List.of(88, 45, 76, 90, 45, 60));

        Collections.sort(scores);
        System.out.println("Sorted: " + scores);

        int idx = Collections.binarySearch(scores, 76);
        System.out.println("Index of 76: " + idx);

        System.out.println("Max: " + Collections.max(scores));
        System.out.println("Min: " + Collections.min(scores));
        System.out.println("Frequency of 45: " + Collections.frequency(scores, 45));

        Collections.reverse(scores);
        System.out.println("Reversed: " + scores);
    }
}

Mental Model

Think of Collections and Arrays as a shared algorithms toolbox that sits outside the container types themselves — instead of every List implementation reimplementing sort/search/shuffle, the algorithm lives once as a static method and is written generically against the interface (List, Comparable) so it works across implementations.

Quick Reference

Collections.sort(list) / sort(list, cmp)
Collections.reverse(list)
Collections.shuffle(list)
Collections.binarySearch(list, key)   // list must be sorted
Collections.max(list) / min(list)
Collections.frequency(list, elem)
Collections.unmodifiableList/synchronizedList(list)
Collections.emptyList() / singletonList(x) / nCopies(n, x)

Arrays.sort(array) / sort(array, cmp)
Arrays.binarySearch(array, key)       // array must be sorted
Arrays.equals(a, b) / deepEquals(a, b)
Arrays.fill(array, value)
Arrays.copyOf(array, len) / copyOfRange(array, from, to)
Arrays.asList(array) / Arrays.toString(array)

Key Takeaways

Collections and Arrays are static-method utility classes, not types you instantiate.

binarySearch requires pre-sorted data and gives meaningless results otherwise.

Arrays.asList on a primitive array produces a single-element List<int[]>, not a boxed List<Integer> — a classic gotcha.

Sorting/reversing/shuffling mutate the list in place and throw on immutable collections.

synchronizedList wraps individual calls but still requires manual synchronization for safe iteration.

int[] primitives = {3, 1, 2}; List<Integer> list = Arrays.asList(primitives); // compiles but wrong: List<int[]> of size 1

List<Integer> list = Arrays.stream(primitives).boxed().collect(Collectors.toList());

Arrays.asList only accepts object arrays as varargs; a primitive array is treated as a single generic argument, producing a one-element List<int[]>.

List<Integer> unsorted = List.of(5, 1, 3); int idx = Collections.binarySearch(unsorted, 3); // meaningless result on unsorted data

List<Integer> sorted = new ArrayList<>(unsorted); Collections.sort(sorted); int idx = Collections.binarySearch(sorted, 3);

binarySearch assumes the input is already sorted in the same order being searched; it silently produces incorrect results on unsorted input rather than throwing.

List<String> immutable = List.of("c", "a", "b"); Collections.sort(immutable); // throws UnsupportedOperationException

List<String> mutable = new ArrayList<>(List.of("c", "a", "b")); Collections.sort(mutable);

Collections.sort mutates the list in place, which is impossible on an immutable List.of(...) instance.

List<String> syncList = Collections.synchronizedList(new ArrayList<>()); for (String s : syncList) { ... } // not thread-safe during iteration

List<String> syncList = Collections.synchronizedList(new ArrayList<>()); synchronized (syncList) { for (String s: syncList) {... } } synchronizedList only synchronizes individual method calls; iterating with a for-each performs multiple calls that must be wrapped in a manual synchronized block to be safe from concurrent modification.

What does Collections.binarySearch return when the searched-for element is not found?

A negative value that encodes the insertion point: -(insertionPoint) - 1, where insertionPoint is the index the element would need to be inserted at to keep the list sorted.

Why must a list be sorted before calling Collections.binarySearch on it?

Binary search relies on comparing against a midpoint and discarding half the remaining range each step, which only produces a correct answer when the data is already ordered consistently with the comparison being used.

What's wrong with calling Arrays.asList on an int[] and expecting a List<Integer>?

Primitives can't be used as generic type arguments, so Arrays.asList(int[]) treats the whole array as a single vararg element, producing a List<int[]> of size 1 rather than a list of boxed Integers.

Why does Collections.synchronizedList still require a manual synchronized block around iteration?

Each individual method call (get, size, etc.) is synchronized internally, but a for-each loop is really a sequence of multiple calls (hasNext/next repeatedly); without an outer synchronized block, another thread could modify the list between those calls.

What exception is thrown if you call Collections.sort on a list of objects that don't implement Comparable and no Comparator is given?

ClassCastException, thrown when sort tries to cast an element to Comparable in order to compare it against another element.

How do Arrays.equals and == differ when comparing two arrays?

== compares reference identity (are they the same array object), while Arrays.equals compares the arrays' contents element by element, returning true for two distinct arrays holding equal elements in the same order.

What is the difference between Arrays.copyOf and Arrays.copyOfRange?

copyOf(array, newLength) creates a copy starting at index 0 with the given total length (padding with defaults if longer than the original), while copyOfRange(array, from, to) copies a specific sub-range [from, to).

Write a program that takes an unsorted array of ints, sorts a copy of it (leaving the original unchanged), and prints both.

import java.util.Arrays; public class CopyAndSort { public static void main(String[] args) { int[] original = {5, 3, 8, 1, 9}; int[] sorted = Arrays.copyOf(original, original.length); Arrays.sort(sorted); System.out.println("Original: " + Arrays.toString(original)); System.out.println("Sorted: " + Arrays.toString(sorted)); } }

Arrays.copyOf makes an independent copy before sorting, so the original array is untouched: Original: [5, 3, 8, 1, 9], Sorted: [1, 3, 5, 8, 9].

What does this print? Trace through each step.

After reverse: [30, 20, 10]. After swap(0, 2): swaps index 0 (30) and index 2 (10), giving [10, 20, 30]. Final output: [10, 20, 30].

Write a program that uses Collections.frequency to find the most frequently occurring element in a List<String>.

import java.util.*; public class MostFrequent { public static void main(String[] args) { List<String> items = List.of("a", "b", "a", "c", "a", "b"); String mostFrequent = null; int maxCount = 0; Set<String> unique = new HashSet<>(items); for (String item : unique) { int count = Collections.frequency(items, item); if (count > maxCount) { maxCount = count; mostFrequent = item; } } System.out.println(mostFrequent + ": " + maxCount); } }

Iterating unique elements and checking frequency() for each finds the mode. Output: a: 3, since 'a' appears three times, more than 'b' (2) or 'c' (1).

List<Integer> list = new ArrayList<>(List.of(10, 20, 30));
Collections.reverse(list);
Collections.swap(list, 0, 2);
System.out.println(list);