The Collections Framework

Collections Utility & Comparators

15 min Lesson 10 of 14

Collections Utility & Comparators

The java.util.Collections class is a toolkit of static helper methods that operate on List, Set, and Map instances. It handles sorting, searching, shuffling, frequency counting, and wrapping collections in read-only or thread-safe views — all without you writing the algorithm yourself. In this lesson you will also learn how to build expressive, chainable Comparator objects so you can sort on any criteria you need.

Sorting with Collections.sort

The simplest use of the utility class is sorting a List whose elements implement Comparable. Strings and numbers already implement it, so sorting them takes one line:

import java.util.ArrayList; import java.util.Collections; import java.util.List; List<String> names = new ArrayList<>(List.of("Charlie", "Alice", "Bob")); Collections.sort(names); System.out.println(names); // [Alice, Bob, Charlie]

Under the hood, Collections.sort delegates to List.sort, which uses a stable merge sort (TimSort). Stable means equal elements keep their original relative order — important when you sort by one field and previously sorted by another.

List.sort vs Collections.sort: Since Java 8, list.sort(comparator) is the idiomatic choice — it is equally efficient and reads more naturally. Collections.sort is older but still widely seen in code you will maintain.

Reversing and Shuffling

Collections.reverse reverses the order of a list in place. Collections.shuffle randomises it, optionally with a seeded Random for reproducibility:

List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5)); Collections.reverse(numbers); System.out.println(numbers); // [5, 4, 3, 2, 1] Collections.shuffle(numbers); System.out.println(numbers); // random order, e.g. [3, 1, 5, 2, 4]
Use shuffle for fair draws. If you need to pick items randomly without duplicates — a quiz drawing random questions from a pool, for example — shuffle the list and take the first N elements. This is cleaner than generating random indices and checking for repeats.

Unmodifiable and Synchronized Views

Sometimes you want to share a collection with other code but prevent that code from changing it. Collections.unmodifiableList (and the equivalent for Set and Map) wraps the original collection in a view that throws UnsupportedOperationException on any write:

List<String> mutable = new ArrayList<>(List.of("read", "only")); List<String> readOnly = Collections.unmodifiableList(mutable); readOnly.get(0); // OK readOnly.add("extra"); // throws UnsupportedOperationException
The original list is still mutable. An unmodifiable view does not copy the data — it just blocks writes through that reference. If you add to mutable above, readOnly will reflect the change. For a truly immutable snapshot, use List.copyOf(mutable) (Java 10+).

Collections.synchronizedList wraps a list so each individual method call is thread-safe. For more demanding concurrency use CopyOnWriteArrayList or ConcurrentHashMap instead — but the synchronized wrappers are a quick option when you just need basic safety.

Other Handy Utility Methods

  • Collections.min(coll) / Collections.max(coll) — find the smallest or largest element.
  • Collections.frequency(coll, obj) — count how many times obj appears.
  • Collections.nCopies(n, obj) — create an immutable list of n copies of obj (useful for padding or default values).
  • Collections.disjoint(c1, c2) — returns true if the two collections share no elements.
  • Collections.swap(list, i, j) — swap elements at two positions.

Building Comparators

When elements do not implement Comparable, or you want a different sort order, you pass a Comparator. The factory method Comparator.comparing creates one from a key-extractor function:

import java.util.Comparator; record Product(String name, double price, int stock) {} List<Product> products = new ArrayList<>(List.of( new Product("Widget", 9.99, 200), new Product("Gadget", 24.99, 50), new Product("Doohickey", 4.99, 500) )); // sort by price, ascending products.sort(Comparator.comparing(Product::price)); products.forEach(p -> System.out.println(p.name() + " $" + p.price())); // Doohickey $4.99 | Widget $9.99 | Gadget $24.99

Chaining Comparators

Real sorting often requires a primary key, then a tiebreaker. Comparator provides thenComparing for exactly this:

record Employee(String department, String name, int salary) {} List<Employee> staff = new ArrayList<>(List.of( new Employee("Engineering", "Zara", 90_000), new Employee("Marketing", "Alice", 70_000), new Employee("Engineering", "Alice", 85_000), new Employee("Marketing", "Bob", 70_000) )); Comparator<Employee> byDeptThenName = Comparator.comparing(Employee::department) .thenComparing(Employee::name); staff.sort(byDeptThenName); staff.forEach(e -> System.out.println(e.department() + " | " + e.name())); // Engineering | Alice // Engineering | Zara // Marketing | Alice // Marketing | Bob

Reversing and Null-Safe Comparators

Call .reversed() on any comparator to flip its order without rewriting the key extractor:

// most expensive first products.sort(Comparator.comparing(Product::price).reversed());

If a key can be null, wrap the comparator with Comparator.nullsFirst or Comparator.nullsLast:

// sort by optional middle name; nulls go to the end Comparator<Person> byMiddleName = Comparator.comparing(Person::middleName, Comparator.nullsLast(Comparator.naturalOrder()));
Keep comparators as named constants or fields. If you sort the same list in multiple places, extract the Comparator into a static final constant. This makes tests straightforward and avoids subtle bugs from accidentally building the comparator differently each time.

Putting It Together

Here is a realistic example combining the utility class and a chained comparator:

List<Product> catalog = new ArrayList<>(List.of( new Product("Widget", 9.99, 200), new Product("Gadget", 24.99, 50), new Product("Gizmo", 24.99, 150), new Product("Doohickey", 4.99, 500) )); // cheapest first; ties broken by highest stock Comparator<Product> displayOrder = Comparator.comparingDouble(Product::price) .thenComparingInt(Product::stock).reversed() // reversed flips both keys; undo for stock by chaining again .thenComparingInt(Product::stock); // Actually, cleaner approach: build two separate comparators Comparator<Product> byPrice = Comparator.comparingDouble(Product::price); Comparator<Product> byStockDesc = Comparator.comparingInt(Product::stock).reversed(); catalog.sort(byPrice.thenComparing(byStockDesc)); catalog.forEach(p -> System.out.println(p.name() + " $" + p.price() + " stock=" + p.stock())); // Doohickey $4.99 stock=500 // Widget $9.99 stock=200 // Gizmo $24.99 stock=150 // Gadget $24.99 stock=50 List<Product> readOnlyCatalog = Collections.unmodifiableList(catalog); System.out.println("Best deal: " + Collections.min(catalog, byPrice).name());

Summary

The Collections utility class gives you production-quality algorithms (sort, reverse, shuffle, min/max, frequency, unmodifiable/synchronized views) in a single import. The Comparator API lets you express any sort criterion with readable, composable code — chaining thenComparing, reversed, and null-safe wrappers instead of writing nested if blocks. Together they complete the Collections Framework toolbox you have built across this tutorial.