Java Collections Framework

Master List, Set, Map & Queue - Choose the Right Data Structure Every Time

What Are Collections?

Imagine you're organizing your closet. You might use different containers for different purposes - a drawer for socks, a shelf for shirts, and hooks for hats. Each container is optimized for its contents and how you access them.

Java Collections work the same way. They're containers for storing groups of objects, but each type is optimized for different use cases. Choosing the right collection can make your code 10x faster (or slower!).

List

Ordered collection with duplicates allowed. Like a playlist where order matters and you can have the same song twice.

Set

Unordered collection with NO duplicates. Like a guest list - each person appears only once.

Map

Key-value pairs. Like a phone book - look up a name (key) to get a number (value).

Queue

First-in-first-out (FIFO) collection. Like a line at a coffee shop - first person in line gets served first.

ArrayList: Your Go-To List

ArrayList is the most commonly used collection. It's like a dynamic array that grows automatically as you add elements. Use it when you need ordered data and fast access by index.

import java.util.ArrayList;
import java.util.List;

public class ArrayListExample {
    public static void main(String[] args) {
        // Create an ArrayList of Strings
        List<String> fruits = new ArrayList<>();

        // Adding elements
        fruits.add("Apple");
        fruits.add("Banana");
        fruits.add("Orange");
        fruits.add("Apple");  // Duplicates allowed!

        System.out.println(fruits);  // [Apple, Banana, Orange, Apple]

        // Access by index (0-based)
        String first = fruits.get(0);  // "Apple"
        String last = fruits.get(fruits.size() - 1);  // "Apple"

        // Update an element
        fruits.set(1, "Blueberry");  // Replace Banana

        // Check if element exists
        boolean hasOrange = fruits.contains("Orange");  // true

        // Find index of element
        int index = fruits.indexOf("Orange");  // 2

        // Remove elements
        fruits.remove("Orange");     // Remove by value
        fruits.remove(0);            // Remove by index

        // Get size
        int size = fruits.size();

        // Loop through elements
        for (String fruit : fruits) {
            System.out.println(fruit);
        }

        // Clear all elements
        fruits.clear();
        boolean isEmpty = fruits.isEmpty();  // true
    }
}

Real-World Example: Shopping Cart

public class ShoppingCart {
    private List<CartItem> items = new ArrayList<>();

    public void addItem(String product, double price) {
        items.add(new CartItem(product, price));
    }

    public void removeItem(String product) {
        items.removeIf(item -> item.getProduct().equals(product));
    }

    public double getTotal() {
        double total = 0;
        for (CartItem item : items) {
            total += item.getPrice();
        }
        return total;
    }

    public void displayCart() {
        System.out.println("Your Cart:");
        for (int i = 0; i < items.size(); i++) {
            System.out.println((i + 1) + ". " + items.get(i));
        }
        System.out.println("Total: $" + getTotal());
    }
}

// Usage
ShoppingCart cart = new ShoppingCart();
cart.addItem("Laptop", 999.99);
cart.addItem("Mouse", 29.99);
cart.addItem("Keyboard", 79.99);
cart.displayCart();

ArrayList Performance

  • get(index) - Very Fast (O(1))
  • add(element) - Fast (O(1) usually)
  • add(index, element) - Slow (O(n)) - shifts elements
  • remove(index) - Slow (O(n)) - shifts elements
  • contains() - Slow (O(n)) - checks every element

LinkedList: Fast Insertions & Deletions

LinkedList stores elements as nodes connected like a chain. Each node knows about its neighbors. It's slower for random access but faster for adding/removing at the beginning or middle.

import java.util.LinkedList;
import java.util.List;

public class LinkedListExample {
    public static void main(String[] args) {
        LinkedList<String> tasks = new LinkedList<>();

        // Same methods as ArrayList
        tasks.add("Task 1");
        tasks.add("Task 2");
        tasks.add("Task 3");

        // Special methods for beginning/end
        tasks.addFirst("Urgent Task");   // Add to front
        tasks.addLast("Optional Task");  // Add to end

        // Get first/last
        String first = tasks.getFirst();  // "Urgent Task"
        String last = tasks.getLast();    // "Optional Task"

        // Remove first/last (great for queues!)
        tasks.removeFirst();
        tasks.removeLast();

        // Peek without removing
        String peek = tasks.peek();  // Returns first, or null if empty
    }
}

