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.