Java Collections Framework Overview

You’ve used ArrayList to store collections of objects. But ArrayList is just one option. The Collections Framework provides many data structures, each optimized for different tasks. Choosing the right one can make your code faster and cleaner.

The Collections Hierarchy

The framework is built on interfaces that define behavior:

Collection
├── List (ordered, allows duplicates)
│   ├── ArrayList
│   ├── LinkedList
│   └── Vector
├── Set (no duplicates)
│   ├── HashSet
│   ├── LinkedHashSet
│   └── TreeSet
└── Queue (FIFO ordering)
    ├── LinkedList
    ├── PriorityQueue
    └── ArrayDeque

Map (key-value pairs, separate hierarchy)
├── HashMap
├── LinkedHashMap
├── TreeMap
└── Hashtable

Map doesn’t extend Collection because it stores pairs, not single elements. But it’s considered part of the framework.

List: Ordered Collections

Lists maintain insertion order and allow duplicates. Access elements by index.

ArrayList

Fast random access, slow insertions in the middle. Best for read-heavy operations.

List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Apple");  // Duplicates allowed

System.out.println(list.get(0));  // Apple - fast
System.out.println(list);  // [Apple, Banana, Apple]

LinkedList

Fast insertions and deletions, slower random access. Also implements Queue and Deque.

List<String> list = new LinkedList<>();
list.add("A");
list.add("B");
list.add(1, "X");  // Insert at index 1 - fast

System.out.println(list);  // [A, X, B]

Use ArrayList by default. Use LinkedList when you frequently add or remove elements from the middle.

Set: Unique Elements

Sets don’t allow duplicates. Attempting to add an existing element has no effect.

HashSet

Fastest set operations. No guaranteed order.

Set<String> set = new HashSet<>();
set.add("Apple");
set.add("Banana");
set.add("Apple");  // Ignored - already exists

System.out.println(set.size());  // 2
System.out.println(set.contains("Banana"));  // true
System.out.println(set);  // Order not guaranteed

LinkedHashSet

Maintains insertion order. Slightly slower than HashSet.

Set<String> set = new LinkedHashSet<>();
set.add("Banana");
set.add("Apple");
set.add("Cherry");

System.out.println(set);  // [Banana, Apple, Cherry] - insertion order

TreeSet

Keeps elements sorted. Uses a tree structure.

Set<String> set = new TreeSet<>();
set.add("Banana");
set.add("Apple");
set.add("Cherry");

System.out.println(set);  // [Apple, Banana, Cherry] - sorted

TreeSet requires elements to be Comparable or you must provide a Comparator.

Map: Key-Value Pairs

Maps store associations between keys and values. Each key maps to one value.

HashMap

Fast lookups by key. No guaranteed order.

Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 25);
ages.put("Bob", 30);
ages.put("Charlie", 35);

System.out.println(ages.get("Bob"));  // 30
System.out.println(ages.containsKey("Alice"));  // true
System.out.println(ages.containsValue(35));  // true

// Overwrite existing key
ages.put("Alice", 26);
System.out.println(ages.get("Alice"));  // 26

LinkedHashMap

Maintains insertion order.

Map<String, Integer> scores = new LinkedHashMap<>();
scores.put("Round 1", 100);
scores.put("Round 2", 85);
scores.put("Round 3", 92);

// Iteration follows insertion order
for (String key : scores.keySet()) {
    System.out.println(key + ": " + scores.get(key));
}

TreeMap

Keeps keys sorted.

Map<String, String> capitals = new TreeMap<>();
capitals.put("USA", "Washington D.C.");
capitals.put("France", "Paris");
capitals.put("Japan", "Tokyo");

// Keys in alphabetical order
for (String country : capitals.keySet()) {
    System.out.println(country + ": " + capitals.get(country));
}
// France: Paris
// Japan: Tokyo
// USA: Washington D.C.

Queue: First-In-First-Out

Queues process elements in order. Add to the back, remove from the front.

Queue<String> queue = new LinkedList<>();
queue.add("First");
queue.add("Second");
queue.add("Third");

System.out.println(queue.peek());   // First (view without removing)
System.out.println(queue.poll());   // First (remove and return)
System.out.println(queue.poll());   // Second
System.out.println(queue);          // [Third]

PriorityQueue

Elements are ordered by priority, not insertion order.

Queue<Integer> pq = new PriorityQueue<>();
pq.add(5);
pq.add(1);
pq.add(3);

System.out.println(pq.poll());  // 1 (smallest first)
System.out.println(pq.poll());  // 3
System.out.println(pq.poll());  // 5

Deque: Double-Ended Queue

Deque (pronounced “deck”) allows adding and removing from both ends.

