Java Collections Framework - Complete Guide
The Java Collections Framework is a unified architecture for storing and manipulating groups of objects. Instead of creating data structures from scratch, Java provides ready-to-use interfaces and implementations for lists, sets, maps, queues, and other collection types.
The most important part of the Collections Framework is understanding that different data structures solve different problems. An ArrayList is useful when you need an ordered collection with fast index-based access, a HashSet is useful when duplicate values should be eliminated, and a HashMap is useful when data needs to be stored as key-value pairs.
In this guide, we will understand the major collection interfaces, their implementations, common operations, performance characteristics, sorting, iterators, immutable collections, concurrent collections, and practical rules for choosing the right collection.
Why Do We Need Collections?
Arrays are useful when the number of elements is fixed and indexed access is required. However, real applications frequently need dynamic data structures where elements can be added, removed, searched, sorted, or grouped.
For example, an e-commerce application may need a list of products, a set of unique product categories, a map of product IDs to products, and a queue of orders waiting to be processed.
List<String> products = new ArrayList<>();
Set<String> categories = new HashSet<>();
Map<Long, String> productNames = new HashMap<>();
Queue<String> orders = new ArrayDeque<>();
Java Collections Hierarchy
The Collection interface represents a group of objects. List, Set, and Queue are the major interfaces derived from Collection. Map is part of the Collections Framework but does not extend Collection because it represents key-value mappings rather than individual elements.
Iterable
└── Collection
├── List
│ ├── ArrayList
│ └── LinkedList
│
├── Set
│ ├── HashSet
│ ├── LinkedHashSet
│ └── TreeSet
│
└── Queue
├── PriorityQueue
└── Deque
└── ArrayDeque
Map
├── HashMap
├── LinkedHashMap
└── TreeMap
List Interface
List represents an ordered collection. Elements have positions called indexes, duplicate elements are allowed, and elements can generally be accessed using their index.
The most commonly used List implementations are ArrayList and LinkedList. In most general-purpose applications, ArrayList should be the first implementation to consider.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> languages = new ArrayList<>();
languages.add("Java");
languages.add("Python");
languages.add("JavaScript");
languages.add("Java");
System.out.println(languages);
System.out.println(languages.get(1));
System.out.println(languages.size());
languages.set(1, "Kotlin");
languages.remove("JavaScript");
System.out.println(languages);
}
}
ArrayList
ArrayList is a resizable-array implementation of List. It provides fast random access because elements are stored in an array-like structure internally.
ArrayList is usually the best default choice when you need a general-purpose list. Adding elements at the end is efficient, while inserting or removing elements near the beginning or middle can require shifting other elements.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> users = new ArrayList<>();
users.add("Alice");
users.add("Bob");
users.add("Charlie");
users.add(1, "David");
System.out.println(users);
System.out.println("First user: " + users.get(0));
System.out.println("Size: " + users.size());
}
}
When Should You Use ArrayList?
Use ArrayList when you frequently access elements by index, mostly add elements to the end, need iteration over a sequence, and do not require special concurrency behavior.
LinkedList
LinkedList implements both List and Deque. It is based on linked nodes rather than a contiguous array. It can efficiently add or remove elements at the ends and can be useful for deque-style operations.
LinkedList is not automatically faster than ArrayList for insertion and removal. If you need to locate an element by index first, traversal can be expensive. For many ordinary list workloads, ArrayList provides better practical performance and memory efficiency.
import java.util.LinkedList;
public class Main {
public static void main(String[] args) {
LinkedList<String> tasks = new LinkedList<>();
tasks.addLast("Task 1");
tasks.addLast("Task 2");
tasks.addFirst("Urgent Task");
System.out.println(tasks);
String first = tasks.removeFirst();
String last = tasks.removeLast();
System.out.println("Removed first: " + first);
System.out.println("Removed last: " + last);
}
}
Set Interface
Set represents a collection that does not allow duplicate elements. The exact ordering behavior depends on the implementation.
HashSet does not guarantee iteration order, LinkedHashSet preserves insertion order, and TreeSet keeps elements sorted.
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> languages = new HashSet<>();
languages.add("Java");
languages.add("Python");
languages.add("Java");
languages.add("Go");
System.out.println(languages);
System.out.println(languages.contains("Java"));
}
}
HashSet
HashSet uses hashing to provide efficient average-time insertion, removal, and membership checks. It is a good choice when uniqueness matters and element ordering is not part of the requirement.
HashSet uses hashCode and equals to determine whether elements are considered duplicates. Custom classes should therefore implement equals and hashCode consistently when they are stored in hash-based collections.
import java.util.HashSet;
import java.util.Set;
record User(String email) {}
public class Main {
public static void main(String[] args) {
Set<User> users = new HashSet<>();
users.add(new User("alice@example.com"));
users.add(new User("alice@example.com"));
users.add(new User("bob@example.com"));
System.out.println(users);
System.out.println("Users: " + users.size());
}
}
LinkedHashSet
LinkedHashSet combines uniqueness with predictable insertion-order iteration. It is useful when duplicate values must be removed but the original order should be preserved.
import java.util.LinkedHashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> tags = new LinkedHashSet<>();
tags.add("java");
tags.add("backend");
tags.add("java");
tags.add("api");
tags.add("backend");
System.out.println(tags);
}
}
TreeSet
TreeSet stores unique elements in sorted order. Elements must either implement Comparable or be ordered using a Comparator supplied to the TreeSet.
import java.util.Set;
import java.util.TreeSet;
public class Main {
public static void main(String[] args) {
Set<Integer> numbers = new TreeSet<>();
numbers.add(50);
numbers.add(10);
numbers.add(30);
numbers.add(10);
numbers.add(20);
System.out.println(numbers);
}
}
Map Interface
Map stores data as key-value pairs. Every key is unique within a map, while multiple keys can point to equal values.
Maps are commonly used for fast lookup. For example, a user ID can be used as a key to find a user object, or a product code can be used to find product information.
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<Integer, String> users = new HashMap<>();
users.put(101, "Alice");
users.put(102, "Bob");
users.put(103, "Charlie");
System.out.println(users.get(102));
System.out.println(users.containsKey(101));
users.put(102, "Robert");
users.remove(103);
System.out.println(users);
}
}
HashMap
HashMap is the most commonly used general-purpose Map implementation. It provides efficient average-time lookup, insertion, and removal based on hashing.
HashMap does not guarantee iteration order. If your application depends on insertion order or sorted keys, choose a different implementation.
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
String[] words = {"java", "python", "java", "go", "java"};
Map<String, Integer> counts = new HashMap<>();
for (String word : words) {
counts.merge(word, 1, Integer::sum);
}
System.out.println(counts);
}
}
Important HashMap Methods
Modern Java provides several methods that make map-based operations concise and safe. getOrDefault is useful when a key may not exist, putIfAbsent inserts only when a key is missing, and computeIfAbsent can lazily create a value.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, List<String>> groups = new HashMap<>();
groups.computeIfAbsent("backend", key -> new ArrayList<>())
.add("Java");
groups.computeIfAbsent("backend", key -> new ArrayList<>())
.add("Spring");
groups.putIfAbsent("frontend", new ArrayList<>());
System.out.println(groups);
}
}
LinkedHashMap
LinkedHashMap maintains a predictable iteration order. By default, that order is insertion order. It can also be configured for access-order behavior, which is useful for certain cache implementations.
import java.util.LinkedHashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> scores = new LinkedHashMap<>();
scores.put("Alice", 90);
scores.put("Bob", 85);
scores.put("Charlie", 95);
scores.forEach((name, score) ->
System.out.println(name + " -> " + score)
);
}
}
TreeMap
TreeMap stores key-value pairs in sorted key order. It is useful when applications need ordered navigation or range-based operations in addition to map lookup.
import java.util.Map;
import java.util.TreeMap;
public class Main {
public static void main(String[] args) {
Map<Integer, String> users = new TreeMap<>();
users.put(30, "Charlie");
users.put(10, "Alice");
users.put(20, "Bob");
System.out.println(users);
System.out.println("First key: " + ((TreeMap<Integer, String>) users).firstKey());
System.out.println("Last key: " + ((TreeMap<Integer, String>) users).lastKey());
}
}
Queue Interface
Queue represents a collection designed for holding elements before they are processed. A typical queue follows FIFO behavior, meaning the first element inserted is normally the first element removed.
The most common Queue methods are offer, poll, and peek. These methods are preferable when you want operations that return special values instead of throwing exceptions when the operation cannot be completed.
import java.util.ArrayDeque;
import java.util.Queue;
public class Main {
public static void main(String[] args) {
Queue<String> queue = new ArrayDeque<>();
queue.offer("Order-101");
queue.offer("Order-102");
queue.offer("Order-103");
System.out.println("Next: " + queue.peek());
System.out.println("Processing: " + queue.poll());
System.out.println("Remaining: " + queue);
}
}
PriorityQueue
PriorityQueue does not behave like a normal FIFO queue. Elements are ordered according to their natural ordering or a supplied Comparator, and poll returns the element at the head of that priority ordering.
import java.util.PriorityQueue;
import java.util.Queue;
record Task(String name, int priority) {}
public class Main {
public static void main(String[] args) {
Queue<Task> tasks = new PriorityQueue<>(
(a, b) -> Integer.compare(a.priority(), b.priority())
);
tasks.offer(new Task("Normal task", 3));
tasks.offer(new Task("Critical task", 1));
tasks.offer(new Task("Important task", 2));
while (!tasks.isEmpty()) {
Task task = tasks.poll();
System.out.println(task.name());
}
}
}
Deque and ArrayDeque
Deque stands for double-ended queue. It supports insertion and removal at both the beginning and the end. ArrayDeque is usually preferred over Stack for stack-like behavior in modern Java code.
import java.util.ArrayDeque;
import java.util.Deque;
public class Main {
public static void main(String[] args) {
Deque<String> deque = new ArrayDeque<>();
deque.addLast("A");
deque.addLast("B");
deque.addLast("C");
System.out.println(deque.removeFirst());
deque.addFirst("Start");
System.out.println(deque);
System.out.println("Stack pop: " + deque.removeLast());
}
}
Iterator
Iterator provides a standard way to traverse collections. One important advantage is that Iterator provides a remove method that can safely remove the current element during iteration.
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.add(40);
Iterator<Integer> iterator = numbers.iterator();
while (iterator.hasNext()) {
int number = iterator.next();
if (number % 20 == 0) {
iterator.remove();
}
}
System.out.println(numbers);
}
}
Sorting Collections
Lists can be sorted using List.sort or Collections.sort. Comparator allows you to define custom sorting rules without changing the underlying class.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Alexander");
names.add("Bob");
names.add("Chris");
names.add("Ann");
names.sort(
Comparator.comparingInt(String::length)
.thenComparing(String::compareTo)
);
System.out.println(names);
}
}
Sorting Custom Objects
When working with domain objects, Comparator can define different sorting strategies. The same list can therefore be sorted by price, name, date, priority, or any other property.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
record Product(String name, double price) {}
public class Main {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("Laptop", 1200));
products.add(new Product("Mouse", 30));
products.add(new Product("Keyboard", 80));
products.sort(Comparator.comparingDouble(Product::price));
products.forEach(product ->
System.out.println(product.name() + " - " + product.price())
);
}
}
equals and hashCode
Hash-based collections such as HashMap and HashSet depend on equals and hashCode. If two objects are logically equal, their hashCode values must also be equal.
When creating custom classes used as keys in HashMap or elements in HashSet, implement equals and hashCode consistently. Java records are convenient because they automatically generate these methods from their components.
import java.util.Objects;
class User {
private final int id;
private final String name;
User(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof User other)) return false;
return id == other.id;
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public String toString() {
return id + ": " + name;
}
}
Immutable and Unmodifiable Collections
Java provides factory methods such as List.of, Set.of, and Map.of for creating unmodifiable collections. These are useful when a collection should not be changed after creation.
These factory methods reject null elements or keys and do not permit structural modification. If you need a mutable collection initialized from existing data, create a mutable copy such as new ArrayList<>(list).
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class Main {
public static void main(String[] args) {
List<String> languages = List.of("Java", "Python", "Go");
Set<Integer> numbers = Set.of(10, 20, 30);
Map<String, Integer> scores = Map.of(
"Alice", 90,
"Bob", 85
);
List<String> mutableLanguages = new ArrayList<>(languages);
mutableLanguages.add("Kotlin");
System.out.println(languages);
System.out.println(numbers);
System.out.println(scores);
System.out.println(mutableLanguages);
}
}
Collections Utility Class
The java.util.Collections class contains utility methods for working with collections. Common operations include sorting, reversing, shuffling, finding minimum and maximum values, and performing binary searches on sorted lists.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
numbers.add(40);
numbers.add(10);
numbers.add(30);
numbers.add(20);
Collections.sort(numbers);
System.out.println("Sorted: " + numbers);
System.out.println("Min: " + Collections.min(numbers));
System.out.println("Max: " + Collections.max(numbers));
int index = Collections.binarySearch(numbers, 30);
System.out.println("Index of 30: " + index);
Collections.reverse(numbers);
System.out.println("Reversed: " + numbers);
}
}
Concurrent Collections
Normal collections such as ArrayList and HashMap are not designed for unsynchronized concurrent modification by multiple threads. The java.util.concurrent package provides specialized collections for concurrent applications.
Important concurrent collections include ConcurrentHashMap, CopyOnWriteArrayList, ConcurrentLinkedQueue, and several BlockingQueue implementations.
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class Main {
public static void main(String[] args) {
ConcurrentMap<String, Integer> counts = new ConcurrentHashMap<>();
counts.put("Java", 10);
counts.merge("Java", 5, Integer::sum);
counts.putIfAbsent("Python", 3);
System.out.println(counts);
}
}
CopyOnWriteArrayList
CopyOnWriteArrayList is a thread-safe List implementation designed for workloads where reads are frequent and modifications are relatively rare. Instead of modifying the existing backing array, a write operation creates a new copy.
This design makes iteration safe without requiring external synchronization, but frequent writes can be expensive because each modification may copy the underlying array.
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class Main {
public static void main(String[] args) {
List<String> listeners = new CopyOnWriteArrayList<>();
listeners.add("Listener-A");
listeners.add("Listener-B");
for (String listener : listeners) {
System.out.println(listener);
}
}
}
BlockingQueue
BlockingQueue is designed for producer-consumer systems. A producer can wait when the queue is full, while a consumer can wait when the queue is empty.
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class Main {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(2);
Thread producer = new Thread(() -> {
try {
queue.put("Task 1");
queue.put("Task 2");
queue.put("Task 3");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumer = new Thread(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
String task = queue.take();
System.out.println("Processing: " + task);
if (task.equals("Task 3")) {
break;
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
producer.join();
consumer.join();
}
}
Collection Performance
Understanding approximate operation complexity helps when choosing a collection. Big-O describes how an operation scales with the number of elements, but real-world performance also depends on memory locality, object allocation, workload, and implementation details.
ArrayList
- get(index): O(1)
- set(index): O(1)
- add at end: amortized O(1)
- search: O(n)
- insert/remove at middle: O(n)
HashSet
- add: average O(1)
- contains: average O(1)
- remove: average O(1)
TreeSet
- add: O(log n)
- contains: O(log n)
- remove: O(log n)
HashMap
- put: average O(1)
- get: average O(1)
- containsKey: average O(1)
TreeMap
- put: O(log n)
- get: O(log n)
- remove: O(log n)
ArrayDeque
- add/remove at either end: O(1) amortized
ArrayList vs LinkedList
ArrayList provides fast index-based access and is generally the better default for ordinary list workloads. LinkedList can be useful when its deque operations match the problem, but it should not be selected simply because insertion or deletion is theoretically efficient.
ArrayList
- Backed by a resizable array
- Fast random access
- Efficient iteration
- Good general-purpose List
- Insert/remove in middle may require shifting elements
LinkedList
- Node-based structure
- Implements List and Deque
- Efficient operations at known ends
- Slower random access
- Higher memory overhead per element
HashMap vs TreeMap
HashMap is generally preferred when you need key-based lookup without ordering requirements. TreeMap is appropriate when keys must remain sorted or when you need navigable and range-based operations.
HashMap
- Hash-based lookup
- Average O(1) basic operations
- No guaranteed iteration order
- General-purpose Map
TreeMap
- Sorted keys
- O(log n) basic operations
- Supports navigable operations
- Useful for ordered and range-based data
How to Choose the Right Collection
Start by identifying the behavior your application needs instead of choosing an implementation first.
Need ordered elements with index access?
-> List / ArrayList
Need unique elements?
-> Set / HashSet
Need unique elements and insertion order?
-> LinkedHashSet
Need unique sorted elements?
-> TreeSet
Need key-value lookup?
-> HashMap
Need key-value lookup with insertion order?
-> LinkedHashMap
Need sorted keys?
-> TreeMap
Need FIFO processing?
-> Queue / ArrayDeque
Need operations at both ends?
-> Deque / ArrayDeque
Need priority-based processing?
-> PriorityQueue
Need concurrent key-value access?
-> ConcurrentHashMap
Need producer-consumer communication?
-> BlockingQueue
Generics and Type Safety
Generics allow collections to specify the type of elements they contain. This provides compile-time type checking and eliminates many explicit casts.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
String firstName = names.get(0);
System.out.println(firstName);
}
}
Prefer List
Java Collections Best Practices
Program to interfaces rather than concrete implementations. Declare variables using List, Set, Map, Queue, or Deque when the implementation details are not part of the API requirement.
Choose a collection based on required behavior rather than assumptions about speed. Consider ordering, uniqueness, lookup patterns, insertion patterns, memory usage, and concurrency requirements.
Use generics consistently, avoid raw collections, and prefer immutable collections when data should not be modified.
Do not rely on the iteration order of HashMap or HashSet. If order matters, explicitly choose LinkedHashMap, LinkedHashSet, TreeMap, or TreeSet depending on the required ordering semantics.
For concurrent applications, use collections from java.util.concurrent when their semantics match the problem instead of manually synchronizing ordinary collections without understanding the required concurrency behavior.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> users = new ArrayList<>();
users.add("Alice");
users.add("Bob");
for (String user : users) {
System.out.println(user);
}
}
}
Common Collection Mistakes
One common mistake is using a List when the application actually requires uniqueness. A Set can express that requirement directly and avoid manually checking for duplicates.
Another common mistake is expecting HashMap or HashSet to preserve insertion order. Their iteration order should not be treated as an application contract.
Removing elements directly from a collection inside an enhanced for loop can cause ConcurrentModificationException for fail-fast iterators. Use Iterator.remove or an appropriate collection operation instead.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(15);
numbers.add(20);
numbers.add(25);
numbers.removeIf(number -> number % 2 != 0);
System.out.println(numbers);
}
}
Real-World Example: Product Catalog
A product catalog can use multiple collection types together. A List can maintain products in display order, a Map can provide fast lookup by product ID, and a Set can maintain unique categories.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
record Product(long id, String name, String category) {}
public class Main {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
Map<Long, Product> productsById = new HashMap<>();
Set<String> categories = new HashSet<>();
Product laptop = new Product(101, "Laptop", "Electronics");
Product keyboard = new Product(102, "Keyboard", "Electronics");
Product chair = new Product(103, "Office Chair", "Furniture");
products.add(laptop);
products.add(keyboard);
products.add(chair);
for (Product product : products) {
productsById.put(product.id(), product);
categories.add(product.category());
}
System.out.println("Product: " + productsById.get(102));
System.out.println("Categories: " + categories);
}
}
Java Collections Quick Reference
ArrayList -> Ordered list, duplicates allowed, fast index access
LinkedList -> List + Deque behavior
HashSet -> Unique elements, no guaranteed order
LinkedHashSet -> Unique elements, insertion order
TreeSet -> Unique elements, sorted order
HashMap -> Key-value lookup, no guaranteed order
LinkedHashMap -> Key-value lookup, insertion order
TreeMap -> Key-value lookup, sorted keys
ArrayDeque -> Efficient operations at both ends
PriorityQueue -> Priority-based processing
ConcurrentHashMap -> Concurrent key-value operations
BlockingQueue -> Producer-consumer communication
Conclusion
The Java Collections Framework provides a complete set of reusable data structures for everyday application development. The key to using collections effectively is not memorizing every class, but understanding the behavior each interface and implementation provides.
Use List when order and duplicates matter, Set when uniqueness matters, Map when key-value lookup is required, Queue when elements need to be processed in sequence, and Deque when both ends need to be accessed.
Once the basic interfaces are understood, choose between implementations such as ArrayList, HashSet, LinkedHashSet, TreeSet, HashMap, LinkedHashMap, TreeMap, ArrayDeque, and PriorityQueue based on ordering, performance, memory, and concurrency requirements.