Collections Framework
List, Set, Map and more
1. Introduction to Collections
The Java Collections Framework is a unified architecture for representing and manipulating collections of objects. It provides interfaces, implementations, and algorithms for efficient data handling.
What are Collections?
A collection is an object that groups multiple elements into a single unit. Collections are used to store, retrieve, manipulate, and communicate aggregate data.
Core Interfaces Overview
| Interface | Description | Key Features | Main Implementations |
|---|---|---|---|
| Collection | Root interface | Basic operations: add, remove, contains | Abstract base for all collections |
| List | Ordered collection | Allows duplicates, indexed access | ArrayList, LinkedList, Vector |
| Set | No duplicates | Mathematical set operations | HashSet, LinkedHashSet, TreeSet |
| Map | Key-value pairs | Unique keys, value mapping | HashMap, LinkedHashMap, TreeMap |
| Queue | Processing order | FIFO operations | LinkedList, PriorityQueue, ArrayDeque |
Basic Example
Collections Overview Example
import java.util.*;
public class CollectionsIntro {
public static void main(String[] args) {
// List - ordered collection with duplicates
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Apple"); // Duplicate allowed
// Set - no duplicates
Set<Integer> numbers = new HashSet<>();
numbers.add(1);
numbers.add(2);
numbers.add(1); // Duplicate ignored
// Map - key-value pairs
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 25);
ages.put("Bob", 30);
// Queue - FIFO operations
Queue<String> tasks = new LinkedList<>();
tasks.offer("Task 1");
tasks.offer("Task 2");
// Display results
System.out.println("Fruits: " + fruits); // [Apple, Banana, Apple]
System.out.println("Numbers: " + numbers); // [1, 2]
System.out.println("Ages: " + ages); // {Alice=25, Bob=30}
System.out.println("Tasks: " + tasks); // [Task 1, Task 2]
}
}
2. Collection Hierarchy
Understanding the collection hierarchy is crucial for choosing the right collection type for your specific use case.
The Collection Hierarchy
Iterable<T>
|
Collection<E>
|-- List<E>
| |-- ArrayList
| |-- LinkedList
| |-- Vector
|
|-- Set<E>
| |-- HashSet
| |-- LinkedHashSet
| |-- TreeSet
|
|-- Queue<E>
|-- PriorityQueue
|-- ArrayDeque
|-- LinkedList
Map<K,V> (separate hierarchy)
|-- HashMap
|-- LinkedHashMap
|-- TreeMap
Collection Interface Methods
Core Collection Methods
public interface Collection<E> extends Iterable<E> {
// Basic Operations
int size();
boolean isEmpty();
boolean contains(Object o);
boolean add(E e);
boolean remove(Object o);
Iterator<E> iterator();
// Bulk Operations
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean retainAll(Collection<?> c);
void clear();
// Array Operations
Object[] toArray();
<T> T[] toArray(T[] a);
}
Common Collection Operations
Collection Operations Example
import java.util.*;
public class CollectionOperations {
public static void main(String[] args) {
List<String> languages = new ArrayList<>();
// Adding elements
languages.add("Java");
languages.add("Python");
languages.add("JavaScript");
// Size and emptiness check
System.out.println("Size: " + languages.size());
System.out.println("Is Empty: " + languages.isEmpty());
// Contains check
System.out.println("Contains Java: " + languages.contains("Java"));
// Iteration with for-each
System.out.println("Languages:");
for (String lang : languages) {
System.out.println("- " + lang);
}
// Safe iteration with Iterator
Iterator<String> it = languages.iterator();
while (it.hasNext()) {
String lang = it.next();
if ("Python".equals(lang)) {
it.remove(); // Safe removal during iteration
}
}
// Bulk operations
List<String> moreLanguages = Arrays.asList("C++", "Go", "Rust");
languages.addAll(moreLanguages);
System.out.println("After operations: " + languages);
}
}
3. List Interface and Implementations
The List interface extends Collection and represents an ordered collection that can contain duplicate elements with indexed access.
Key Characteristics
- Maintains insertion order
- Allows duplicate elements
- Provides positional access (indexed)
- Supports list-specific operations
ArrayList Implementation
Dynamic array implementation providing fast random access and efficient iteration.
ArrayList Examples
import java.util.*;
public class ArrayListExample {
public static void main(String[] args) {
// Creation with initial capacity
List<String> fruits = new ArrayList<>(10);
// Adding elements
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
fruits.add(1, "Mango"); // Insert at index 1
System.out.println("Fruits: " + fruits);
// Accessing elements
String first = fruits.get(0);
System.out.println("First fruit: " + first);
// Modifying elements
fruits.set(2, "Grapes");
// Finding elements
int index = fruits.indexOf("Mango");
System.out.println("Mango index: " + index);
// Removing elements
fruits.remove(0); // Remove by index
fruits.remove("Orange"); // Remove by value
// Sublist operations
List<String> subList = fruits.subList(0, 2);
System.out.println("Sublist: " + subList);
// Sorting
Collections.sort(fruits);
System.out.println("Sorted: " + fruits);
}
}
LinkedList Implementation
Doubly-linked list implementation providing efficient insertion and deletion operations.
LinkedList Examples
import java.util.*;
public class LinkedListExample {
public static void main(String[] args) {
LinkedList<Integer> numbers = new LinkedList<>();
// Adding elements
numbers.add(10);
numbers.add(20);
numbers.add(30);
// LinkedList specific methods
numbers.addFirst(5); // Add to beginning
numbers.addLast(40); // Add to end
System.out.println("Numbers: " + numbers);
// Accessing first and last elements
System.out.println("First: " + numbers.getFirst());
System.out.println("Last: " + numbers.getLast());
// Using as Stack (LIFO)
LinkedList<String> stack = new LinkedList<>();
stack.push("First");
stack.push("Second");
stack.push("Third");
System.out.println("Stack: " + stack);
System.out.println("Pop: " + stack.pop());
// Using as Queue (FIFO)
LinkedList<String> queue = new LinkedList<>();
queue.offer("A");
queue.offer("B");
queue.offer("C");
System.out.println("Queue: " + queue);
System.out.println("Poll: " + queue.poll());
}
}
Performance Comparison
ArrayList
- Random Access: O(1)
- Insert at End: O(1)*
- Insert at Beginning: O(n)
- Memory Overhead: Low
LinkedList
- Random Access: O(n)
- Insert at End: O(1)
- Insert at Beginning: O(1)
- Memory Overhead: High
Choosing Between ArrayList and LinkedList
Use ArrayList when: You need frequent random access, have more reads than writes, or memory is a concern.
Use LinkedList when: You frequently insert/delete at the beginning/end, use it as a stack/queue, or have unpredictable insertion patterns.
4. Set Interface and Implementations
The Set interface models the mathematical set abstraction - a collection containing no duplicate elements.
Key Characteristics
- No duplicate elements allowed
- At most one null element
- Mathematical set operations
- Various ordering guarantees depending on implementation
HashSet Implementation
HashSet Examples
import java.util.*;
public class HashSetExample {
public static void main(String[] args) {
Set<String> colors = new HashSet<>();
// Adding elements
colors.add("Red");
colors.add("Green");
colors.add("Blue");
colors.add("Red"); // Duplicate - ignored
System.out.println("Colors: " + colors);
System.out.println("Size: " + colors.size());
// Set operations
Set<String> moreColors = new HashSet<>(Arrays.asList("Blue", "Yellow", "Purple"));
// Union
Set<String> union = new HashSet<>(colors);
union.addAll(moreColors);
System.out.println("Union: " + union);
// Intersection
Set<String> intersection = new HashSet<>(colors);
intersection.retainAll(moreColors);
System.out.println("Intersection: " + intersection);
// Difference
Set<String> difference = new HashSet<>(colors);
difference.removeAll(moreColors);
System.out.println("Difference: " + difference);
}
}
TreeSet Implementation
TreeSet Examples
import java.util.*;
public class TreeSetExample {
public static void main(String[] args) {
TreeSet<Integer> numbers = new TreeSet<>();
// Adding elements
numbers.add(5);
numbers.add(2);
numbers.add(8);
numbers.add(1);
numbers.add(9);
System.out.println("TreeSet: " + numbers); // Automatically sorted
// NavigableSet operations
System.out.println("First: " + numbers.first());
System.out.println("Last: " + numbers.last());
System.out.println("Lower than 5: " + numbers.lower(5));
System.out.println("Higher than 5: " + numbers.higher(5));
// Range operations
SortedSet<Integer> subset = numbers.subSet(2, 8);
System.out.println("Subset [2, 8): " + subset);
// Custom comparator
TreeSet<String> words = new TreeSet<>(Comparator.comparing(String::length));
words.add("elephant");
words.add("cat");
words.add("dog");
words.add("butterfly");
System.out.println("Words by length: " + words);
}
}
| Feature | HashSet | LinkedHashSet | TreeSet |
|---|---|---|---|
| Ordering | No guarantee | Insertion order | Natural/Comparator |
| Performance | O(1) | O(1) | O(log n) |
| Memory | Lowest | Medium | Highest |
| Null values | One allowed | One allowed | Not allowed |
| Range operations | No | No | Yes |
5. Map Interface and Implementations
The Map interface represents a mapping between keys and values. Each key can map to at most one value.
HashMap Implementation
HashMap Examples
import java.util.*;
public class HashMapExample {
public static void main(String[] args) {
Map<String, Integer> grades = new HashMap<>();
// Adding key-value pairs
grades.put("Alice", 95);
grades.put("Bob", 87);
grades.put("Charlie", 92);
// Accessing values
Integer aliceGrade = grades.get("Alice");
System.out.println("Alice's grade: " + aliceGrade);
// Safe operations
Integer defaultGrade = grades.getOrDefault("Eve", 0);
grades.putIfAbsent("Diana", 88);
// Iterating over entries
System.out.println("All grades:");
for (Map.Entry<String, Integer> entry : grades.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// Using streams for advanced operations
double average = grades.values().stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0.0);
System.out.println("Average grade: " + average);
// Find top student
Optional<Map.Entry<String, Integer>> topStudent =
grades.entrySet().stream()
.max(Map.Entry.comparingByValue());
topStudent.ifPresent(entry ->
System.out.println("Top student: " + entry.getKey()));
}
}
TreeMap Implementation
TreeMap Examples
import java.util.*;
public class TreeMapExample {
public static void main(String[] args) {
TreeMap<String, Double> prices = new TreeMap<>();
// Adding entries
prices.put("AAPL", 150.25);
prices.put("GOOGL", 2800.50);
prices.put("MSFT", 300.75);
prices.put("AMZN", 3200.00);
System.out.println("Sorted prices: " + prices);
// NavigableMap operations
System.out.println("First entry: " + prices.firstEntry());
System.out.println("Last entry: " + prices.lastEntry());
// Range queries
System.out.println("Stocks A-M: " + prices.subMap("A", "M"));
System.out.println("Stocks from M: " + prices.tailMap("M"));
// Finding highest priced stock
Map.Entry<String, Double> highest = prices.entrySet().stream()
.max(Map.Entry.comparingByValue())
.orElse(null);
System.out.println("Highest priced: " + highest);
}
}
6. Queue and Deque Interfaces
The Queue interface represents a collection designed for holding elements prior to processing, typically in FIFO order.
Queue Operations
| Operation Type | Throws Exception | Returns Special Value |
|---|---|---|
| Insert | add(e) | offer(e) |
| Remove | remove() | poll() |
| Examine | element() | peek() |
PriorityQueue Implementation
PriorityQueue Examples
import java.util.*;
public class PriorityQueueExample {
public static void main(String[] args) {
// Min-heap (natural ordering)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.offer(5);
minHeap.offer(2);
minHeap.offer(8);
minHeap.offer(1);
System.out.println("Polling from min-heap:");
while (!minHeap.isEmpty()) {
System.out.println(minHeap.poll()); // 1, 2, 5, 8
}
// Max-heap using reverse comparator
PriorityQueue<Integer> maxHeap =
new PriorityQueue<>(Collections.reverseOrder());
maxHeap.addAll(Arrays.asList(5, 2, 8, 1));
System.out.println("Polling from max-heap:");
while (!maxHeap.isEmpty()) {
System.out.println(maxHeap.poll()); // 8, 5, 2, 1
}
// Custom objects with priority
PriorityQueue<Task> taskQueue = new PriorityQueue<>(
Comparator.comparing(Task::getPriority).reversed()
);
taskQueue.offer(new Task("Low priority", 1));
taskQueue.offer(new Task("High priority", 10));
taskQueue.offer(new Task("Medium priority", 5));
System.out.println("Task execution order:");
while (!taskQueue.isEmpty()) {
Task task = taskQueue.poll();
System.out.println(task.getName() + " (Priority: " + task.getPriority() + ")");
}
}
static class Task {
private String name;
private int priority;
public Task(String name, int priority) {
this.name = name;
this.priority = priority;
}
public String getName() { return name; }
public int getPriority() { return priority; }
}
}
ArrayDeque Implementation
ArrayDeque Examples
import java.util.*;
public class ArrayDequeExample {
public static void main(String[] args) {
ArrayDeque<String> deque = new ArrayDeque<>();
// Adding elements to both ends
deque.addFirst("Second");
deque.addFirst("First");
deque.addLast("Third");
deque.addLast("Fourth");
System.out.println("Deque: " + deque);
// Using as Stack (LIFO)
ArrayDeque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println("Stack operations:");
while (!stack.isEmpty()) {
System.out.println("Pop: " + stack.pop());
}
// Using as Queue (FIFO)
ArrayDeque<String> queue = new ArrayDeque<>();
queue.offer("A");
queue.offer("B");
queue.offer("C");
System.out.println("Queue operations:");
while (!queue.isEmpty()) {
System.out.println("Poll: " + queue.poll());
}
// Palindrome checker
System.out.println("Is 'racecar' palindrome? " + isPalindrome("racecar"));
System.out.println("Is 'hello' palindrome? " + isPalindrome("hello"));
}
public static boolean isPalindrome(String str) {
ArrayDeque<Character> deque = new ArrayDeque<>();
for (char c : str.toLowerCase().toCharArray()) {
if (Character.isLetterOrDigit(c)) {
deque.offer(c);
}
}
while (deque.size() > 1) {
if (!deque.pollFirst().equals(deque.pollLast())) {
return false;
}
}
return true;
}
}
7. Advanced Operations
This chapter covers advanced operations and utilities for working with Java Collections effectively.
Collections Utility Class
Collections Utilities
import java.util.*;
public class CollectionsUtilities {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>(Arrays.asList(5, 2, 8, 1, 9));
System.out.println("Original: " + numbers);
// Sorting
Collections.sort(numbers);
System.out.println("Sorted: " + numbers);
// Reverse sorting
Collections.sort(numbers, Collections.reverseOrder());
System.out.println("Reverse sorted: " + numbers);
// Shuffling
Collections.shuffle(numbers);
System.out.println("Shuffled: " + numbers);
// Binary search (requires sorted list)
Collections.sort(numbers);
int index = Collections.binarySearch(numbers, 5);
System.out.println("Index of 5: " + index);
// Min and Max
System.out.println("Min: " + Collections.min(numbers));
System.out.println("Max: " + Collections.max(numbers));
// Frequency
List<String> words = Arrays.asList("hello", "world", "hello", "java");
System.out.println("Frequency of 'hello': " + Collections.frequency(words, "hello"));
// Rotate
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
Collections.rotate(list, 2);
System.out.println("Rotated by 2: " + list);
// Unmodifiable collections
List<String> immutable = Collections.unmodifiableList(list);
System.out.println("Immutable list: " + immutable);
}
}
Stream API with Collections
Stream Operations
import java.util.*;
import java.util.stream.*;
public class StreamOperations {
public static void main(String[] args) {
List<String> words = Arrays.asList(
"hello", "world", "java", "stream", "api", "collection"
);
// Filter, map, and collect
List<String> longWords = words.stream()
.filter(word -> word.length() > 4)
.map(String::toUpperCase)
.sorted()
.collect(Collectors.toList());
System.out.println("Long words: " + longWords);
// Grouping by length
Map<Integer, List<String>> wordsByLength = words.stream()
.collect(Collectors.groupingBy(String::length));
System.out.println("Words by length: " + wordsByLength);
// Partitioning
Map<Boolean, List<String>> partitioned = words.stream()
.collect(Collectors.partitioningBy(word -> word.length() > 4));
System.out.println("Long words: " + partitioned.get(true));
System.out.println("Short words: " + partitioned.get(false));
// Joining strings
String joined = words.stream()
.collect(Collectors.joining(", ", "[", "]"));
System.out.println("Joined: " + joined);
// Statistical operations
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
IntSummaryStatistics stats = numbers.stream()
.mapToInt(Integer::intValue)
.summaryStatistics();
System.out.println("Statistics: " + stats);
}
}
Custom Comparators
Advanced Comparators
import java.util.*;
public class CustomComparators {
public static void main(String[] args) {
List<Person> people = Arrays.asList(
new Person("Alice", 30, 75000),
new Person("Bob", 25, 65000),
new Person("Charlie", 35, 80000),
new Person("Diana", 28, 70000)
);
// Sort by age
people.sort(Comparator.comparing(Person::getAge));
System.out.println("By age: " + people);
// Sort by salary descending, then age ascending
people.sort(Comparator.comparing(Person::getSalary).reversed()
.thenComparing(Person::getAge));
System.out.println("By salary desc, age asc: " + people);
// Sort by name length, then alphabetically
people.sort(Comparator.comparing((Person p) -> p.getName().length())
.thenComparing(Person::getName));
System.out.println("By name length, then alphabetically: " + people);
}
static class Person {
private String name;
private int age;
private double salary;
public Person(String name, int age, double salary) {
this.name = name;
this.age = age;
this.salary = salary;
}
public String getName() { return name; }
public int getAge() { return age; }
public double getSalary() { return salary; }
@Override
public String toString() {
return String.format("Person{name='%s', age=%d, salary=%.0f}", name, age, salary);
}
}
}
8. Performance and Best Practices
Understanding performance characteristics and following best practices is crucial for efficient collection usage.
Performance Characteristics
| Collection | Get/Access | Add | Remove | Contains | Memory |
|---|---|---|---|---|---|
| ArrayList | O(1) | O(1) amortized | O(n) | O(n) | Low |
| LinkedList | O(n) | O(1) | O(1) if node known | O(n) | High |
| HashSet | N/A | O(1) | O(1) | O(1) | Medium |
| TreeSet | N/A | O(log n) | O(log n) | O(log n) | High |
| HashMap | O(1) | O(1) | O(1) | O(1) | Medium |
| TreeMap | O(log n) | O(log n) | O(log n) | O(log n) | High |
Best Practices Example
Collections Best Practices
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class BestPractices {
public static void main(String[] args) {
// 1. Choose the right collection type
demonstrateCollectionChoice();
// 2. Initialize with appropriate capacity
demonstrateCapacity();
// 3. Use immutable collections when possible
demonstrateImmutableCollections();
// 4. Thread safety considerations
demonstrateThreadSafety();
}
private static void demonstrateCollectionChoice() {
System.out.println("1. Choosing the right collection:");
// Use ArrayList for frequent random access
List<String> frequentAccess = new ArrayList<>();
// Use LinkedList for frequent insertions at beginning/end
List<String> frequentModification = new LinkedList<>();
// Use HashSet for fast membership testing
Set<String> fastLookup = new HashSet<>();
// Use ArrayDeque instead of Stack
Deque<String> stack = new ArrayDeque<>();
System.out.println("Choose collection based on usage patterns\n");
}
private static void demonstrateCapacity() {
System.out.println("2. Capacity initialization:");
// Initialize with expected size to avoid resizing
List<Integer> list = new ArrayList<>(1000);
// For maps, consider load factor
int expectedSize = 1000;
Map<String, String> map = new HashMap<>((int) (expectedSize / 0.75f) + 1);
System.out.println("Initialize with appropriate capacity\n");
}
private static void demonstrateImmutableCollections() {
System.out.println("3. Immutable collections:");
// Java 9+ immutable collections
List<String> immutableList = List.of("A", "B", "C");
Set<String> immutableSet = Set.of("X", "Y", "Z");
Map<String, Integer> immutableMap = Map.of("one", 1, "two", 2);
System.out.println("Immutable list: " + immutableList);
System.out.println("Use immutable collections when data won't change\n");
}
private static void demonstrateThreadSafety() {
System.out.println("4. Thread safety:");
// Thread-safe collections
Map<String, String> concurrentMap = new ConcurrentHashMap<>();
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
System.out.println("Choose appropriate thread-safe collections\n");
}
}
Key Performance Tips
- Initialize collections with expected capacity
- Use appropriate collection for access patterns
- Implement equals() and hashCode() properly
- Use enhanced for-loops when possible
- Consider thread-safety requirements
Common Pitfalls
- ConcurrentModificationException during iteration
- Poor hashCode() implementation
- Using wrong collection type
- Memory leaks with unused references
- Not using generics (raw types)
Collection Selection Guide
- ArrayList: Random access, few modifications
- LinkedList: Frequent insertions/deletions
- HashSet: Fast lookups, no duplicates
- TreeSet: Sorted unique elements
- HashMap: Key-value mapping
- ArrayDeque: Stack/Queue operations
Key Takeaways
- Choose the right collection based on your specific use case
- Understand time complexity for different operations
- Use generics for type safety and better performance
- Leverage Stream API for functional programming
- Consider thread safety requirements in concurrent applications
- Always implement equals() and hashCode() consistently