Java Collections Utility Class
Java Collections Utility Class
java.util.Collections is a final class with nothing but static methods. Every method it provides operates on an existing Collection or List — it creates nothing, holds nothing, and cannot be instantiated. The class covers six functional areas: sorting and searching, reordering, finding extremes and frequencies, safe read-only wrappers, synchronised thread-safe wrappers, and factory methods for fixed-size or empty collections. Knowing these methods saves writing the same utility logic from scratch across every project.
What Is java.util.Collections?
java.util.Collections is a utility class — final, with a private constructor, containing only static methods. Every method it defines works on existing collection objects: it sorts them, wraps them, searches them, or creates constrained versions of them.
java.util.Collections
← final class, cannot be subclassed
← cannot be instantiated (private constructor)
← all methods are static
← since Java 1.2, enhanced through Java 21
FUNCTIONAL AREAS:
┌─────────────────────────────────────────────────────────────────┐
│ SORT & SEARCH │
│ sort(list) reverse natural order │
│ sort(list, comparator) custom order │
│ binarySearch(list, key) O(log n) — list must be sorted │
│ binarySearch(list, key, cmp) with Comparator │
├─────────────────────────────────────────────────────────────────┤
│ REORDERING │
│ reverse(list) in-place reversal │
│ shuffle(list) random reorder │
│ shuffle(list, random) seeded random reorder │
│ swap(list, i, j) exchange two elements │
│ rotate(list, distance) circular shift │
│ fill(list, element) overwrite all with one value │
│ copy(dest, src) bulk copy from src to dest │
├─────────────────────────────────────────────────────────────────┤
│ EXTREMES & STATISTICS │
│ min(collection) smallest by natural order │
│ min(collection, comparator) smallest by Comparator │
│ max(collection) largest by natural order │
│ max(collection, comparator) largest by Comparator │
│ frequency(collection, element) count of element occurrences │
│ disjoint(c1, c2) true if no common elements │
├─────────────────────────────────────────────────────────────────┤
│ UNMODIFIABLE WRAPPERS │
│ unmodifiableList(list) read-only List view │
│ unmodifiableSet(set) read-only Set view │
│ unmodifiableSortedSet(set) read-only SortedSet view │
│ unmodifiableMap(map) read-only Map view │
│ unmodifiableSortedMap(map) read-only SortedMap view │
│ unmodifiableCollection(coll) read-only Collection view │
├─────────────────────────────────────────────────────────────────┤
│ SYNCHRONIZED WRAPPERS │
│ synchronizedList(list) global-lock thread-safe List │
│ synchronizedSet(set) global-lock thread-safe Set │
│ synchronizedSortedSet(set) global-lock SortedSet │
│ synchronizedMap(map) global-lock thread-safe Map │
│ synchronizedSortedMap(map) global-lock SortedMap │
│ synchronizedCollection(coll) global-lock Collection │
├─────────────────────────────────────────────────────────────────┤
│ FACTORY — FIXED SIZE & EMPTY │
│ singletonList(element) immutable one-element List │
│ singleton(element) immutable one-element Set │
│ singletonMap(key, value) immutable one-entry Map │
│ nCopies(n, element) immutable List of n copies │
│ emptyList() immutable empty List │
│ emptySet() immutable empty Set │
│ emptyMap() immutable empty Map │
│ EMPTY_LIST, EMPTY_SET, EMPTY_MAP ← raw-type constants (avoid)│
└─────────────────────────────────────────────────────────────────┘
When to Use Collections Methods
USE Collections.sort() WHEN: - Sorting a List in natural or custom order in-place - Equivalent to list.sort(null) or list.sort(comparator) — pick either USE Collections.binarySearch() WHEN: - The list is already sorted and O(log n) lookup is needed - Do NOT call on unsorted lists — undefined behaviour (wrong results) USE Collections.reverse() / shuffle() / rotate() / swap() WHEN: - In-place reordering without creating a new collection - shuffle() for randomising test data or game mechanics USE Collections.min() / max() / frequency() / disjoint() WHEN: - Quick one-liner statistics without a stream pipeline - disjoint() to check overlap between two collections USE unmodifiable*() WRAPPERS WHEN: - Returning a collection from a method and callers must not modify it - Java 9+ List.of(), Set.of(), Map.of() are cleaner alternatives - Note: unmodifiable is a VIEW — the backing collection is still mutable USE synchronized*() WRAPPERS WHEN: - Legacy code requires thread-safe wrappers on existing collections - For new code, prefer ConcurrentHashMap, CopyOnWriteArrayList - Iteration STILL requires external synchronized block even with wrapper USE singletonList / emptyList WHEN: - Returning a fixed result from a method without allocating a full list - API methods that accept Collection but you have zero or one item
How Collections Methods Work Internally
sort(List<T> list):
Delegates to list.sort(null) since Java 8.
Uses TimSort — stable, O(n log n), optimised for partially sorted data.
REQUIRES T implements Comparable, or ClassCastException at runtime.
binarySearch(List<? extends Comparable<T>> list, T key):
Requires list to be sorted in ascending natural order.
Returns: index of key if found.
-(insertionPoint) - 1 if not found.
insertionPoint = where key would be inserted to maintain order.
Uses RandomAccess check: ArrayList gets true binary search O(log n).
LinkedList falls back to linear search O(n) — binary search on linked
lists cannot do O(log n) jumps.
unmodifiableList(List<T> list):
Returns an UnmodifiableList that wraps the original.
All mutating methods (add, remove, set, clear) throw UnsupportedOperationException.
Read methods (get, size, iterator) delegate to the backing list.
THE BACKING LIST IS STILL MUTABLE — changes to it are visible through the wrapper.
This is a VIEW, not a copy.
synchronizedList(List<T> list):
Returns a SynchronizedList wrapping the original.
Every method is synchronized on the wrapper object (global lock).
Iteration is NOT automatically synchronised:
synchronized(synchronizedList) { for (T t : synchronizedList) {...} }
For new code: prefer CopyOnWriteArrayList for read-heavy lists.
Core Operations with Examples
Sorting, Searching, and Reordering
1// File: CollectionsSortSearchDemo.java
2
3import java.util.ArrayList;
4import java.util.Collections;
5import java.util.Comparator;
6import java.util.List;
7
8public class CollectionsSortSearchDemo {
9
10 public static void main(String[] args) {
11
12 List<Integer> scores = new ArrayList<>(List.of(72, 45, 91, 38, 67, 88, 55, 79));
13
14 System.out.println("Original : " + scores);
15
16 // sort() — natural order ascending
17 Collections.sort(scores);
18 System.out.println("Sorted ASC: " + scores);
19
20 // sort() with Comparator — descending
21 Collections.sort(scores, Comparator.reverseOrder());
22 System.out.println("Sorted DSC: " + scores);
23
24 // reverse() — in-place reversal (back to ascending)
25 Collections.reverse(scores);
26 System.out.println("Reversed : " + scores);
27
28 System.out.println();
29
30 // binarySearch() — list must be sorted ascending first
31 int target = 67;
32 int idx = Collections.binarySearch(scores, target);
33 System.out.println("binarySearch(" + target + ") → index " + idx
34 + " value=" + scores.get(idx));
35
36 // binarySearch on absent key — returns negative insertion point
37 int absent = 60;
38 int absIdx = Collections.binarySearch(scores, absent);
39 System.out.println("binarySearch(" + absent + ") → " + absIdx
40 + " (not found; insertion point = " + (-(absIdx + 1)) + ")");
41
42 System.out.println();
43
44 // shuffle() — randomise for quiz/game mechanics
45 Collections.shuffle(scores, new java.util.Random(42)); // seeded for reproducibility
46 System.out.println("Shuffled : " + scores);
47
48 // swap() — exchange positions 0 and scores.size()-1
49 Collections.swap(scores, 0, scores.size() - 1);
50 System.out.println("After swap(0, last): " + scores);
51
52 System.out.println();
53
54 // rotate() — circular shift right by distance
55 List<String> pipeline = new ArrayList<>(List.of("VALIDATE","AUTH","PROCESS","RESPOND"));
56 System.out.println("Before rotate : " + pipeline);
57 Collections.rotate(pipeline, 1); // shift right by 1
58 System.out.println("rotate(+1) : " + pipeline);
59 Collections.rotate(pipeline, -2); // shift left by 2
60 System.out.println("rotate(-2) : " + pipeline);
61
62 System.out.println();
63
64 // fill() — overwrite every element with a single value
65 List<String> slots = new ArrayList<>(List.of("A","B","C","D","E"));
66 System.out.println("Before fill : " + slots);
67 Collections.fill(slots, "EMPTY");
68 System.out.println("After fill : " + slots);
69 }
70}Output:
Original : [72, 45, 91, 38, 67, 88, 55, 79]
Sorted ASC: [38, 45, 55, 67, 72, 79, 88, 91]
Sorted DSC: [91, 88, 79, 72, 67, 55, 45, 38]
Reversed : [38, 45, 55, 67, 72, 79, 88, 91]
binarySearch(67) → index 3 value=67
binarySearch(60) → -4 (not found; insertion point = 3)
Shuffled : [72, 79, 91, 55, 38, 67, 88, 45]
After swap(0, last): [45, 79, 91, 55, 38, 67, 88, 72]
Before rotate : [VALIDATE, AUTH, PROCESS, RESPOND]
rotate(+1) : [RESPOND, VALIDATE, AUTH, PROCESS]
rotate(-2) : [AUTH, PROCESS, RESPOND, VALIDATE]
Before fill : [A, B, C, D, E]
After fill : [EMPTY, EMPTY, EMPTY, EMPTY, EMPTY]
Extremes, Frequency, and Disjoint
1// File: CollectionsStatsDemo.java
2
3import java.util.Collections;
4import java.util.Comparator;
5import java.util.List;
6import java.util.Set;
7
8public class CollectionsStatsDemo {
9
10 record Product(String sku, String name, double price) {
11 @Override public String toString() {
12 return name + "(Rs." + price + ")";
13 }
14 }
15
16 public static void main(String[] args) {
17
18 // min() and max() — natural ordering
19 List<Integer> temps = List.of(28, 33, 19, 41, 25, 37, 22);
20 System.out.println("Temperatures: " + temps);
21 System.out.println("min() : " + Collections.min(temps));
22 System.out.println("max() : " + Collections.max(temps));
23
24 System.out.println();
25
26 // min() and max() — with Comparator
27 List<Product> products = List.of(
28 new Product("P001", "Mouse", 999.0),
29 new Product("P002", "Keyboard", 2499.0),
30 new Product("P003", "Monitor", 18999.0),
31 new Product("P004", "Webcam", 3499.0),
32 new Product("P005", "USB Hub", 799.0)
33 );
34 Comparator<Product> byPrice = Comparator.comparingDouble(Product::price);
35 System.out.println("Cheapest : " + Collections.min(products, byPrice));
36 System.out.println("Priciest : " + Collections.max(products, byPrice));
37
38 System.out.println();
39
40 // frequency() — count occurrences of an element
41 List<String> tags = List.of("java","spring","java","hibernate","java","spring","kafka");
42 System.out.println("Tags: " + tags);
43 System.out.println("frequency(java) : " + Collections.frequency(tags, "java"));
44 System.out.println("frequency(spring) : " + Collections.frequency(tags, "spring"));
45 System.out.println("frequency(hibernate) : " + Collections.frequency(tags, "hibernate"));
46 System.out.println("frequency(docker) : " + Collections.frequency(tags, "docker"));
47
48 System.out.println();
49
50 // disjoint() — true if collections share NO elements
51 Set<String> backendSkills = Set.of("Java","Spring","MySQL","Docker");
52 Set<String> frontendSkills = Set.of("React","CSS","TypeScript","Node");
53 Set<String> cloudSkills = Set.of("Docker","Kubernetes","Terraform","AWS");
54
55 System.out.println("backend ∩ frontend empty? " +
56 Collections.disjoint(backendSkills, frontendSkills)); // true — no overlap
57 System.out.println("backend ∩ cloud empty? " +
58 Collections.disjoint(backendSkills, cloudSkills)); // false — Docker in both
59
60 System.out.println();
61
62 // nCopies() — immutable list of n identical elements
63 List<String> defaultSlots = Collections.nCopies(5, "AVAILABLE");
64 System.out.println("nCopies(5, AVAILABLE): " + defaultSlots);
65 System.out.println("size: " + defaultSlots.size());
66
67 // copy() — bulk copy from source into destination (dest must be pre-sized)
68 List<String> source = List.of("Alpha","Beta","Gamma");
69 List<String> dest = new java.util.ArrayList<>(Collections.nCopies(3, ""));
70 Collections.copy(dest, source);
71 System.out.println("After copy(): " + dest);
72 }
73}Output:
Temperatures: [28, 33, 19, 41, 25, 37, 22]
min() : 19
max() : 41
Cheapest : USB Hub(Rs.799.0)
Priciest : Monitor(Rs.18999.0)
Tags: [java, spring, java, hibernate, java, spring, kafka]
frequency(java) : 3
frequency(spring) : 2
frequency(hibernate) : 1
frequency(docker) : 0
backend ∩ frontend empty? true
backend ∩ cloud empty? false
nCopies(5, AVAILABLE): [AVAILABLE, AVAILABLE, AVAILABLE, AVAILABLE, AVAILABLE]
size: 5
After copy(): [Alpha, Beta, Gamma]
Unmodifiable and Synchronized Wrappers
1// File: CollectionsWrappersDemo.java
2
3import java.util.ArrayList;
4import java.util.Collections;
5import java.util.HashMap;
6import java.util.HashSet;
7import java.util.List;
8import java.util.Map;
9import java.util.Set;
10
11public class CollectionsWrappersDemo {
12
13 public static void main(String[] args) {
14
15 // unmodifiableList — read-only view of an existing list
16 System.out.println("=== unmodifiableList ===");
17 List<String> mutable = new ArrayList<>(List.of("Java","Spring","MySQL"));
18 List<String> readOnly = Collections.unmodifiableList(mutable);
19
20 System.out.println("Read-only list: " + readOnly);
21 System.out.println("get(0) : " + readOnly.get(0)); // reads work
22
23 try {
24 readOnly.add("Kafka"); // mutation throws immediately
25 } catch (UnsupportedOperationException e) {
26 System.out.println("add() throws UnsupportedOperationException");
27 }
28
29 // KEY: backing list mutation IS reflected in the wrapper
30 mutable.add("Docker"); // modifies backing list
31 System.out.println("After backing add: " + readOnly); // Docker visible!
32 System.out.println("(wrapper is a VIEW — backing list change is visible)");
33
34 System.out.println();
35
36 // unmodifiableMap — protect configuration maps
37 System.out.println("=== unmodifiableMap ===");
38 Map<String, String> config = new HashMap<>();
39 config.put("db.host", "localhost");
40 config.put("db.port", "5432");
41 Map<String, String> safeConfig = Collections.unmodifiableMap(config);
42 System.out.println("Config: " + safeConfig);
43
44 try {
45 safeConfig.put("db.user", "admin");
46 } catch (UnsupportedOperationException e) {
47 System.out.println("put() throws UnsupportedOperationException");
48 }
49
50 System.out.println();
51
52 // Factory collections — empty and singleton
53 System.out.println("=== Factory collections ===");
54 List<String> empty = Collections.emptyList();
55 List<String> oneItem = Collections.singletonList("JavaOnly");
56 Map<String, Integer> oneEntry = Collections.singletonMap("score", 99);
57 Set<String> oneElement = Collections.singleton("UniqueTag");
58
59 System.out.println("emptyList() : " + empty + " size=" + empty.size());
60 System.out.println("singletonList() : " + oneItem + " size=" + oneItem.size());
61 System.out.println("singletonMap() : " + oneEntry + " size=" + oneEntry.size());
62 System.out.println("singleton() : " + oneElement + " size=" + oneElement.size());
63
64 try {
65 oneItem.add("AnotherItem");
66 } catch (UnsupportedOperationException e) {
67 System.out.println("singletonList.add() throws UnsupportedOperationException");
68 }
69
70 System.out.println();
71
72 // synchronizedList — thread-safe wrapper (legacy pattern)
73 System.out.println("=== synchronizedList ===");
74 List<String> threadSafeList = Collections.synchronizedList(new ArrayList<>());
75 threadSafeList.add("entry1");
76 threadSafeList.add("entry2");
77 System.out.println("synchronizedList: " + threadSafeList);
78 System.out.println("(iteration still requires: synchronized(list) { for(...){} })");
79 }
80}Output:
=== unmodifiableList ===
Read-only list: [Java, Spring, MySQL]
get(0) : Java
add() throws UnsupportedOperationException
After backing add: [Java, Spring, MySQL, Docker]
(wrapper is a VIEW — backing list change is visible)
=== unmodifiableMap ===
Config: {db.port=5432, db.host=localhost}
put() throws UnsupportedOperationException
=== Factory collections ===
emptyList() : [] size=0
singletonList() : [JavaOnly] size=1
singletonMap() : {score=99} size=1
singleton() : [UniqueTag] size=1
singletonList.add() throws UnsupportedOperationException
=== synchronizedList ===
synchronizedList: [entry1, entry2]
(iteration still requires: synchronized(list) { for(...){} })
Real-World Example — Razorpay Payment Batch Processor
A payment batch processor at Razorpay uses Collections utility methods across the full processing lifecycle. shuffle() randomises the processing order to distribute load evenly across payment gateways. sort() and binarySearch() enable O(log n) duplicate detection. unmodifiableList() protects the audit log from accidental modification. min() and max() compute batch statistics. frequency() counts retry attempts per payment.
1// File: PaymentEntry.java
2
3public record PaymentEntry(
4 String paymentId,
5 String merchantId,
6 double amount,
7 String gateway,
8 int retryCount,
9 String status) implements Comparable<PaymentEntry> {
10
11 // Natural ordering by paymentId — used by binarySearch for duplicate detection
12 @Override
13 public int compareTo(PaymentEntry other) {
14 return this.paymentId.compareTo(other.paymentId);
15 }
16
17 @Override
18 public String toString() {
19 return String.format("[%s] %-8s Rs.%7.2f %-10s retries=%d %s",
20 paymentId, merchantId, amount, gateway, retryCount, status);
21 }
22}1// File: PaymentBatchProcessor.java
2
3import java.util.ArrayList;
4import java.util.Collections;
5import java.util.Comparator;
6import java.util.List;
7
8public class PaymentBatchProcessor {
9
10 // Audit log — callers must not modify it
11 private final List<String> auditLog = new ArrayList<>();
12 private final List<String> readOnlyAudit = Collections.unmodifiableList(auditLog);
13
14 public void processBatch(List<PaymentEntry> batch) {
15
16 System.out.println("=== BATCH RECEIVED: " + batch.size() + " entries ===");
17 batch.forEach(p -> System.out.println(" " + p));
18
19 // Step 1: Shuffle to distribute gateway load evenly — no one gateway first
20 System.out.println("\n--- Step 1: Shuffle to randomise gateway routing ---");
21 List<PaymentEntry> processing = new ArrayList<>(batch);
22 Collections.shuffle(processing, new java.util.Random(7));
23 processing.forEach(p -> System.out.println(" " + p));
24
25 // Step 2: Sort by paymentId for O(log n) duplicate detection
26 System.out.println("\n--- Step 2: Sort for binary-search duplicate check ---");
27 List<PaymentEntry> sorted = new ArrayList<>(batch);
28 Collections.sort(sorted); // natural order by paymentId
29 System.out.println(" Sorted IDs: " +
30 sorted.stream().map(PaymentEntry::paymentId).toList());
31
32 // binarySearch — O(log n) presence check
33 PaymentEntry probe = new PaymentEntry("PAY-003","",0,"",0,"");
34 int idx = Collections.binarySearch(sorted, probe);
35 System.out.println(" binarySearch(PAY-003) → " + (idx >= 0 ? "FOUND at index " + idx : "NOT FOUND"));
36
37 // Step 3: Gateway failure stats using frequency()
38 System.out.println("\n--- Step 3: Retry count frequency analysis ---");
39 List<Integer> retryCounts = batch.stream()
40 .map(PaymentEntry::retryCount).toList();
41 System.out.println(" Retry count distribution:");
42 for (int r = 0; r <= 3; r++) {
43 int count = Collections.frequency(retryCounts, r);
44 if (count > 0) {
45 System.out.println(" retries=" + r + " → " + count + " payment(s)");
46 }
47 }
48
49 // Step 4: Batch statistics using min/max with Comparator
50 System.out.println("\n--- Step 4: Batch statistics ---");
51 Comparator<PaymentEntry> byAmount = Comparator.comparingDouble(PaymentEntry::amount);
52 PaymentEntry lowest = Collections.min(batch, byAmount);
53 PaymentEntry highest = Collections.max(batch, byAmount);
54 double total = batch.stream().mapToDouble(PaymentEntry::amount).sum();
55 System.out.printf(" Smallest : %s%n", lowest);
56 System.out.printf(" Largest : %s%n", highest);
57 System.out.printf(" Total : Rs.%.2f%n", total);
58
59 // Step 5: Sort by status + amount descending for prioritised processing
60 System.out.println("\n--- Step 5: Prioritised processing order ---");
61 Comparator<PaymentEntry> processingOrder =
62 Comparator.comparing(PaymentEntry::status,
63 Comparator.comparingInt(s -> switch(s) {
64 case "RETRY" -> 0; // retries first
65 case "PENDING" -> 1; // then fresh
66 default -> 2;
67 }))
68 .thenComparingDouble(PaymentEntry::amount).reversed(); // highest amount first
69 List<PaymentEntry> prioritised = new ArrayList<>(batch);
70 prioritised.sort(processingOrder);
71 prioritised.forEach(p -> System.out.println(" " + p));
72
73 // Step 6: Audit log — unmodifiable to prevent tampering
74 auditLog.add("BATCH_START: " + batch.size() + " entries");
75 batch.forEach(p -> auditLog.add("PROCESSED: " + p.paymentId()));
76 auditLog.add("BATCH_END: total Rs." + String.format("%.2f", total));
77
78 System.out.println("\n--- Audit log (read-only view) ---");
79 readOnlyAudit.forEach(entry -> System.out.println(" " + entry));
80
81 try {
82 readOnlyAudit.add("TAMPER"); // rejected
83 } catch (UnsupportedOperationException e) {
84 System.out.println(" [audit] Add rejected — unmodifiable view");
85 }
86 }
87
88 public static void main(String[] args) {
89
90 List<PaymentEntry> batch = List.of(
91 new PaymentEntry("PAY-005","M-Swiggy", 2499.0, "Razorpay", 0, "PENDING"),
92 new PaymentEntry("PAY-001","M-Zepto", 349.0, "PayU", 2, "RETRY"),
93 new PaymentEntry("PAY-003","M-Zomato", 1299.0, "Razorpay", 0, "PENDING"),
94 new PaymentEntry("PAY-002","M-CRED", 4999.0, "Cashfree", 1, "RETRY"),
95 new PaymentEntry("PAY-004","M-Meesho", 799.0, "PayU", 0, "PENDING")
96 );
97
98 new PaymentBatchProcessor().processBatch(batch);
99 }
100}Output:
=== BATCH RECEIVED: 5 entries ===
[PAY-005] M-Swiggy Rs. 2499.00 Razorpay retries=0 PENDING
[PAY-001] M-Zepto Rs. 349.00 PayU retries=2 RETRY
[PAY-003] M-Zomato Rs. 1299.00 Razorpay retries=0 PENDING
[PAY-002] M-CRED Rs. 4999.00 Cashfree retries=1 RETRY
[PAY-004] M-Meesho Rs. 799.00 PayU retries=0 PENDING
--- Step 1: Shuffle to randomise gateway routing ---
[PAY-003] M-Zomato Rs. 1299.00 Razorpay retries=0 PENDING
[PAY-005] M-Swiggy Rs. 2499.00 Razorpay retries=0 PENDING
[PAY-001] M-Zepto Rs. 349.00 PayU retries=2 RETRY
[PAY-004] M-Meesho Rs. 799.00 PayU retries=0 PENDING
[PAY-002] M-CRED Rs. 4999.00 Cashfree retries=1 RETRY
--- Step 2: Sort for binary-search duplicate check ---
Sorted IDs: [PAY-001, PAY-002, PAY-003, PAY-004, PAY-005]
binarySearch(PAY-003) → FOUND at index 2
--- Step 3: Retry count frequency analysis ---
Retry count distribution:
retries=0 → 3 payment(s)
retries=1 → 1 payment(s)
retries=2 → 1 payment(s)
--- Step 4: Batch statistics ---
Smallest : [PAY-001] M-Zepto Rs. 349.00 PayU retries=2 RETRY
Largest : [PAY-002] M-CRED Rs. 4999.00 Cashfree retries=1 RETRY
Total : Rs.9945.00
--- Step 5: Prioritised processing order ---
[PAY-002] M-CRED Rs. 4999.00 Cashfree retries=1 RETRY
[PAY-001] M-Zepto Rs. 349.00 PayU retries=2 RETRY
[PAY-005] M-Swiggy Rs. 2499.00 Razorpay retries=0 PENDING
[PAY-003] M-Zomato Rs. 1299.00 Razorpay retries=0 PENDING
[PAY-004] M-Meesho Rs. 799.00 PayU retries=0 PENDING
--- Audit log (read-only view) ---
BATCH_START: 5 entries
PROCESSED: PAY-005
PROCESSED: PAY-001
PROCESSED: PAY-003
PROCESSED: PAY-002
PROCESSED: PAY-004
BATCH_END: total Rs.9945.00
[audit] Add rejected — unmodifiable view
Performance Considerations
| Method | Time Complexity | Notes |
|---|---|---|
| sort(list) | O(n log n) | TimSort — stable, optimised for partially sorted data |
| binarySearch(list, key) | O(log n) ArrayList, O(n) LinkedList | List must be sorted; LinkedList has no random access |
| reverse(list) | O(n) | In-place pointer/index swap |
| shuffle(list) | O(n) | Fisher-Yates algorithm |
| rotate(list) | O(n) | In-place rotation |
| fill(list, e) | O(n) | Sets every element |
| copy(dest, src) | O(n) | dest.size() must be >= src.size() |
| min(coll) / max(coll) | O(n) | Linear scan — no sorting |
| frequency(coll, e) | O(n) | Linear scan |
| disjoint(c1, c2) | O(n) avg | Uses contains() on the smaller collection |
| unmodifiable*() | O(1) | Wrapper creation — no copy |
| synchronized*() | O(1) | Wrapper creation — no copy |
| emptyList/Set/Map() | O(1) | Returns cached singleton instances |
| singletonList() | O(1) | Thin wrapper |
| nCopies(n, e) | O(1) creation | Stores element once, returns on every get(i) |
binarySearch() on LinkedList is O(n), not O(log n). The binarySearch implementation checks RandomAccess — ArrayList has it, LinkedList does not. Without random access, binary search degrades to a sequential scan. Always use binarySearch on ArrayList or another RandomAccess list.
unmodifiable*() creates a view, not a copy. The creation cost is O(1) — no elements are copied. But because it is a view, changes to the backing collection are visible through the wrapper. For a true defensive copy, use List.copyOf(list) (Java 10+) or new ArrayList<>(list).
Best Practices
Prefer List.of(), Set.of(), and Map.of() over Collections.unmodifiable*() for truly immutable collections in new code. List.of("A","B","C") produces a genuinely immutable list with no backing mutable collection — the view-through-backing-mutation problem does not exist. Collections.unmodifiableList() is useful when wrapping an existing mutable list to prevent callers from modifying it while the original owner can still update it.
Sort before calling binarySearch() — and use the same comparator for both. binarySearch(list, key, comparator) requires the list to be sorted with the same comparator. Sorting with Comparator.comparing(X::getId) then calling binarySearch(list, probe) (natural order) produces undefined results. If the list is sorted by a custom comparator, use binarySearch(list, probe, sameComparator).
Use Collections.unmodifiableList() at API boundaries to protect internal state. When a service method returns an internal list, wrap it: return Collections.unmodifiableList(internalList). Callers cannot mutate the list, but the service still controls the backing data. This is the standard pattern for exposing internal collections without defensive copying.
Use Collections.emptyList(), emptySet(), emptyMap() instead of new ArrayList<>() for empty return values. They return cached singleton instances — no allocation. They are also explicitly immutable, signalling to callers that the empty result should not be modified. new ArrayList<>() returns a mutable empty list — calling code might attempt to add to it, which is almost always wrong for an empty-result return value.
Common Mistakes
Mistake 1 — Calling binarySearch() on an Unsorted List
1List<Integer> scores = new ArrayList<>(List.of(72, 45, 91, 38, 67));
2
3// WRONG — list is not sorted; result is undefined (may return wrong index)
4int idx = Collections.binarySearch(scores, 67); // undefined behaviour!
5System.out.println(idx); // might return wrong result or -1
6
7// CORRECT — sort first, then search
8Collections.sort(scores);
9int correctIdx = Collections.binarySearch(scores, 67);
10System.out.println(correctIdx); // reliable resultMistake 2 — Assuming unmodifiable*() Is an Immutable Copy
1List<String> original = new ArrayList<>(List.of("Java", "Spring"));
2List<String> wrapper = Collections.unmodifiableList(original);
3
4System.out.println(wrapper); // [Java, Spring]
5
6original.add("Kafka"); // modifies backing list — NOT blocked by wrapper
7
8System.out.println(wrapper); // [Java, Spring, Kafka] — change is visible!
9
10// CORRECT — for a true immutable snapshot, use List.copyOf() (Java 10+)
11List<String> snapshot = List.copyOf(original); // deep immutable copy
12original.add("Docker");
13System.out.println(snapshot); // still [Java, Spring, Kafka] — isolatedMistake 3 — Iterating synchronizedList Without External Lock
1List<String> syncList = Collections.synchronizedList(new ArrayList<>());
2syncList.add("entry1"); syncList.add("entry2"); syncList.add("entry3");
3
4// WRONG — iteration is NOT thread-safe even on synchronizedList
5// Another thread can modify the list between iterations → CME
6for (String entry : syncList) {
7 System.out.println(entry); // potential ConcurrentModificationException
8}
9
10// CORRECT — explicit synchronized block around the iteration
11synchronized (syncList) {
12 for (String entry : syncList) {
13 System.out.println(entry); // safe — no other thread can modify during iteration
14 }
15}
16
17// FOR NEW CODE: prefer CopyOnWriteArrayList (no external sync needed)
18// or ConcurrentHashMap for mapsMistake 4 — Using Collections.copy() Without Pre-Sizing the Destination
1List<String> source = List.of("Alpha", "Beta", "Gamma");
2
3// WRONG — empty ArrayList has size 0; copy() requires dest.size() >= src.size()
4List<String> dest = new ArrayList<>();
5// Collections.copy(dest, source); // throws IndexOutOfBoundsException
6
7// CORRECT — destination must be pre-sized
8List<String> dest2 = new ArrayList<>(Collections.nCopies(source.size(), null));
9Collections.copy(dest2, source);
10System.out.println(dest2); // [Alpha, Beta, Gamma]Output:
[Alpha, Beta, Gamma]
Interview Questions
Q1. What is java.util.Collections and what are its main categories of methods?
java.util.Collections is a final utility class with only static methods that operate on existing Collection and Map objects. It cannot be instantiated. Its six method categories are: sort/search (sort(), binarySearch()); reordering (reverse(), shuffle(), rotate(), swap(), fill(), copy()); extremes and statistics (min(), max(), frequency(), disjoint()); unmodifiable wrappers (unmodifiableList(), etc.); synchronised wrappers (synchronizedList(), etc.); and factory methods for fixed and empty collections (emptyList(), singletonList(), nCopies()).
Q2. What is the difference between Collections.unmodifiableList() and List.of() in Java?
Collections.unmodifiableList(list) creates a read-only wrapper over an existing list — mutation attempts on the wrapper throw UnsupportedOperationException, but changes to the backing list are still reflected through the wrapper. It is a view, not a copy. List.of(elements) creates a truly immutable list with no backing mutable structure — it cannot be changed by any reference. For new code, List.of() is cleaner and safer. Collections.unmodifiableList() is appropriate when the owner of the backing list needs to continue updating it while callers must not.
Q3. What is the contract for using Collections.binarySearch() correctly?
The list must be sorted in ascending natural order before calling binarySearch(list, key). If a custom Comparator was used for sorting, the same Comparator must be passed: binarySearch(list, key, comparator). The method returns the index of the key if found, or -(insertionPoint) - 1 if not found, where insertionPoint is the index at which the key would be inserted to maintain sorted order. Calling binarySearch() on an unsorted list produces undefined results. Additionally, binarySearch() on LinkedList degrades to O(n) because it lacks the RandomAccess interface — use ArrayList.
Q4. How does Collections.synchronizedList() differ from CopyOnWriteArrayList?
Collections.synchronizedList() wraps any list with a global synchronized lock on every method call — reads and writes all serialise on the same lock. Iteration still requires external synchronisation: synchronized(list) { for (T t : list) {...} }. CopyOnWriteArrayList creates a complete array copy on every write, allowing lock-free reads and never throwing ConcurrentModificationException during iteration — no external synchronisation needed. Use synchronizedList for write-heavy lists where O(n) copy per write would be too costly. Use CopyOnWriteArrayList for read-heavy, write-rare lists (listener registries, config).
Q5. What does Collections.frequency() do and when would you use it over a stream?
Collections.frequency(collection, element) counts the number of elements in the collection equal to the given element, using equals(). It is O(n) and works on any Collection. It is a one-liner alternative to list.stream().filter(e -> e.equals(target)).count(). Use frequency() for simple one-off counts in procedural code where a full stream pipeline would be verbose. Use the stream approach when filtering, grouping, or chaining additional operations — the stream's full power is not needed for a plain count.
Q6. What does Collections.disjoint() return and when is it useful?
Collections.disjoint(c1, c2) returns true if the two collections have no elements in common — their intersection is empty. It uses contains() on the smaller collection for each element of the larger. It is used for permission checks (does this user's role set intersect the required permissions set?), conflict detection (do two booking time slots share any time IDs?), and feature flag validation (does the enabled feature set conflict with the disabled set?). It is simpler than a stream-based solution: c1.stream().noneMatch(c2::contains) is equivalent but more verbose.
FAQs
What is the difference between Collections.sort() and List.sort() in Java?
Collections.sort(list) and list.sort(null) are functionally identical since Java 8 — Collections.sort() now delegates to list.sort(). Both use TimSort, a stable O(n log n) algorithm. list.sort(comparator) is the modern preferred form introduced in Java 8. Collections.sort(list, comparator) is the older equivalent. Prefer list.sort() for clarity; use Collections.sort() when calling utility methods alongside other Collections.* operations in the same block.
What does the negative return value from binarySearch() mean?
When the key is not found, binarySearch() returns -(insertionPoint) - 1. The insertionPoint is the index at which the key would need to be inserted to maintain sorted order. For example, if the list is [10, 20, 30, 40] and you search for 25, the result is -(2) - 1 = -3 — 25 would be inserted at index 2. To recover the insertion point: int insertAt = -(result) - 1. This is used for sorted insertions and range queries.
Is Collections.nCopies() memory-efficient?
Yes. nCopies(n, element) stores the element exactly once and returns it from every get(i) call through an internal fixed-size list implementation. It does not allocate an array of n references — it stores only the single element and the count. This makes it extremely memory-efficient for initialising a destination list before Collections.copy() or for producing a large list of identical values without materialising it.
Can I use Collections.sort() on a List of objects that do not implement Comparable?
No. Collections.sort(list) without a Comparator casts elements to Comparable and calls compareTo(). If the element type does not implement Comparable, it throws ClassCastException at runtime — not a compile error. Always use Collections.sort(list, comparator) when the element type does not have a natural ordering.
When should I prefer Collections.min/max() over Stream.min/max()?
Use Collections.min() / max() for quick, readable one-liners in procedural code on any Collection. Collections.max(batch, Comparator.comparingDouble(Payment::amount)) is concise and does not require stream creation. Use stream().min() / max() when the result is part of a larger pipeline — filtering, mapping, or flatMapping before finding the extreme.
Does Collections.shuffle() work on all List types?
Yes — Collections.shuffle(list) works on any List implementation. It uses an in-place Fisher-Yates algorithm, iterating the list from last to first and swapping each element with a random earlier position. For ArrayList, this is O(n) with O(1) swaps since random index access is O(1). For LinkedList, each swap requires O(n) traversal, making the full shuffle O(n²) — use shuffle() on LinkedList only for small lists or convert to ArrayList first.
Summary
java.util.Collections is a static utility library for six problem areas: sorting and binary search, in-place reordering, extremes and set statistics, unmodifiable views, synchronised wrappers, and factory collections. Every method operates on existing collections — nothing is stored, nothing is instantiated.
The two most important behavioural distinctions: unmodifiableList() creates a read-only view of a mutable backing list — it is not an immutable copy, and changes to the backing list are visible through the wrapper (use List.copyOf() for a true snapshot). And synchronizedList() acquires a global lock on every method — iteration still requires an explicit synchronized block — for new concurrent code, CopyOnWriteArrayList or ConcurrentHashMap are better choices.
For interviews: know the difference between unmodifiableList() and List.of(), explain why binarySearch() requires a sorted list and returns a negative value for absent keys, describe the iteration-synchronisation requirement for synchronizedList(), and explain when emptyList() / singletonList() are preferable to new ArrayList<>().
What to Read Next
| Topic | Link |
|---|---|
| How ArrayList serves as the primary List that Collections utility methods operate on | Java ArrayList → |
| How TreeMap and TreeSet use natural ordering that powers Collections.sort and binarySearch | Java TreeMap → |
| How the Collections Framework organises every collection interface and implementation | Java Collections Framework → |
| How Java Streams provide a modern alternative to many Collections utility operations | Java Streams API → |
| How Generics make Collections utility methods type-safe with bounded wildcards | Java Generics → |