Deque<String> deque = new ArrayDeque<>();
deque.addFirst("A");
deque.addLast("B");
deque.addFirst("C");

System.out.println(deque);  // [C, A, B]
System.out.println(deque.removeFirst());  // C
System.out.println(deque.removeLast());   // B

ArrayDeque is also efficient as a stack (LIFO):

Deque<String> stack = new ArrayDeque<>();
stack.push("Bottom");
stack.push("Middle");
stack.push("Top");

System.out.println(stack.pop());  // Top
System.out.println(stack.pop());  // Middle

Iterating Collections

Several ways to loop through collections:

List<String> list = Arrays.asList("A", "B", "C");

// Enhanced for loop
for (String item : list) {
    System.out.println(item);
}

// forEach with lambda
list.forEach(item -> System.out.println(item));

// Iterator
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    System.out.println(it.next());
}

For maps, iterate over keys, values, or entries:

Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);

// Keys
for (String key : map.keySet()) {
    System.out.println(key);
}

// Values
for (Integer value : map.values()) {
    System.out.println(value);
}

// Entries (key-value pairs)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

Common Operations

The Collections utility class provides helpful methods:

import java.util.Collections;

List<Integer> numbers = new ArrayList<>(Arrays.asList(3, 1, 4, 1, 5, 9));

Collections.sort(numbers);           // [1, 1, 3, 4, 5, 9]
Collections.reverse(numbers);        // [9, 5, 4, 3, 1, 1]
Collections.shuffle(numbers);        // Random order
int max = Collections.max(numbers);  // 9
int min = Collections.min(numbers);  // 1
int freq = Collections.frequency(numbers, 1);  // 2

Choosing the Right Collection

Need ordered elements with duplicates? Use List (ArrayList for most cases).

Need unique elements? Use Set (HashSet for speed, TreeSet for sorting).

Need key-value lookups? Use Map (HashMap for speed, TreeMap for sorted keys).

Need FIFO processing? Use Queue (LinkedList or ArrayDeque).

Need LIFO (stack)? Use Deque as a stack (ArrayDeque).

Practical Example: Word Frequency Counter

import java.util.*;

public class WordCounter {
    public static void main(String[] args) {
        String text = "the quick brown fox jumps over the lazy dog the fox is quick";
        
        // Split into words
        String[] words = text.toLowerCase().split("\\s+");
        
        // Count frequencies using HashMap
        Map<String, Integer> frequency = new HashMap<>();
        for (String word : words) {
            frequency.put(word, frequency.getOrDefault(word, 0) + 1);
        }
        
        System.out.println("Word frequencies:");
        for (Map.Entry<String, Integer> entry : frequency.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        
        // Find unique words using Set
        Set<String> uniqueWords = new HashSet<>(Arrays.asList(words));
        System.out.println("\nUnique words: " + uniqueWords.size());
        
        // Sort by frequency
        List<Map.Entry<String, Integer>> sorted = new ArrayList<>(frequency.entrySet());
        sorted.sort((a, b) -> b.getValue() - a.getValue());
        
        System.out.println("\nTop words:");
        for (int i = 0; i < Math.min(5, sorted.size()); i++) {
            Map.Entry<String, Integer> entry = sorted.get(i);
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}

Output:

Word frequencies:
the: 3
quick: 2
brown: 1
fox: 2
jumps: 1
over: 1
lazy: 1
dog: 1
is: 1

Unique words: 9

Top words:
the: 3
quick: 2
fox: 2
brown: 1
jumps: 1

Performance Comparison

Operation ArrayList LinkedList HashSet TreeSet
Access by index O(1) O(n) N/A N/A
Add/remove at end O(1) O(1) O(1) O(log n)
Add/remove in middle O(n) O(1)* N/A N/A
Search (contains) O(n) O(n) O(1) O(log n)

*LinkedList insertion is O(1) only if you already have a reference to the position.

Common Mistakes

Modifying during iteration. Don’t add or remove elements while iterating with a for-each loop. Use Iterator.remove() or iterate over a copy.

Using wrong equals/hashCode. HashSet and HashMap rely on equals() and hashCode(). Custom objects need both methods implemented correctly.

Choosing based on familiarity. Don’t use ArrayList for everything. Pick the collection that matches your access patterns.

Ignoring null behavior. TreeSet and TreeMap don’t allow null keys. HashMap allows one null key.

What’s Next

Collections store objects of specific types thanks to generics. The final tutorial in this series explains how generics work and how to create your own generic classes and methods.


Related: Reading and Writing Files in Java | Introduction to Java Generics

Sources

  • Oracle. “Collections.” The Java Tutorials. docs.oracle.com
  • Oracle. “The Map Interface.” The Java Tutorials. docs.oracle.com
Scroll to Top