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
- Binary search needs a sorted list
- Unmodifiable views and immutable copies
- Empty and singleton collections
- Synchronised wrappers, and why to avoid them
- Choosing a collection: a decision path
- Complexity at a glance
- Worked choices
- Common mistakes
- Best practices
- Practice
- Conclusion
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
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))); // trueBinary 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 pointOn 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] - unaffectedAn 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 listReturn 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 -> ArrayListComplexity at a glance
| Collection | Add | Remove | Contains or get | Ordered |
|---|---|---|---|---|
ArrayList | O(1) at the end | O(n) | O(1) by index, O(n) by value | Insertion |
LinkedList | O(1) at the ends | O(1) at the ends | O(n) | Insertion |
ArrayDeque | O(1) | O(1) | O(n) | Insertion |
HashSet | O(1) | O(1) | O(1) | None |
LinkedHashSet | O(1) | O(1) | O(1) | Insertion |
TreeSet | O(log n) | O(log n) | O(log n) | Sorted |
HashMap | O(1) | O(1) | O(1) | None |
TreeMap | O(log n) | O(log n) | O(log n) | Sorted |
PriorityQueue | O(log n) | O(log n) | O(n) | Head only |
Worked choices
| Requirement | Choice | Why |
|---|---|---|
| The last 50 pages a user visited | LinkedHashMap in access order | Order matters and eviction is built in |
| Tags on a note, no duplicates, displayed alphabetically | TreeSet | Uniqueness plus sorting in one step |
| Counting words in a document | HashMap with merge | Fast lookup, order irrelevant |
| Jobs served by urgency | PriorityQueue | Always removes the most urgent |
| A request cache shared by threads | ConcurrentHashMap | Safe and fast under concurrency |
| A fixed list of allowed statuses | Set.of(...) or EnumSet | Immutable and cheap to test |
Common mistakes
- Using
List.containsin a loop where aSetwould make it constant time. - Calling
binarySearchon an unsorted list. - Returning
nullinstead of an empty collection. - Wrapping with
synchronizedListand assuming compound operations are safe. - Choosing
TreeMapwhen nothing needs sorting, and paying O(log n) for nothing. - Returning an unmodifiable view and calling it immutable.
Best practices
- Start with
ArrayListandHashMap, and change only for a stated reason. - Return empty collections, never
null. - Return
List.copyOffrom getters when the caller must not see later changes. - Use
EnumSetandEnumMapfor enum keys. - Use the concurrent collections rather than synchronised wrappers.
- Pick from access patterns, not from habit.
Practice
- Choose a collection for each: an ordered playlist, unique visitor identifiers, a leaderboard, an undo stack. Justify each.
- Explain the difference in behaviour between
Collections.unmodifiableList(x)andList.copyOf(x)afterxchanges. - Why does
binarySearchon an unsorted list return a wrong answer rather than throwing? - Rewrite a nested loop that checks membership with a
Set, and state the change in complexity. - 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.