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 like ArrayList, HashSet, and LinkedList implement 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);
    }
}
Before sort: [Alice, Charlie, Bob]
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();
    }
}
List: [Apple, Banana, Cherry, Date]
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);
    }
}
LinkedList: [First, Middle, Last]
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());
[Red, Green, Blue]
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());
Top: Second
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);
    }
}
Set: [Java, Go, C++, Python]
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);
[Java, Python, Go]

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"));
[Amit, Priya, 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);
    }
}
Queue: [Task-1, Task-2, Task-3]
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 (natural): 10 20 30 40
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();
    }
}
Map: {Alice=95, Bob=82, Charlie=91, Diana=88}
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);
{Java=1, Spring=2, SQL=3}

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));
{1=Gold, 2=Silver, 3=Bronze}
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();
    }
}
Odd numbers only: [1, 3, 5, 7, 9]
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 overrides compareTo(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 GPA (Comparable): [Charlie(GPA=92, age=21), Alice(GPA=90, age=22), Diana(GPA=88, age=23), Bob(GPA=85, age=20)]
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));
[1, 2, 2, 3, 4]
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

Continue learning