ArrayList vs LinkedList: When to Use Which?

  • ArrayList - Reading data often, accessing by index, most general purposes
  • LinkedList - Frequently adding/removing from beginning, implementing queues/stacks

Rule of thumb: Use ArrayList by default. Switch to LinkedList only if you're doing lots of insertions at the start.

HashSet: Unique Elements Only

HashSet stores unique elements with no duplicates and no order. It's incredibly fast for checking if an element exists. Think of it as a bag where you can quickly check if something is inside.

import java.util.HashSet;
import java.util.Set;

public class HashSetExample {
    public static void main(String[] args) {
        Set<String> uniqueVisitors = new HashSet<>();

        // Adding elements
        uniqueVisitors.add("user123");
        uniqueVisitors.add("user456");
        uniqueVisitors.add("user123");  // Ignored - already exists!

        System.out.println(uniqueVisitors.size());  // 2, not 3

        // Super fast existence check
        boolean hasUser = uniqueVisitors.contains("user123");  // true

        // Remove element
        uniqueVisitors.remove("user456");

        // Loop through (order not guaranteed!)
        for (String user : uniqueVisitors) {
            System.out.println(user);
        }
    }
}

Real-World Example: Tracking Unique Emails

public class NewsletterService {
    private Set<String> subscribers = new HashSet<>();

    public boolean subscribe(String email) {
        // add() returns false if element already exists
        if (subscribers.add(email.toLowerCase())) {
            System.out.println(email + " subscribed successfully!");
            return true;
        } else {
            System.out.println(email + " is already subscribed.");
            return false;
        }
    }

    public void unsubscribe(String email) {
        subscribers.remove(email.toLowerCase());
    }

    public boolean isSubscribed(String email) {
        return subscribers.contains(email.toLowerCase());
    }

    public int getSubscriberCount() {
        return subscribers.size();
    }
}

// Usage
NewsletterService newsletter = new NewsletterService();
newsletter.subscribe("john@email.com");   // Subscribed!
newsletter.subscribe("JOHN@email.com");   // Already subscribed (case insensitive)
newsletter.subscribe("jane@email.com");   // Subscribed!
System.out.println(newsletter.getSubscriberCount());  // 2

Set Operations: Union, Intersection, Difference

Set<String> team1 = new HashSet<>(Arrays.asList("Alice", "Bob", "Charlie"));
Set<String> team2 = new HashSet<>(Arrays.asList("Bob", "Diana", "Eve"));

// Union: All members from both teams
Set<String> allMembers = new HashSet<>(team1);
allMembers.addAll(team2);
// [Alice, Bob, Charlie, Diana, Eve]

// Intersection: Members in BOTH teams
Set<String> inBothTeams = new HashSet<>(team1);
inBothTeams.retainAll(team2);
// [Bob]

// Difference: Members only in team1
Set<String> onlyInTeam1 = new HashSet<>(team1);
onlyInTeam1.removeAll(team2);
// [Alice, Charlie]

TreeSet: Sorted Unique Elements

TreeSet is like HashSet but keeps elements sorted. It's slightly slower than HashSet but useful when you need elements in order.

import java.util.TreeSet;
import java.util.Set;

public class TreeSetExample {
    public static void main(String[] args) {
        Set<Integer> scores = new TreeSet<>();

        scores.add(85);
        scores.add(92);
        scores.add(78);
        scores.add(92);  // Duplicate ignored
        scores.add(88);

        System.out.println(scores);  // [78, 85, 88, 92] - Sorted!

        // TreeSet special methods
        TreeSet<Integer> treeScores = (TreeSet<Integer>) scores;

        System.out.println(treeScores.first());   // 78 (lowest)
        System.out.println(treeScores.last());    // 92 (highest)
        System.out.println(treeScores.lower(85)); // 78 (less than 85)
        System.out.println(treeScores.higher(85));// 88 (greater than 85)
    }
}

HashMap: Key-Value Storage

HashMap is one of the most useful collections. It stores data as key-value pairs, like a dictionary where you look up a word (key) to find its definition (value). Keys must be unique, but values can repeat.

import java.util.HashMap;
import java.util.Map;

