Collections Class
The Collections class in java.util is a utility class consisting entirely of static methods that operate on or return collections. It is the companion to the Arrays class — where Arrays serves arrays, Collections serves Collection instances. Collections provides algorithms (sort, shuffle, reverse, rotate, swap, fill, copy, frequency, disjoint), factory methods for wrapper collections (unmodifiableList, synchronizedSet, singletonList, emptyList, nCopies), and extremal-value queries (min, max). Understanding Collections means knowing which algorithm is available, how each works, what the preconditions are, and how the unmodifiable and synchronised wrappers differ from truly immutable or concurrent collections. This entry covers every major method group with precise semantics, performance characteristics, and the pitfalls of each wrapper type.
Sorting and Reordering Algorithms
// ── Sorting ───────────────────────────────────────────────────────────
List<Integer> numbers = new ArrayList<>(List.of(5, 2, 8, 1, 9, 3));
Collections.sort(numbers); // natural order
System.out.println(numbers); // [1, 2, 3, 5, 8, 9]
Collections.sort(numbers, Comparator.reverseOrder()); // descending
System.out.println(numbers); // [9, 8, 5, 3, 2, 1]
// Custom comparator
List<String> words = new ArrayList<>(List.of("banana", "apple", "cherry", "fig"));
Collections.sort(words, Comparator.comparingInt(String::length)
.thenComparing(Comparator.naturalOrder()));
System.out.println(words); // [fig, apple, banana, cherry]
// ── Reordering algorithms ──────────────────────────────────────────────
List<String> list = new ArrayList<>(List.of("A", "B", "C", "D", "E"));
Collections.reverse(list);
System.out.println(list); // [E, D, C, B, A]
Collections.shuffle(list);
System.out.println(list); // random order, e.g. [C, A, E, B, D]
Collections.shuffle(list, new Random(42)); // seeded for reproducible shuffle
// ── rotate — shift elements right by distance ─────────────────────────
List<Integer> r = new ArrayList<>(List.of(1, 2, 3, 4, 5));
Collections.rotate(r, 2); // shift right by 2
System.out.println(r); // [4, 5, 1, 2, 3]
Collections.rotate(r, -2); // shift left by 2 (negative distance)
System.out.println(r); // [1, 2, 3, 4, 5]
// ── swap and fill ──────────────────────────────────────────────────────
List<String> s = new ArrayList<>(List.of("A", "B", "C", "D"));
Collections.swap(s, 0, 3); // swap index 0 and 3
System.out.println(s); // [D, B, C, A]
Collections.fill(s, "X");
System.out.println(s); // [X, X, X, X]
// ── copy — destination must be at least as large as source ────────────
List<String> src = List.of("a", "b", "c");
List<String> dest = new ArrayList<>(List.of("X", "X", "X", "X", "X"));
Collections.copy(dest, src);
System.out.println(dest); // [a, b, c, X, X] — only first 3 overwrittenSearch and Statistics Methods
// ── Binary search ────────────────────────────────────────────────────
List<Integer> sorted = new ArrayList<>(List.of(1, 3, 5, 7, 9, 11));
int idx = Collections.binarySearch(sorted, 7);
System.out.println(idx); // 3 — found at index 3
int notFound = Collections.binarySearch(sorted, 6);
System.out.println(notFound); // -4 — not found
System.out.println(-(notFound) - 1); // 3 — insertion point
// Custom comparator binary search
List<String> byLength = new ArrayList<>(
List.of("fig", "apple", "cherry", "strawberry"));
Collections.sort(byLength, Comparator.comparingInt(String::length));
int pos = Collections.binarySearch(byLength, "banana",
Comparator.comparingInt(String::length));
System.out.println(pos); // index of "banana" or a negative insertion point
// ── Frequency and disjoint ────────────────────────────────────────────
List<String> items = List.of("apple", "banana", "apple", "cherry", "apple");
System.out.println(Collections.frequency(items, "apple")); // 3
System.out.println(Collections.frequency(items, "grape")); // 0
List<Integer> a = List.of(1, 2, 3, 4);
List<Integer> b = List.of(5, 6, 7, 8);
List<Integer> c = List.of(3, 4, 5, 6);
System.out.println(Collections.disjoint(a, b)); // true — no common elements
System.out.println(Collections.disjoint(a, c)); // false — 3 and 4 in both
// ── Min and max ────────────────────────────────────────────────────────
List<Integer> values = List.of(7, 2, 9, 1, 5, 8);
System.out.println(Collections.min(values)); // 1
System.out.println(Collections.max(values)); // 9
// With comparator:
List<String> strs = List.of("banana", "apple", "cherry");
System.out.println(Collections.min(strs,
Comparator.comparingInt(String::length))); // apple (shortest)
// ── nCopies ───────────────────────────────────────────────────────────
List<String> fiveHello = Collections.nCopies(5, "Hello");
System.out.println(fiveHello); // [Hello, Hello, Hello, Hello, Hello]
// Use to initialise an ArrayList of given size:
List<Integer> zeros = new ArrayList<>(Collections.nCopies(10, 0));
System.out.println(zeros); // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
zeros.set(3, 99); // mutable — can modify
System.out.println(zeros); // [0, 0, 0, 99, 0, 0, 0, 0, 0, 0]Unmodifiable Wrappers — Not Immutable
// ── Unmodifiable wrappers — write-through views ──────────────────────
List<String> mutable = new ArrayList<>(List.of("A", "B", "C"));
List<String> readOnly = Collections.unmodifiableList(mutable);
System.out.println(readOnly); // [A, B, C]
// Read operations work normally:
System.out.println(readOnly.get(0)); // A
System.out.println(readOnly.size()); // 3
System.out.println(readOnly.contains("B")); // true
// Write operations throw UnsupportedOperationException:
try {
readOnly.add("D"); // throws UnsupportedOperationException
} catch (UnsupportedOperationException e) {
System.out.println("Cannot modify");
}
// ── Critical: underlying collection CAN still change ─────────────────
mutable.add("D"); // modify the ORIGINAL
System.out.println(readOnly); // [A, B, C, D] — wrapper sees the change!
// This is NOT immutability — only modification through the wrapper is blocked
// ── True immutability — Java 9+ List.of() ────────────────────────────
List<String> immutable = List.of("A", "B", "C");
// immutable.add("D"); // throws UnsupportedOperationException
// Cannot be modified through ANY reference — no mutable backing exists
// ── Defensive publishing pattern ──────────────────────────────────────
public class ItemRepository {
private final List<Item> items = new ArrayList<>();
public void add(Item item) {
items.add(item);
}
// Returns unmodifiable view — external code cannot modify our list
public List<Item> getItems() {
return Collections.unmodifiableList(items);
}
// 'items' field is private — no external code can modify via original reference
}
// ── All unmodifiable variants ─────────────────────────────────────────
Set<String> roSet = Collections.unmodifiableSet(new HashSet<>());
Map<String, Integer> roMap = Collections.unmodifiableMap(new HashMap<>());
SortedSet<String> roSorted = Collections.unmodifiableSortedSet(new TreeSet<>());
SortedMap<String, Integer> roSMap = Collections.unmodifiableSortedMap(new TreeMap<>());
Collection<String> roColl = Collections.unmodifiableCollection(new ArrayList<>());Singleton, Empty, and Synchronised Collections
// ── Singleton collections — exactly one element ──────────────────────
List<String> oneList = Collections.singletonList("only");
Set<String> oneSet = Collections.singleton("only");
Map<String, Integer> oneMap = Collections.singletonMap("key", 42);
System.out.println(oneList.get(0)); // "only"
System.out.println(oneSet.contains("only")); // true
System.out.println(oneMap.get("key")); // 42
// Modifications throw:
oneList.add("second"); // UnsupportedOperationException
// ── Empty collections — canonical empty, no allocation ───────────────
List<String> empty = Collections.emptyList();
Set<String> emptyS = Collections.emptySet();
Map<String,Integer> emptyM = Collections.emptyMap();
// emptyList() returns the SAME INSTANCE every call:
System.out.println(Collections.emptyList() == Collections.emptyList()); // true
// Use as null-safe return value instead of null:
public List<User> findByRole(String role) {
List<User> result = userRepository.findByRole(role);
return result != null ? result : Collections.emptyList();
// Better: never return null from a method returning a collection
}
// ── Synchronised wrappers ──────────────────────────────────────────────
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// Individual operations are thread-safe:
syncList.add("A"); // synchronised
syncList.get(0); // synchronised
// WRONG — iteration is NOT thread-safe without external synchronisation:
for (String s : syncList) { // UNSAFE — another thread may modify during iteration
System.out.println(s);
}
// CORRECT — synchronise on the list object during iteration:
synchronized (syncList) {
for (String s : syncList) { // safe — lock held for entire iteration
System.out.println(s);
}
}
// ── Synchronised variants ─────────────────────────────────────────────
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());
SortedMap<String, Integer> syncSMap =
Collections.synchronizedSortedMap(new TreeMap<>());
// ── Prefer concurrent collections for high contention ─────────────────
// Instead of:
Map<String, Integer> sync = Collections.synchronizedMap(new HashMap<>());
// Use:
Map<String, Integer> concurrent = new ConcurrentHashMap<>();
// ConcurrentHashMap: lock-free reads, segment-level write locks → much faster