Java Collections Framework: From Beginner to Advanced ☕
A complete guide to Lists, Sets, Maps, Queues, internal working, sorting, and more
When we start learning Java, we usually begin with variables, arrays, classes, and objects. But as we start building real applications, we encounter a common problem: How do we store and manage a large amount of data efficiently?
Imagine we are developing a banking application. We need to store customer details, maintain transaction histories, prevent duplicate transaction IDs, map account IDs to account details, and process transactions in a particular order.
We could use arrays for some of these tasks. But arrays have limitations, and managing all this data manually would make our code complicated.
Java provides a solution called the Java Collections Framework.
In this blog, we will start with the basics and gradually move towards advanced concepts, including how collections work internally and how to choose the right collection for real-world applications.
1. What is a Collection in Java?
A collection is an object that groups multiple objects into a single unit.
For example, instead of creating separate variables for every student:
String student1 = "Rahul";
String student2 = "Priya";
String student3 = "Aman";We can store them together:
ArrayList<String> students = new ArrayList<>();students.add("Rahul");
students.add("Priya");
students.add("Aman");System.out.println(students);
Output:
[Rahul, Priya, Aman]Now we can add, remove, search, and process students using a single collection.
Why do we need collections?
Arrays are useful, but they have some limitations:
- Their length is fixed after creation.
- Inserting and removing elements can require manual shifting.
- They do not directly provide higher-level operations such as removing duplicates or maintaining sorted data.
- We may need to write additional code for searching, sorting, and managing elements.
Collections provide reusable data structures and algorithms that make these operations easier.
Important: Collections generally store objects, not primitive values directly. Java’s wrapper classes, such as Integer, Double, and Character, allow primitive values to be represented as objects.
ArrayList<Integer> numbers = new ArrayList<>();numbers.add(10); // int is automatically boxed into Integer
numbers.add(20);This automatic conversion is called autoboxing.
2. What is the Java Collections Framework?
The Java Collections Framework is a unified architecture for storing, organizing, accessing, and manipulating groups of objects.
It consists of three major parts:
- Interfaces: Define what operations a data structure supports since we cannot directly create the object of the interface we can use a class that implements the interface.
- Implementations: Provide the actual data structures.
- Algorithms: Provide reusable operations such as sorting and searching.
For example:
List<String> names = new ArrayList<>();Here:
Listis an interface.ArrayListis an implementation of that interface.- Methods such as
Collections.sort()provide reusable algorithms.
Why is this useful?
We can replace ArrayList with another suitable List implementation without changing all the code that uses the List interface.
This is an example of programming to an interface rather than depending unnecessarily on a particular implementation.
3. Understanding the Collections Framework hierarchy
Before learning the individual collection types, we need to understand their hierarchy.
Iterable
|
Collection
/ | \
List Set Queue
| | |
ArrayList | Deque
LinkedList | |
Vector | ArrayDeque
| |
Stack |
HashSet
LinkedHashSet
TreeSetThere is another important hierarchy for key-value storage:
Map
|
---------------------------
| | |
HashMap TreeMap Hashtable
|
LinkedHashMapOther important implementation:
ConcurrentHashMapThis is a simplified view, not the complete inheritance tree.
One important interview point: Map is part of the Collections Framework, but it does not extend the Collection interface. A List, Set, and Queue are collections; a Map stores key-value mappings.
4. The Collection interface
The Collection interface is the root interface for the main collection types.
It defines common operations that many collections support.
MethodPurposeadd()Adds an elementaddAll()Adds elements from another collectionremove()Removes an elementcontains()Checks whether an element existssize()Returns the number of elementsisEmpty()Checks whether the collection is emptyclear()Removes all elementsiterator()Returns an iteratortoArray()Converts the collection into an array
Example:
Collection<String> names = new ArrayList<>();names.add("Rahul");
names.add("Priya");
names.add("Aman");System.out.println(names.size()); // 3
System.out.println(names.contains("Priya")); // truenames.remove("Rahul");System.out.println(names);
Output:
3
true
[Priya, Aman]The exact behavior of some operations depends on the implementation. For example, some collections permit duplicates, while others do not.
Now let’s explore the most commonly used collection types.
Part 1: List
5. What is a List?
A List is an ordered collection that allows elements to be accessed using their index.
A List generally allows duplicate elements.
For example:
List<String> names = new ArrayList<>();names.add("Rahul");
names.add("Priya");
names.add("Rahul");System.out.println(names);
Output:
[Rahul, Priya, Rahul]The duplicate "Rahul" is allowed.
A List also maintains positional order. The first element is at index 0, the second at index 1, and so on.
System.out.println(names.get(0)); // Rahul
System.out.println(names.get(1)); // PriyaImportant List methods
names.add("Aman");
names.add(1, "Sneha"); // Insert at index 1names.set(0, "Arjun"); // Replace element at index 0names.remove(2); // Remove element at index 2System.out.println(names.get(0));
Be careful when using remove() with an ArrayList<Integer>:
List<Integer> numbers = new ArrayList<>();numbers.add(10);
numbers.add(20);
numbers.add(30);numbers.remove(1); // Removes the element at index 1: 20
numbers.remove(Integer.valueOf(30)); // Removes the value 30
This distinction exists because List provides both remove(int index) and remove(Object value).
6. ArrayList
ArrayList is one of the most commonly used collection implementations in Java.
Internally, it uses a resizable array.
Unlike a regular array, an ArrayList can grow as elements are added.
ArrayList<Integer> numbers = new ArrayList<>();numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.add(40);System.out.println(numbers);
How does ArrayList work internally?
Imagine the internal array has a capacity of four elements.
Index: 0 1 2 3
-------------------
Values: | 10 | 20 | 30 | 40 |
-------------------When we add another element and the backing array has no free capacity, Java creates a larger array and copies the existing elements into it.
The exact growth policy is an implementation detail, so we should not rely on a particular capacity increase as part of application logic.
Time complexity of ArrayList
OperationTypical complexityAccess by indexO(1)Add at the endAmortized O(1)Insert in the middleO(n)Remove from the middleO(n)Search by valueO(n)
Why is insertion in the middle O(n)?
Because elements after the insertion position may need to be shifted to make space.
When should we use ArrayList?
Use ArrayList when:
- You frequently access elements by index.
- You need a resizable sequence.
- You frequently add elements at the end.
- You do not need efficient insertion and removal at arbitrary positions.
For most ordinary list use cases, ArrayList is a good default choice.
7. LinkedList
LinkedList uses a doubly linked list internally.
Each node stores an element and references to the previous and next nodes.
Conceptually:
null <- [10] <-> [20] <-> [30] -> nullEach node is connected to its neighbors.
LinkedList<Integer> numbers = new LinkedList<>();numbers.add(10);
numbers.add(20);
numbers.add(30);numbers.addFirst(5);
numbers.addLast(40);System.out.println(numbers);
Output:
[5, 10, 20, 30, 40]When should we use LinkedList?
It can be useful when operations at both ends are important, but ArrayDeque is often a better choice for queue and stack behavior.
For ordinary indexed lists, ArrayList is usually the preferred starting point.
8. Vector and Stack
Before the modern collection implementations became common, Java provided classes such as Vector and Stack.
Vector
Vector is a resizable array similar to ArrayList, but its individual methods are synchronized.
Vector<Integer> numbers = new Vector<>();numbers.add(10);
numbers.add(20);It is a legacy class. Synchronizing individual methods does not automatically make a multi-step operation atomic.
For most modern applications, ArrayList is preferred when synchronization is not needed.
Stack
Stack extends Vector and provides last-in, first-out behavior.
Stack<Integer> stack = new Stack<>();stack.push(10);
stack.push(20);
stack.push(30);System.out.println(stack.pop()); // 30
System.out.println(stack.peek()); // 20
The last element inserted is the first one removed.
For modern Java code, ArrayDeque is generally preferred for stack operations.
Part 2: Set
9. What is a Set?
A Set is a collection that does not allow duplicate elements.
Imagine a system that stores unique customer IDs. If the same ID is added twice, the Set will retain only one equal element.
Set<Integer> ids = new HashSet<>();ids.add(101);
ids.add(102);
ids.add(101);System.out.println(ids);
Output contains 101 and 102, but only one occurrence of each.
The exact iteration order of a HashSet is not guaranteed.
A Set is useful for:
- Removing duplicate values.
- Tracking unique IDs.
- Checking membership.
- Performing mathematical set operations.
10. HashSet
HashSet is backed by a hash table, implemented using a HashMap in standard OpenJDK implementations.
It uses an element’s hashCode() to help determine where to store the element and equals() to determine whether elements are equal.
Set<String> languages = new HashSet<>();languages.add("Java");
languages.add("Python");
languages.add("Java");System.out.println(languages.contains("Java")); // true
System.out.println(languages.size()); // 2
How does HashSet work internally?
When we add an element:
- Java obtains its hash code.
- The hash helps determine a bucket in the underlying hash table.
- Java checks for an equal element in the relevant bucket.
- If an equal element already exists, the Set does not add a duplicate.
- Otherwise, the new element is stored.
Hash collisions can occur when different objects have the same hash code. A correct implementation must still distinguish unequal objects using equality checks.
Under normal hashing conditions, HashSet offers average constant-time basic operations. Poor hash distribution can affect performance.
11. LinkedHashSet
LinkedHashSet prevents duplicates like HashSet, but also preserves insertion order during iteration.
Set<String> names = new LinkedHashSet<>();names.add("Rahul");
names.add("Priya");
names.add("Aman");
names.add("Rahul");System.out.println(names);
Output:
[Rahul, Priya, Aman]Use LinkedHashSet when you want unique elements while retaining the order in which they were inserted.
12. TreeSet
TreeSet stores unique elements in sorted order.
Set<Integer> numbers = new TreeSet<>();numbers.add(50);
numbers.add(10);
numbers.add(30);
numbers.add(20);System.out.println(numbers);
Output:
[10, 20, 30, 50]Internally, TreeSet is backed by a TreeMap in standard OpenJDK implementations, which uses a balanced search tree.
Basic operations such as insertion, removal, and lookup typically take O(log n).
Important: TreeSet normally uses natural ordering or a supplied Comparator. Its notion of equality for maintaining uniqueness is based on comparison returning zero. This should be consistent with equals() to avoid surprising behavior.
Part 3: Map
13. What is a Map?
A Map stores data in key-value pairs.
For example, imagine storing employee IDs and employee names.
101 -> Rahul
102 -> Priya
103 -> AmanInstead of searching through a list to find an employee’s name, we can retrieve the value using the employee ID.
Map<Integer, String> employees = new HashMap<>();employees.put(101, "Rahul");
employees.put(102, "Priya");
employees.put(103, "Aman");System.out.println(employees.get(102));
Output:
PriyaA Map does not permit duplicate keys. If we insert a value using an existing key, the previous value is replaced.
employees.put(101, "Arjun");System.out.println(employees.get(101)); // ArjunA Map can contain duplicate values, but each key can map to at most one value.
14. HashMap
HashMap is one of the most commonly used Map implementations.
It stores key-value pairs using hashing.
Map<String, Integer> marks = new HashMap<>();marks.put("Rahul", 85);
marks.put("Priya", 92);
marks.put("Aman", 78);System.out.println(marks.get("Priya")); // 92
How does HashMap work internally?
Let’s understand the basic process.
When we execute:
marks.put("Rahul", 85);Java uses the key’s hash code to help determine a bucket in the internal hash table.
The entry stores the key and value, along with the information needed to manage the bucket.
When we execute:
marks.get("Rahul");Java calculates the key’s hash again, identifies the appropriate bucket, and checks keys for a match.
If multiple keys land in the same bucket, Java handles the collision. In modern OpenJDK implementations, heavily populated buckets can be converted into tree structures under specific conditions, improving worst-case lookup behavior in those buckets.
The exact thresholds and internal details are implementation-specific.
Important HashMap characteristics
- It does not guarantee iteration order.
- It allows one null key and multiple null values.
- It is not thread-safe for concurrent structural modifications.
- Basic lookup and insertion are generally O(1) on average with suitable hashing.
- It uses
hashCode()andequals()to manage key identity and equality.
Why are equals() and hashCode() important?
Suppose we create a custom Employee class and use its objects as keys in a HashMap.
If two employees should be considered equal, we must implement equals() and hashCode() consistently.
The general contract is:
- Equal objects must have equal hash codes.
- Unequal objects may have the same hash code.
equals()determines whether two keys are equal.
If a key’s fields used for hashing or equality change after it has been inserted into a map, retrieval may fail because the object may no longer be found in the bucket where it was originally stored.
Prefer immutable keys or avoid modifying equality-relevant state while an object is being used as a key.
15. LinkedHashMap
LinkedHashMap combines hash-table lookup with a linked structure that maintains iteration order.
By default, it preserves insertion order.
Map<Integer, String> students = new LinkedHashMap<>();students.put(3, "Aman");
students.put(1, "Rahul");
students.put(2, "Priya");System.out.println(students);
Iteration follows insertion order:
{3=Aman, 1=Rahul, 2=Priya}It can also be configured to maintain access order, which is useful for implementing certain cache strategies, including least-recently-used cache designs.
16. TreeMap
TreeMap stores entries sorted by their keys.
Map<Integer, String> students = new TreeMap<>();students.put(3, "Aman");
students.put(1, "Rahul");
students.put(2, "Priya");System.out.println(students);
Output:
{1=Rahul, 2=Priya, 3=Aman}Internally, TreeMap uses a red-black tree, a self-balancing binary search tree.
Its basic lookup, insertion, and removal operations typically take O(log n).
Use it when you need keys to remain sorted or need navigation operations such as finding the nearest key.
17. Hashtable
Hashtable is a legacy Map implementation.
Its methods are synchronized, and it does not allow null keys or null values.
Hashtable<Integer, String> users = new Hashtable<>();users.put(1, "Rahul");
users.put(2, "Priya");For most modern applications:
- Use
HashMapwhen external synchronization is not required. - Use
ConcurrentHashMapwhen concurrent access and updates are needed.
Hashtable is mainly important for understanding legacy Java code.
Part 4: Queue and Deque
19. What is a Queue?
A Queue is designed to hold elements before they are processed.
Get Aishu Reddy’s stories in your inbox
Join Medium for free to get updates from this writer.
A traditional queue follows FIFO: First In, First Out.
Think of a line at a ticket counter. The first person to join the line is normally the first person served.
Java’s Queue interface provides methods for inserting, retrieving, and removing elements.
MethodBehavioroffer(e)Attempts to add an elementpoll()Removes and returns the head, or returns null if emptypeek()Returns the head without removing it, or null if emptyremove()Removes the head, throwing an exception if emptyelement()Returns the head, throwing an exception if empty
For queue insertion, offer() is often preferred over add() because some queue implementations can reject an element without throwing an exception.
20. PriorityQueue
A PriorityQueue processes elements according to priority rather than insertion order.
By default, Java orders elements according to their natural ordering, so the smallest element is at the head for numbers.
Queue<Integer> numbers = new PriorityQueue<>();numbers.offer(40);
numbers.offer(10);
numbers.offer(30);
numbers.offer(20);while (!numbers.isEmpty()) {
System.out.println(numbers.poll());
}
Output:
10
20
30
40Internally, PriorityQueue uses a heap.
Adding or removing an element generally takes O(log n), while inspecting the head takes O(1).
Important: Iterating directly over a PriorityQueue does not guarantee that all elements will appear in sorted order. Repeatedly removing the head retrieves elements in priority order.
21. Deque and ArrayDeque
Deque stands for double-ended queue.
It allows insertion and removal at both ends.
Deque<Integer> deque = new ArrayDeque<>();deque.addFirst(20);
deque.addFirst(10);
deque.addLast(30);System.out.println(deque);
Output:
[10, 20, 30]ArrayDeque uses a resizable array internally and is commonly used for both queue and stack behavior.
Using ArrayDeque as a stack
Deque<Integer> stack = new ArrayDeque<>();stack.push(10);
stack.push(20);
stack.push(30);System.out.println(stack.pop()); // 30
System.out.println(stack.peek()); // 20
Using ArrayDeque as a queue
Queue<String> queue = new ArrayDeque<>();queue.offer("First");
queue.offer("Second");
queue.offer("Third");System.out.println(queue.poll()); // First
ArrayDeque does not permit null elements.
It is generally preferred over the legacy Stack class for stack operations and is often an excellent choice for single-threaded queue operations.
Part 5: Iteration
22. What is an Iterator?
An iterator provides a standard way to traverse elements in a collection without exposing its internal data structure.
List<String> names = new ArrayList<>();names.add("Rahul");
names.add("Priya");
names.add("Aman");Iterator<String> iterator = names.iterator();while (iterator.hasNext()) {
System.out.println(iterator.next());
}
Output:
Rahul
Priya
AmanImportant methods:
hasNext()checks whether another element exists.next()returns the next element.remove()removes the last element returned bynext(), when supported by the iterator.
An iterator is useful when we want to traverse a collection without depending on its implementation.
23. Enhanced for loop
Java also provides the enhanced for loop.
List<String> names = new ArrayList<>();names.add("Rahul");
names.add("Priya");
names.add("Aman");for (String name : names) {
System.out.println(name);
}
This is simpler when we only need to read elements.
However, we should not structurally modify an ordinary collection directly inside an enhanced for loop. Doing so may cause a ConcurrentModificationException.
If we need to remove elements while traversing, use an iterator’s supported remove() method or another appropriate operation.
24. List Iterator
ListIterator is specifically designed for Lists.
Unlike an ordinary Iterator, it can traverse in both directions and supports additional operations.
List<String> names = new ArrayList<>();names.add("Rahul");
names.add("Priya");
names.add("Aman");ListIterator<String> iterator = names.listIterator();while (iterator.hasNext()) {
System.out.println(iterator.next());
}while (iterator.hasPrevious()) {
System.out.println(iterator.previous());
}
It also supports operations such as add(), set(), and remove() where permitted.
Use ListIterator when you need more control over traversal and modification of a list.
25. Fail-fast iterators
Many standard collection iterators are described as fail-fast.
If a collection is structurally modified outside the iterator while iteration is in progress, the iterator may throw ConcurrentModificationException.
List<Integer> numbers = new ArrayList<>();numbers.add(10);
numbers.add(20);
numbers.add(30);for (Integer number : numbers) {
if (number == 20) {
numbers.remove(number);
}
}
This code can throw ConcurrentModificationException.
A safer approach is:
Iterator<Integer> iterator = numbers.iterator();while (iterator.hasNext()) {
Integer number = iterator.next(); if (number == 20) {
iterator.remove();
}
}
Fail-fast behavior is best-effort bug detection, not a synchronization guarantee. It should not be used as a way to coordinate multiple threads.
Part 6: Sorting
26. Comparable
Suppose we have a list of employees. We want to sort employees according to their IDs.
How does Java know which employee should come first?
We can implement Comparable to define the class's natural ordering.
class Employee implements Comparable<Employee> {int id;
String name; Employee(int id, String name) {
this.id = id;
this.name = name;
} @Override
public int compareTo(Employee other) {
return Integer.compare(this.id, other.id);
} @Override
public String toString() {
return id + " - " + name;
}
}
Now we can sort a list:
List<Employee> employees = new ArrayList<>();employees.add(new Employee(103, "Aman"));
employees.add(new Employee(101, "Rahul"));
employees.add(new Employee(102, "Priya"));Collections.sort(employees);System.out.println(employees);
The employees are sorted by ID.
compareTo() should return a negative number if the current object comes before the other object, zero if they compare equally, or a positive number if it comes after.
Use Integer.compare() rather than subtracting IDs, which can overflow.
27. Comparator
What if we want to sort the same employees by name instead of ID?
We can use Comparator to define an alternative ordering without changing the Employee class.
employees.sort(
Comparator.comparing(employee -> employee.name)
);Or, using a method reference:
employees.sort(Comparator.comparing(Employee::getName));The second example assumes Employee has a getName() method.
We can sort by multiple fields:
employees.sort(
Comparator.comparing(Employee::getName)
.thenComparingInt(Employee::getId)
);This sorts by name first, then by ID when names compare equally.
Comparable vs. Comparator
ComparableComparatorDefines natural ordering in the classDefines an ordering externallyImplements compareTo()Implements compare() or uses factory methodsUsually represents the default orderingSupports multiple alternative orderingsCollections.sort(list) can use itlist.sort(comparator) can use it
Be careful when writing custom comparators: they should follow a consistent ordering contract.
Part 7: Collections utility class
28. Collections vs. Collection
These names look almost identical, but they mean different things.
Collectionis an interface.Collectionsis a utility class containing static methods that operate on collections.
For example:
List<Integer> numbers =
new ArrayList<>(Arrays.asList(40, 10, 30, 20));Collections.sort(numbers);
System.out.println(numbers);Collections.reverse(numbers);
System.out.println(numbers);Collections.shuffle(numbers);
System.out.println(numbers);
The final output after shuffling is not predictable.
Other useful methods include:
Collections.min()Collections.max()Collections.frequency()Collections.binarySearch()Collections.unmodifiableList()Collections.synchronizedList()
Binary search
Binary search works on a sorted list.
List<Integer> numbers =
new ArrayList<>(Arrays.asList(10, 20, 30, 40, 50));int index = Collections.binarySearch(numbers, 30);System.out.println(index); // 2
If the value is not found, the method returns a negative value encoding an insertion point.
Do not use binary search on an unsorted list and expect correct results.
Part 8: Advanced concepts
29. Generics in collections
Generics allow us to specify the type of elements a collection is intended to hold.
List<String> names = new ArrayList<>();This tells the compiler that the list holds Strings.
The following is invalid:
names.add(100);The compiler catches the type mismatch before execution.
Without generics, older code might use raw collections:
List names = new ArrayList();names.add("Rahul");
names.add(100);Now the collection can contain unrelated types, and casting values back to their expected types can cause runtime errors.
Prefer parameterized collections:
List<String> names = new ArrayList<>();Generics improve type safety, readability, and maintainability.
30. Wildcards in collections
Java generics also support wildcards.
Unbounded wildcard
void printAll(List<?> values) {
for (Object value : values) {
System.out.println(value);
}
}List<?> means a List of some specific but unknown type.
We can read elements as Object, but generally cannot add arbitrary non-null values to the list.
Upper-bounded wildcard
double sum(List<? extends Number> numbers) {
double total = 0;for (Number number : numbers) {
total += number.doubleValue();
} return total;
}
? extends Number allows a list of Integer, Double, or another Number subtype.
This is useful when a method needs to read values from a collection of related types.
Lower-bounded wildcard
void addNumbers(List<? super Integer> numbers) {
numbers.add(10);
numbers.add(20);
}? super Integer accepts a List whose element type is Integer or a supertype such as Number or Object.
A useful rule is PECS:
- Producer Extends: use
? extends Twhen a structure produces values for us to read. - Consumer Super: use
? super Twhen a structure consumes values we add.
This is a guideline, not a replacement for understanding the actual type requirements.
31. Immutable and unmodifiable collections
Sometimes we want to prevent a collection from being modified.
Java provides several approaches.
Unmodifiable view
List<String> names = new ArrayList<>();names.add("Rahul");
names.add("Priya");List<String> view = Collections.unmodifiableList(names);
The view cannot be modified through its own modifying methods.
However, changes to the original names list can still be visible through the view.
Immutable factory methods
Java 9 introduced factory methods such as:
List<String> names = List.of("Rahul", "Priya");
Set<Integer> ids = Set.of(101, 102, 103);
Map<Integer, String> users =
Map.of(1, "Rahul", 2, "Priya");These collections cannot be structurally modified, and the factory methods reject null elements, keys, or values as applicable.
This is useful when we need fixed configuration or constant data.
Remember that an unmodifiable collection does not automatically make its contained objects immutable.
32. The Stream API and collections
Java 8 introduced the Stream API, which allows us to process collection data using operations such as filtering, mapping, and reducing.
For example:
List<Integer> numbers =
Arrays.asList(10, 15, 20, 25, 30);List<Integer> evenNumbers = numbers.stream()
.filter(number -> number % 2 == 0)
.collect(Collectors.toList());System.out.println(evenNumbers);
Output:
[10, 20, 30]Let’s understand the operations:
stream()creates a stream over the collection.filter()keeps elements matching a condition.collect()gathers the results into a collection.
Another example:
int total = numbers.stream()
.mapToInt(Integer::intValue)
.sum();System.out.println(total);Streams do not automatically modify the original collection. They process elements and produce results according to the pipeline operations.
Use streams when they make data-processing logic clearer. For simple operations, a regular loop can be equally readable.
33. Concurrent collections
Most standard collection implementations, including ArrayList and HashMap, are not designed for unsynchronized concurrent modification by multiple threads.
Java provides concurrent collections for multithreaded applications.
ConcurrentHashMap
ConcurrentHashMap supports concurrent access and updates.
Map<String, Integer> counts = new ConcurrentHashMap<>();counts.put("Java", 10);
counts.put("Python", 5);counts.merge("Java", 1, Integer::sum);System.out.println(counts.get("Java")); // 11
The merge() method performs the update using an atomic map operation, making it more appropriate for this example than separately reading and writing the value.
ConcurrentHashMap does not permit null keys or values.
CopyOnWriteArrayList
CopyOnWriteArrayList creates a new backing array when it is modified.
This can be useful when reads and iterations are frequent but updates are relatively rare.
It is not a good choice for every workload because each write can be expensive.
BlockingQueue
A BlockingQueue is useful when one thread produces work and another consumes it.
For example:
Producer thread
|
v
BlockingQueue
|
v
Consumer threadThe queue can block a consumer when it is empty or block a producer when a bounded queue is full.
This is useful in task-processing systems, worker pools, and producer-consumer architectures.
34. Performance and Big-O complexity
Choosing the right collection requires understanding the operations your application performs most frequently.
CollectionAccess or lookupInsertRemoveArrayListIndex: O(1); search: O(n)End: amortized O(1); middle: O(n)Middle: O(n)LinkedListIndex: O(n)At an already-known end or node: O(1)At an already-known end or node: O(1)HashSetAverage O(1)Average O(1)Average O(1)TreeSetO(log n)O(log n)O(log n)HashMapAverage O(1)Average O(1)Average O(1)TreeMapO(log n)O(log n)O(log n)PriorityQueueHead: O(1); arbitrary search: O(n)O(log n)Head removal: O(log n)
These are typical complexity estimates, not guarantees for every implementation or every input. Hash-based collections depend on hashing behavior, while linked structures depend on whether the target position is already known.
35. Common mistakes when using collections
Mistake 1: Assuming HashMap maintains insertion order.
It does not guarantee iteration order. Use LinkedHashMap if insertion order matters.
Mistake 2: Assuming Set preserves insertion order.
HashSet does not guarantee it. Use LinkedHashSet when insertion order is required.
Mistake 3: Modifying a collection directly while iterating.
This can trigger ConcurrentModificationException. Use supported iterator operations or a suitable alternative.
Mistake 4: Forgetting that Map keys are unique.
Adding a value for an existing key replaces the previous mapping.
Mistake 5: Using a mutable object as a HashMap key.
Changing fields used by equals() or hashCode() can make the mapping difficult to retrieve.
Mistake 6: Assuming thread safety from individual operations.
Thread-safe operations do not automatically make a sequence of operations atomic.
Mistake 7: Using the wrong collection for the requirement.
A TreeSet is useful for sorted unique elements; an ArrayList is useful for indexed access; a HashMap is useful for key-based lookup.
36. Which collection should we choose?
Here is a practical decision guide.
RequirementRecommended starting pointResizable list with fast indexed accessArrayListUnique values with no ordering requirementHashSetUnique values in insertion orderLinkedHashSetUnique values in sorted orderTreeSetFast average key-based lookupHashMapMap entries in insertion orderLinkedHashMapMap entries sorted by keyTreeMapFIFO queueArrayDeque or a suitable Queue implementationStack operationsArrayDequePriority-based processingPriorityQueueConcurrent key-value accessConcurrentHashMapProducer-consumer task processingA suitable BlockingQueue
The best collection depends on your actual requirements, including ordering, null handling, concurrency, memory use, and operation frequency.
37. Final revision: What have we learned?
We started with a simple question: how do we manage groups of objects in Java?
We learned that the Collections Framework provides reusable interfaces, data structures, and algorithms.
We covered:
- The Collections Framework and its hierarchy.
- The
Collectioninterface. - Lists:
ArrayList,LinkedList,Vector, andStack. - Sets:
HashSet,LinkedHashSet, andTreeSet. - Maps:
HashMap,LinkedHashMap,TreeMap,Hashtable, andConcurrentHashMap. - Queues:
Queue,PriorityQueue,Deque, andArrayDeque. - Iteration using
Iterator,ListIterator, and enhanced for loops. - Sorting using
ComparableandComparator. - Utility methods from
Collections. - Generics, wildcards, and immutable collections.
- Streams and concurrent collections.
- Internal data structures, time complexity, and common mistakes.
Conclusion
The Java Collections Framework is much more than a group of classes for storing data. It provides the foundation for writing efficient, reusable, and maintainable Java applications.
As you practise, focus on understanding why a particular collection is useful, how it works internally, and what trade-offs it makes. That understanding will help you solve coding problems, answer interview questions, and choose the right data structure in real-world projects.
Happy learning! ☕