public class HashMapExample {
    public static void main(String[] args) {
        // Map from country code to country name
        Map<String, String> countries = new HashMap<>();

        // Adding entries
        countries.put("US", "United States");
        countries.put("IN", "India");
        countries.put("UK", "United Kingdom");
        countries.put("JP", "Japan");

        // Get value by key
        String country = countries.get("IN");  // "India"

        // Get with default if key doesn't exist
        String unknown = countries.getOrDefault("XX", "Unknown");

        // Check if key exists
        boolean hasUS = countries.containsKey("US");  // true

        // Check if value exists
        boolean hasIndia = countries.containsValue("India");  // true

        // Update a value
        countries.put("UK", "Great Britain");  // Replaces old value

        // Remove entry
        countries.remove("JP");

        // Get all keys
        Set<String> codes = countries.keySet();

        // Get all values
        Collection<String> names = countries.values();

        // Loop through entries
        for (Map.Entry<String, String> entry : countries.entrySet()) {
            System.out.println(entry.getKey() + " -> " + entry.getValue());
        }
    }
}

Real-World Example: Word Counter

public class WordCounter {

    public Map<String, Integer> countWords(String text) {
        Map<String, Integer> wordCount = new HashMap<>();

        String[] words = text.toLowerCase().split("\\s+");

        for (String word : words) {
            // Old way
            // if (wordCount.containsKey(word)) {
            //     wordCount.put(word, wordCount.get(word) + 1);
            // } else {
            //     wordCount.put(word, 1);
            // }

            // Modern way - much cleaner!
            wordCount.merge(word, 1, Integer::sum);
        }

        return wordCount;
    }

    public static void main(String[] args) {
        WordCounter counter = new WordCounter();
        String text = "Java is great and Java is powerful";

        Map<String, Integer> result = counter.countWords(text);
        System.out.println(result);
        // {java=2, is=2, great=1, and=1, powerful=1}
    }
}

Real-World Example: User Cache

public class UserCache {
    private Map<Long, User> cache = new HashMap<>();

    public User getUser(Long userId) {
        // Check cache first
        if (cache.containsKey(userId)) {
            System.out.println("Cache hit for user " + userId);
            return cache.get(userId);
        }

        // Not in cache, fetch from database
        System.out.println("Cache miss, fetching from DB...");
        User user = fetchFromDatabase(userId);

        // Store in cache for next time
        cache.put(userId, user);

        return user;
    }

    // Using computeIfAbsent (cleaner way)
    public User getUserV2(Long userId) {
        return cache.computeIfAbsent(userId, this::fetchFromDatabase);
    }

    private User fetchFromDatabase(Long userId) {
        // Simulate database call
        return new User(userId, "User" + userId);
    }
}

TreeMap: Sorted Keys

TreeMap is like HashMap but keeps keys sorted. Useful when you need to iterate through entries in a specific order.

import java.util.TreeMap;
import java.util.Map;

public class TreeMapExample {
    public static void main(String[] args) {
        Map<String, Double> prices = new TreeMap<>();

        prices.put("Banana", 1.50);
        prices.put("Apple", 2.00);
        prices.put("Orange", 1.75);
        prices.put("Cherry", 3.50);

        // Keys are automatically sorted alphabetically
        for (Map.Entry<String, Double> entry : prices.entrySet()) {
            System.out.println(entry.getKey() + ": $" + entry.getValue());
        }
        // Output:
        // Apple: $2.0
        // Banana: $1.5
        // Cherry: $3.5
        // Orange: $1.75

        TreeMap<String, Double> treeMap = (TreeMap<String, Double>) prices;

        // First and last entries
        System.out.println(treeMap.firstKey());   // Apple
        System.out.println(treeMap.lastKey());    // Orange
        System.out.println(treeMap.firstEntry()); // Apple=2.0
    }
}

Queue: First-In-First-Out

Queue is like a line at a store - the first person to join the line is the first to be served. Use it when you need to process items in the order they were added.

import java.util.LinkedList;
import java.util.Queue;

public class QueueExample {
    public static void main(String[] args) {
        Queue<String> printQueue = new LinkedList<>();

        // Add to queue (back of the line)
        printQueue.offer("Document1.pdf");
        printQueue.offer("Document2.pdf");
        printQueue.offer("Document3.pdf");

        // Peek at front without removing
        String next = printQueue.peek();  // "Document1.pdf"

        // Remove from front (process first in line)
        while (!printQueue.isEmpty()) {
            String document = printQueue.poll();
            System.out.println("Printing: " + document);
        }
        // Prints in order: Document1, Document2, Document3
    }
}

