Collections Framework in Java
What is the Collections Framework?
The Java Collections Framework (JCF) is a unified architecture for storing and manipulating groups of objects. It provides a set of interfaces, implementations, and algorithms that make it easy to work with data structures like lists, sets, queues, and maps without having to write them from scratch.
The core hierarchy of the Collections Framework looks like this:
java.lang.Iterable
+-- java.util.Collection
+-- List (ordered, duplicates allowed)
¦ +-- ArrayList
¦ +-- LinkedList
¦ +-- Vector
+-- Set (no duplicates)
¦ +-- HashSet
¦ +-- LinkedHashSet
¦ +-- TreeSet (SortedSet)
+-- Queue (FIFO ordering)
+-- LinkedList
+-- PriorityQueue
+-- Deque
+-- ArrayDeque
+-- LinkedList
java.util.Map (key-value pairs, separate hierarchy)
+-- HashMap
+-- LinkedHashMap
+-- TreeMap (SortedMap)
Interfaces define the contract (what operations
are supported). Implementations are the concrete
classes you instantiate. For example,
List is the interface;
ArrayList and
LinkedList are implementations.
Important: Iterable
is the root of the hierarchy and provides
iterator(). The
Collection interface extends it and
adds common operations such as add,
remove, size,
and contains. A
Map is part of the framework but is
not a subtype of Collection.
Note: Map does not
extend Collection. It is a separate
top-level interface in the framework.
Collection vs Collections
These two are frequently confused but serve very different purposes:
-
java.util.Collection— an interface. It is the root interface of the collection hierarchy. Classes likeArrayList,HashSet, andLinkedListimplement it. -
java.util.Collections— a utility class (all static methods). It provides algorithms like sorting, shuffling, binary search, and synchronization wrappers.
import java.util.*;
public class CollectionVsCollections {
public static void main(String[] args) {
// Collection (interface) — used via implementation
Collection<String> names = new ArrayList<>();
names.add("Alice");
names.add("Charlie");
names.add("Bob");
// Collections (utility class) — static methods
System.out.println("Before sort: " + names);
Collections.sort((List<String>) names);
System.out.println("After sort: " + names);
System.out.println("Max: " + Collections.max(names));
System.out.println("Min: " + Collections.min(names));
Collections.shuffle((List<String>) names);
System.out.println("After shuffle: " + names);
}
}
After sort: [Alice, Bob, Charlie]
Max: Charlie
Min: Alice
After shuffle: [Bob, Charlie, Alice]
List Interface
The List interface represents an
ordered (sequenced) collection that allows duplicate elements.
Elements can be accessed by their integer index (position). Key
characteristics:
- Maintains insertion order
- Allows duplicate elements
- Allows null elements
-
Provides positional access via
get(int index)
ArrayList vs LinkedList Comparison
| Feature | ArrayList | LinkedList |
|---|---|---|
| Internal Structure | Dynamic array | Doubly linked nodes |
| Random Access (get) | O(1) — fast | O(n) — slow |
| Add at end | O(1) amortized | O(1) |
| Insert / Delete in middle | O(n) — shift needed | O(1) once at node |
| Memory | Less overhead | More (node pointers) |
| Implements Deque | No | Yes |
| Best for | Read-heavy workloads | Frequent insert/delete |
ArrayList
ArrayList is backed by a resizable
array. When the array is full, a new array of 1.5× the size is
allocated and all elements are copied. It is the most commonly
used List implementation.
import java.util.*;
public class ArrayListDemo {
public static void main(String[] args) {
ArrayList<String> fruits = new ArrayList<>();
// add elements
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
fruits.add("Date");
System.out.println("List: " + fruits);
// get element by index
System.out.println("Index 1: " + fruits.get(1));
// set (replace) element
fruits.set(2, "Coconut");
System.out.println("After set(2): " + fruits);
// remove by index
fruits.remove(0);
System.out.println("After remove(0): " + fruits);
// remove by value
fruits.remove("Date");
System.out.println("After remove('Date'): " + fruits);
// contains check
System.out.println("Contains Banana? " + fruits.contains("Banana"));
// size
System.out.println("Size: " + fruits.size());
// iterate with enhanced for
System.out.print("Elements: ");
for (String f : fruits) {
System.out.print(f + " ");
}
System.out.println();
// iterate with Iterator
Iterator<String> it = fruits.iterator();
System.out.print("Iterator: ");
while (it.hasNext()) {
System.out.print(it.next() + " ");
}
System.out.println();
}
}
Index 1: Banana
After set(2): [Apple, Banana, Coconut, Date]
After remove(0): [Banana, Coconut, Date]
After remove('Date'): [Banana, Coconut]
Contains Banana? true
Size: 2
Elements: Banana Coconut
Iterator: Banana Coconut
LinkedList
LinkedList implements both
List and
Deque. Each element (node) stores a
reference to the next and previous nodes, making insertions and
deletions at either end O(1).
import java.util.*;
public class LinkedListDemo {
public static void main(String[] args) {
LinkedList<String> ll = new LinkedList<>();
// add elements
ll.add("Middle");
ll.addFirst("First"); // add to head
ll.addLast("Last"); // add to tail
System.out.println("LinkedList: " + ll);
// peek — view without removing
System.out.println("First: " + ll.getFirst());
System.out.println("Last: " + ll.getLast());
// remove from head and tail
ll.removeFirst();
System.out.println("After removeFirst: " + ll);
ll.removeLast();
System.out.println("After removeLast: " + ll);
// use as a stack (LIFO)
LinkedList<Integer> stack = new LinkedList<>();
stack.push(10); // addFirst
stack.push(20);
stack.push(30);
System.out.println("Stack: " + stack);
System.out.println("Pop: " + stack.pop()); // removeFirst
System.out.println("Stack after pop: " + stack);
}
}
First: First
Last: Last
After removeFirst: [Middle, Last]
After removeLast: [Middle]
Stack: [30, 20, 10]
Pop: 30
Stack after pop: [20, 10]
Vector
Vector is a legacy, synchronized,
growable array implementation of List.
Its methods are synchronized, which can add overhead. Prefer
ArrayList for single-threaded code and
use an explicit concurrent collection when thread-safe behavior is
required.
import java.util.Vector;
Vector<String> colors = new Vector<>();
colors.add("Red");
colors.add("Green");
colors.add("Blue");
System.out.println(colors);
System.out.println("First: " + colors.firstElement());
First: Red
Stack
Stack extends
Vector and models last-in, first-out
(LIFO) behavior. Its main operations are
push, pop,
and peek. For new code, prefer
Deque with
ArrayDeque because it has a clearer
stack API and is usually faster.
import java.util.ArrayDeque;
import java.util.Deque;
Deque<String> stack = new ArrayDeque<>();
stack.push("First");
stack.push("Second");
System.out.println("Top: " + stack.peek());
System.out.println("Removed: " + stack.pop());
Removed: Second
When to use LinkedList over ArrayList: When your code frequently inserts or deletes elements at the beginning or middle of the list, LinkedList avoids the expensive array-shifting cost. For most read-heavy scenarios, ArrayList is faster due to better cache locality.
Set Interface
A Set is a collection that contains
no duplicate elements. It models the mathematical
set abstraction. The three main implementations differ in ordering
and performance:
| Feature | HashSet | LinkedHashSet | TreeSet |
|---|---|---|---|
| Order | No guaranteed order | Insertion order | Sorted (natural / Comparator) |
| Null allowed | Yes (one) | Yes (one) | No (throws NPE) |
| add / remove / contains | O(1) | O(1) | O(log n) |
| Backed by | HashMap | LinkedHashMap | TreeMap (Red-Black tree) |
| Best for | Fast membership tests | Dedup + preserve order | Sorted unique elements |
HashSet
HashSet is backed by a
HashMap. Elements are stored as keys
in the map with a dummy value. It offers O(1) average time for
add, remove, and contains. The order of elements is
not predictable.
import java.util.*;
public class HashSetDemo {
public static void main(String[] args) {
HashSet<String> set = new HashSet<>();
// add elements
set.add("Java");
set.add("Python");
set.add("C++");
set.add("Java"); // duplicate — ignored
set.add("Go");
System.out.println("Set: " + set);
System.out.println("Size: " + set.size()); // 4, not 5
// contains check
System.out.println("Contains Python? " + set.contains("Python"));
System.out.println("Contains Rust? " + set.contains("Rust"));
// remove
set.remove("C++");
System.out.println("After remove C++: " + set);
// iteration (order not guaranteed)
System.out.print("Iterating: ");
for (String lang : set) {
System.out.print(lang + " ");
}
System.out.println();
// set operations — union
HashSet<String> other = new HashSet<>(Arrays.asList("Kotlin", "Java", "Swift"));
set.addAll(other); // union
System.out.println("Union: " + set);
// intersection
set.retainAll(other);
System.out.println("Intersection: " + set);
}
}
Size: 4
Contains Python? true
Contains Rust? false
After remove C++: [Java, Go, Python]
Iterating: Java Go Python
Union: [Java, Go, Python, Kotlin, Swift]
Intersection: [Java]
LinkedHashSet
LinkedHashSet combines the uniqueness
of HashSet with predictable insertion
order. It is useful when duplicates must be removed without
changing the order in which values first appeared.
import java.util.LinkedHashSet;
LinkedHashSet<String> languages = new LinkedHashSet<>();
languages.add("Java");
languages.add("Python");
languages.add("Java"); // ignored
languages.add("Go");
System.out.println(languages);
SortedSet and TreeSet
SortedSet extends
Set and describes a set whose
elements have a total ordering. The usual implementation is
TreeSet, which stores unique values in
ascending natural order or in the order supplied by a
Comparator. Basic lookup and update
operations take O(log n) time.
import java.util.SortedSet;
import java.util.TreeSet;
SortedSet<String> names = new TreeSet<>();
names.add("Rahul");
names.add("Priya");
names.add("Amit");
System.out.println(names);
System.out.println("First: " + names.first());
System.out.println("Range: " + names.headSet("Rahul"));
First: Amit
Range: [Amit, Priya]
Queue and Deque
A Queue is designed for holding
elements prior to processing. Most queues order elements in FIFO
(first-in, first-out) order. Key methods follow two styles:
throwing exceptions (add/remove/element) or returning special values (offer/poll/peek).
Deque (double-ended queue) supports
element insertion and removal at both ends.
ArrayDeque is the recommended
implementation for both stack and queue use cases — faster than
LinkedList for most operations.
import java.util.*;
public class QueueDequeDemo {
public static void main(String[] args) {
// Queue via LinkedList (FIFO)
Queue<String> queue = new LinkedList<>();
queue.offer("Task-1");
queue.offer("Task-2");
queue.offer("Task-3");
System.out.println("Queue: " + queue);
System.out.println("Peek (head): " + queue.peek()); // view, no remove
System.out.println("Poll (remove): " + queue.poll()); // remove head
System.out.println("Queue after poll: " + queue);
System.out.println();
// Deque as Queue (FIFO) via ArrayDeque
Deque<Integer> deque = new ArrayDeque<>();
deque.offerLast(1);
deque.offerLast(2);
deque.offerLast(3);
System.out.println("Deque (queue mode): " + deque);
System.out.println("pollFirst: " + deque.pollFirst());
System.out.println();
// Deque as Stack (LIFO)
Deque<String> stack = new ArrayDeque<>();
stack.push("Page A"); // pushes to front
stack.push("Page B");
stack.push("Page C");
System.out.println("Stack: " + stack);
System.out.println("pop: " + stack.pop()); // removes front
System.out.println("peek: " + stack.peek()); // views front
System.out.println("Stack after pop: " + stack);
}
}
Peek (head): Task-1
Poll (remove): Task-1
Queue after poll: [Task-2, Task-3]
Deque (queue mode): [1, 2, 3]
pollFirst: 1
Stack: [Page C, Page B, Page A]
pop: Page C
peek: Page B
Stack after pop: [Page B, Page A]
PriorityQueue
PriorityQueue orders elements
according to their natural ordering (for
Comparable types) or according to a
Comparator provided at construction
time. It does not permit null elements. The head
of the queue is always the least element.
import java.util.*;
public class PriorityQueueDemo {
public static void main(String[] args) {
// Natural ordering (min-heap — smallest first)
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(40);
pq.offer(10);
pq.offer(30);
pq.offer(20);
System.out.print("Poll order (natural): ");
while (!pq.isEmpty()) {
System.out.print(pq.poll() + " ");
}
System.out.println();
// Max-heap using custom Comparator
PriorityQueue<Integer> maxPQ =
new PriorityQueue<>(Comparator.reverseOrder());
maxPQ.offer(40);
maxPQ.offer(10);
maxPQ.offer(30);
maxPQ.offer(20);
System.out.print("Poll order (max-heap): ");
while (!maxPQ.isEmpty()) {
System.out.print(maxPQ.poll() + " ");
}
System.out.println();
// Custom objects — sort tasks by priority number
PriorityQueue<String> taskQueue =
new PriorityQueue<>(Comparator.comparingInt(String::length));
taskQueue.offer("LongTaskName");
taskQueue.offer("Go");
taskQueue.offer("Medium");
System.out.print("Tasks by name length: ");
while (!taskQueue.isEmpty()) {
System.out.print(taskQueue.poll() + " ");
}
System.out.println();
}
}
Poll order (max-heap): 40 30 20 10
Tasks by name length: Go Medium LongTaskName
Map Interface
A Map stores key-value pairs. Each key
is unique; each key maps to exactly one value. Maps are not part
of the Collection hierarchy but are a
fundamental part of the framework.
| Feature | HashMap | LinkedHashMap | TreeMap |
|---|---|---|---|
| Order | No guaranteed order | Insertion order | Sorted by key |
| Null keys | One null key allowed | One null key allowed | Not allowed |
| Null values | Allowed | Allowed | Allowed |
| get / put / remove | O(1) average | O(1) average | O(log n) |
| Backed by | Hash table | Hash table + linked list | Red-Black tree |
| Best for | General purpose | Cache (LRU pattern) | Sorted key access |
HashMap
HashMap is the most widely used
Map implementation. It stores entries
in an array of buckets using the hash of each key. Since Java 8,
buckets with many collisions are converted from linked lists to
balanced trees (red-black trees) for O(log n) worst-case lookup.
import java.util.*;
public class HashMapDemo {
public static void main(String[] args) {
HashMap<String, Integer> scores = new HashMap<>();
// put — add key-value pairs
scores.put("Alice", 95);
scores.put("Bob", 82);
scores.put("Charlie", 91);
scores.put("Diana", 88);
System.out.println("Map: " + scores);
// get — retrieve value by key
System.out.println("Alice's score: " + scores.get("Alice"));
// getOrDefault — safe retrieval
System.out.println("Eve's score: " + scores.getOrDefault("Eve", 0));
// containsKey and containsValue
System.out.println("Has Bob? " + scores.containsKey("Bob"));
System.out.println("Has score 91? " + scores.containsValue(91));
// update existing key
scores.put("Bob", 90);
System.out.println("Bob updated: " + scores.get("Bob"));
// remove
scores.remove("Diana");
System.out.println("After remove Diana: " + scores);
// iterate over entries
System.out.println("--- Entry Set ---");
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
// iterate over keys only
System.out.print("Keys: ");
for (String key : scores.keySet()) {
System.out.print(key + " ");
}
System.out.println();
// iterate over values only
System.out.print("Values: ");
for (int val : scores.values()) {
System.out.print(val + " ");
}
System.out.println();
}
}
Alice's score: 95
Eve's score: 0
Has Bob? true
Has score 91? true
Bob updated: 90
After remove Diana: {Alice=95, Bob=90, Charlie=91}
--- Entry Set ---
Alice -> 95
Bob -> 90
Charlie -> 91
Keys: Alice Bob Charlie
Values: 95 90 91
LinkedHashMap
LinkedHashMap preserves insertion
order while providing hash-table lookup. It is a good choice when
a map must be fast and deterministic to iterate. It can also be
configured for access order, which is useful for simple LRU caches.
import java.util.LinkedHashMap;
LinkedHashMap<String, Integer> counts = new LinkedHashMap<>();
counts.put("Java", 1);
counts.put("Spring", 2);
counts.put("SQL", 3);
System.out.println(counts);
TreeMap
TreeMap implements
NavigableMap and keeps entries sorted
by key. It is backed by a balanced tree, so
get, put,
and remove take O(log n) time. Use it
when sorted keys or range operations such as
subMap are important.
import java.util.TreeMap;
TreeMap<Integer, String> ranks = new TreeMap<>();
ranks.put(3, "Bronze");
ranks.put(1, "Gold");
ranks.put(2, "Silver");
System.out.println(ranks);
System.out.println("At or above 2: " + ranks.tailMap(2));
At or above 2: {2=Silver, 3=Bronze}
Iterator
The Iterator interface provides a
standard way to traverse any collection one element at a time. It
is the foundation of the enhanced for-loop. The key advantage of
using an explicit Iterator is the
ability to safely remove elements during traversal using
it.remove().
Warning: Do not modify a collection
directly (e.g., via list.remove())
while iterating with an enhanced for-loop — this causes a
ConcurrentModificationException. Use
Iterator.remove() instead.
import java.util.*;
public class IteratorDemo {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>(
Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
);
// Use Iterator to remove even numbers safely
Iterator<Integer> it = numbers.iterator();
while (it.hasNext()) {
int num = it.next();
if (num % 2 == 0) {
it.remove(); // safe removal during iteration
}
}
System.out.println("Odd numbers only: " + numbers);
// ListIterator — bidirectional traversal
List<String> words = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
ListIterator<String> li = words.listIterator(words.size()); // start at end
System.out.print("Reverse: ");
while (li.hasPrevious()) {
System.out.print(li.previous() + " ");
}
System.out.println();
}
}
Reverse: d c b a
Comparable vs Comparator
Java provides two interfaces to define object ordering:
-
Comparable<T>— defines the natural ordering of a class. The class itself implements this interface and overridescompareTo(T o). One sort order per class. -
Comparator<T>— defines an external ordering. Passed as a parameter to sort methods. Multiple different orderings can be defined.
import java.util.*;
class Student implements Comparable<Student> {
String name;
int gpa;
int age;
Student(String name, int gpa, int age) {
this.name = name; this.gpa = gpa; this.age = age;
}
// Natural ordering: by GPA descending
@Override
public int compareTo(Student other) {
return Integer.compare(other.gpa, this.gpa); // descending
}
@Override
public String toString() {
return name + "(GPA=" + gpa + ", age=" + age + ")";
}
}
public class ComparableComparatorDemo {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student("Alice", 90, 22));
students.add(new Student("Bob", 85, 20));
students.add(new Student("Charlie", 92, 21));
students.add(new Student("Diana", 88, 23));
// Sort using Comparable (natural order = GPA descending)
Collections.sort(students);
System.out.println("By GPA (Comparable): " + students);
// Sort using Comparator (by name alphabetically)
students.sort(Comparator.comparing(s -> s.name));
System.out.println("By Name (Comparator): " + students);
// Sort using Comparator (by age ascending)
students.sort(Comparator.comparingInt(s -> s.age));
System.out.println("By Age (Comparator): " + students);
// Chained comparators: by GPA desc, then by name asc
students.sort(
Comparator.comparingInt((Student s) -> s.gpa)
.reversed()
.thenComparing(s -> s.name)
);
System.out.println("By GPA desc then Name: " + students);
}
}
By Name (Comparator): [Alice(GPA=90, age=22), Bob(GPA=85, age=20), Charlie(GPA=92, age=21), Diana(GPA=88, age=23)]
By Age (Comparator): [Bob(GPA=85, age=20), Charlie(GPA=92, age=21), Alice(GPA=90, age=22), Diana(GPA=88, age=23)]
By GPA desc then Name: [Charlie(GPA=92, age=21), Alice(GPA=90, age=22), Diana(GPA=88, age=23), Bob(GPA=85, age=20)]
Rule of thumb: Use
Comparable for the class's single,
most natural ordering (e.g., numbers sort numerically, strings
sort lexicographically). Use
Comparator when you need multiple
orderings, when the class is not yours to modify, or for ad-hoc
lambda-based sorting.
Collections Utility Class
The java.util.Collections class
provides static algorithms and wrappers for collection objects.
Common methods include sort,
reverse, shuffle,
min, max,
binarySearch, and
frequency. The collection itself is
still supplied by an implementation such as
ArrayList.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
List<Integer> values = new ArrayList<>(List.of(4, 1, 3, 2, 2));
Collections.sort(values);
int position = Collections.binarySearch(values, 3);
System.out.println(values);
System.out.println("Index of 3: " + position);
System.out.println("Twos: " + Collections.frequency(values, 2));
Index of 3: 3
Twos: 2
Choosing the Right Collection
Use this decision table to pick the best collection for your use case:
| Use Case | Recommended Class | Reason |
|---|---|---|
| Ordered list, fast random access | ArrayList |
O(1) get by index |
| Frequent insert/delete at head or middle | LinkedList |
O(1) node pointer change |
| Unique elements, order not important | HashSet |
O(1) add/contains |
| Unique elements, preserve insertion order | LinkedHashSet |
Ordered + no dups |
| Unique elements, sorted order | TreeSet |
Always sorted, O(log n) |
| FIFO task queue | ArrayDeque |
Faster than LinkedList |
| Priority-based processing | PriorityQueue |
Min-heap, O(log n) poll |
| Key-value lookup, no order needed | HashMap |
O(1) get/put |
| Key-value, preserve insertion order | LinkedHashMap |
Ordered map, LRU cache |
| Key-value, sorted by key | TreeMap |
O(log n), sorted keys |
| Thread-safe list | CopyOnWriteArrayList |
Concurrent reads |
| Thread-safe map | ConcurrentHashMap |
Segment-level locking |
