Java Tutorial
🔍

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

Java
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

Java
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

Java
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.

Java
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}
Java
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

MethodTime ComplexityNotes
sort(list)O(n log n)TimSort — stable, optimised for partially sorted data
binarySearch(list, key)O(log n) ArrayList, O(n) LinkedListList 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) avgUses 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) creationStores element once, returns on every get(i)

binarySearch() on LinkedList is O(n), not O(log n). The binarySearch implementation checks RandomAccessArrayList 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

Java
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 result

Mistake 2 — Assuming unmodifiable*() Is an Immutable Copy

Java
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] — isolated

Mistake 3 — Iterating synchronizedList Without External Lock

Java
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 maps

Mistake 4 — Using Collections.copy() Without Pre-Sizing the Destination

Java
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

TopicLink
How ArrayList serves as the primary List that Collections utility methods operate onJava ArrayList →
How TreeMap and TreeSet use natural ordering that powers Collections.sort and binarySearchJava TreeMap →
How the Collections Framework organises every collection interface and implementationJava Collections Framework →
How Java Streams provide a modern alternative to many Collections utility operationsJava Streams API →
How Generics make Collections utility methods type-safe with bounded wildcardsJava Generics →
Java Collections Utility Class | DevStackFlow