Real-World Example: Task Queue

public class TaskProcessor {
    private Queue<Runnable> taskQueue = new LinkedList<>();

    public void addTask(Runnable task) {
        taskQueue.offer(task);
        System.out.println("Task added. Queue size: " + taskQueue.size());
    }

    public void processTasks() {
        while (!taskQueue.isEmpty()) {
            Runnable task = taskQueue.poll();
            System.out.println("Processing task...");
            task.run();
        }
        System.out.println("All tasks completed!");
    }

    public static void main(String[] args) {
        TaskProcessor processor = new TaskProcessor();

        processor.addTask(() -> System.out.println("Send email"));
        processor.addTask(() -> System.out.println("Generate report"));
        processor.addTask(() -> System.out.println("Update database"));

        processor.processTasks();
    }
}

Deque: Double-Ended Queue

Deque (pronounced "deck") allows adding/removing from both ends. It can work as both a queue (FIFO) and a stack (LIFO).

import java.util.ArrayDeque;
import java.util.Deque;

public class DequeExample {
    public static void main(String[] args) {
        Deque<String> deque = new ArrayDeque<>();

        // Add to front or back
        deque.addFirst("First");
        deque.addLast("Last");

        // Use as a Stack (LIFO)
        Deque<String> browserHistory = new ArrayDeque<>();
        browserHistory.push("google.com");
        browserHistory.push("github.com");
        browserHistory.push("stackoverflow.com");

        // Go back
        String current = browserHistory.pop();  // stackoverflow.com
        current = browserHistory.pop();          // github.com
    }
}

Choosing the Right Collection

Quick Decision Guide

  • Need ordered items with duplicates? -> ArrayList
  • Frequently add/remove from beginning? -> LinkedList
  • Need unique items, don't care about order? -> HashSet
  • Need unique items in sorted order? -> TreeSet
  • Need key-value pairs? -> HashMap
  • Need sorted key-value pairs? -> TreeMap
  • Need FIFO processing? -> Queue (LinkedList)
  • Need stack (LIFO)? -> Deque (ArrayDeque)

Performance Comparison

// Performance: O(1) = instant, O(n) = depends on size, O(log n) = very fast

// ArrayList
get(index)    -> O(1)     // Very fast
add(end)      -> O(1)     // Fast
add(middle)   -> O(n)     // Slow
contains()    -> O(n)     // Slow

// HashSet / HashMap
add()         -> O(1)     // Very fast
contains()    -> O(1)     // Very fast - this is why we use them!
remove()      -> O(1)     // Very fast

// TreeSet / TreeMap
add()         -> O(log n) // Fast
contains()    -> O(log n) // Fast
Sorted!       -> Yes      // Elements are always ordered

// LinkedList
addFirst()    -> O(1)     // Very fast
addLast()     -> O(1)     // Very fast
get(index)    -> O(n)     // Slow - must traverse

Best Practices

1. Program to Interfaces

Declare variables using interface types, not implementation types:

// GOOD - flexible, can switch implementations
List<String> names = new ArrayList<>();
Set<Integer> ids = new HashSet<>();
Map<String, User> users = new HashMap<>();

// BAD - locked to specific implementation
ArrayList<String> names = new ArrayList<>();

2. Specify Initial Capacity for Large Collections

// If you know you'll have ~1000 items
List<User> users = new ArrayList<>(1000);
Map<String, Order> orders = new HashMap<>(1000);

// This prevents multiple internal array resizes

3. Use Diamond Operator

// GOOD - Java infers the type
List<String> names = new ArrayList<>();

// OLD way - redundant
List<String> names = new ArrayList<String>();

4. Use Immutable Collections When Possible

// Create immutable list (Java 9+)
List<String> colors = List.of("Red", "Green", "Blue");
Set<Integer> primes = Set.of(2, 3, 5, 7, 11);
Map<String, Integer> ages = Map.of("Alice", 30, "Bob", 25);

// These cannot be modified - safer code!

5. Choose the Right Collection for the Job

Using the wrong collection can make your code 100x slower. Always think about what operations you'll perform most frequently.

Master Java Collections with Expert Guidance

Learn to choose and use the right data structures for optimal performance in your applications.