Collections Utilities and Choosing the Right Collection

The helper methods that save writing loops, and a practical way to pick the right collection every time.

The Collections utility class

List<Integer> marks = new ArrayList<>(List.of(45, 78, 62, 91, 33));

Collections.sort(marks);
Collections.reverse(marks);
Collections.shuffle(marks);
Collections.swap(marks, 0, 4);

System.out.println(Collections.max(marks));            // 91
System.out.println(Collections.min(marks));            // 33
System.out.println(Collections.frequency(marks, 62));  // 1

Collections.fill(marks, 0);
List<String> blanks = Collections.nCopies(3, "n/a");   // immutable

System.out.println(Collections.disjoint(List.of(1, 2), List.of(3, 4)));  // true

Binary search needs a sorted list

List<Integer> sorted = new ArrayList<>(List.of(10, 20, 30, 40));
System.out.println(Collections.binarySearch(sorted, 30));    // 2
System.out.println(Collections.binarySearch(sorted, 35));    // negative insertion point

On an unsorted list the result is meaningless rather than an error, which is a quiet source of bugs.

Unmodifiable views and immutable copies

List<String> live = new ArrayList<>(List.of("a", "b"));

List<String> view = Collections.unmodifiableList(live);  // a read only window
live.add("c");
System.out.println(view);      // [a, b, c] - the view still sees changes

List<String> snapshot = List.copyOf(live);               // an independent copy
live.add("d");
System.out.println(snapshot);  // [a, b, c] - unaffected
An unmodifiable view blocks writes through itself but still reflects changes to the original. List.copyOf takes a real snapshot. Choose deliberately, because returning a view from a getter can still surprise a caller.

Empty and singleton collections

return Collections.emptyList();       // no allocation, immutable
return List.of();                      // the modern equivalent
return List.of(single);                // a one element immutable list

Return an empty collection rather than null. Callers can then loop without a null check, which removes an entire class of bug.

Synchronised wrappers, and why to avoid them

List<String> wrapped = Collections.synchronizedList(new ArrayList<>());

// Each call is atomic, but this pair is not
if (!wrapped.contains("x")) {
    wrapped.add("x");            // another thread may have added it in between
}

// Compound operations still need external synchronisation
synchronized (wrapped) {
    if (!wrapped.contains("x")) {
        wrapped.add("x");
    }
}

Prefer ConcurrentHashMap, CopyOnWriteArrayList and the concurrent queues, which are designed for this and perform far better.

Choosing a collection: a decision path

Do you store pairs of key and value?
  yes -> Map
          need sorted keys or range queries?  -> TreeMap
          need predictable iteration order?   -> LinkedHashMap
          shared between threads?             -> ConcurrentHashMap
          keys are enum constants?            -> EnumMap
          otherwise                           -> HashMap
  no  -> must elements be unique?
          yes -> Set
                  sorted or range queries?    -> TreeSet
                  insertion order matters?    -> LinkedHashSet
                  enum elements?              -> EnumSet
                  otherwise                   -> HashSet
          no  -> is it processed in an order?
                  by priority                 -> PriorityQueue
                  FIFO or LIFO                -> ArrayDeque
                  across threads              -> a BlockingQueue
                  otherwise                   -> ArrayList

Complexity at a glance

CollectionAddRemoveContains or getOrdered
ArrayListO(1) at the endO(n)O(1) by index, O(n) by valueInsertion
LinkedListO(1) at the endsO(1) at the endsO(n)Insertion
ArrayDequeO(1)O(1)O(n)Insertion
HashSetO(1)O(1)O(1)None
LinkedHashSetO(1)O(1)O(1)Insertion
TreeSetO(log n)O(log n)O(log n)Sorted
HashMapO(1)O(1)O(1)None
TreeMapO(log n)O(log n)O(log n)Sorted
PriorityQueueO(log n)O(log n)O(n)Head only

Worked choices

RequirementChoiceWhy
The last 50 pages a user visitedLinkedHashMap in access orderOrder matters and eviction is built in
Tags on a note, no duplicates, displayed alphabeticallyTreeSetUniqueness plus sorting in one step
Counting words in a documentHashMap with mergeFast lookup, order irrelevant
Jobs served by urgencyPriorityQueueAlways removes the most urgent
A request cache shared by threadsConcurrentHashMapSafe and fast under concurrency
A fixed list of allowed statusesSet.of(...) or EnumSetImmutable and cheap to test

Common mistakes

  • Using List.contains in a loop where a Set would make it constant time.
  • Calling binarySearch on an unsorted list.
  • Returning null instead of an empty collection.
  • Wrapping with synchronizedList and assuming compound operations are safe.
  • Choosing TreeMap when nothing needs sorting, and paying O(log n) for nothing.
  • Returning an unmodifiable view and calling it immutable.

Best practices

  • Start with ArrayList and HashMap, and change only for a stated reason.
  • Return empty collections, never null.
  • Return List.copyOf from getters when the caller must not see later changes.
  • Use EnumSet and EnumMap for enum keys.
  • Use the concurrent collections rather than synchronised wrappers.
  • Pick from access patterns, not from habit.

Practice

  1. Choose a collection for each: an ordered playlist, unique visitor identifiers, a leaderboard, an undo stack. Justify each.
  2. Explain the difference in behaviour between Collections.unmodifiableList(x) and List.copyOf(x) after x changes.
  3. Why does binarySearch on an unsorted list return a wrong answer rather than throwing?
  4. Rewrite a nested loop that checks membership with a Set, and state the change in complexity.
  5. Show a compound operation on a synchronised list that is still unsafe, and fix it.

Conclusion

Pick a collection from how the data will be read and written, not from habit. Default to ArrayList and HashMap, return empty rather than null, prefer immutable copies at boundaries, and use the concurrent collections when threads are involved.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.