Java Collections
Introduction
The Java Collections Framework is a unified architecture for representing and manipulating collections of objects. It contains interfaces, implementations, and algorithms that allow you to store, retrieve, process, and manipulate groups of objects efficiently. Understanding collections is essential for Java development and particularly important when working with Spring Framework, as Spring heavily utilizes collections for dependency injection, configuration, and data management.
In this guide, we'll explore the core collection interfaces and their implementations, common operations, and practical examples that demonstrate how collections are used in real-world applications.
Collection Hierarchy
The Java Collections Framework is organized into a hierarchy of interfaces and their implementations:
Collection (interface)
├── List (interface)
│ ├── ArrayList
│ ├── LinkedList
│ └── Vector (legacy)
├── Set (interface)
│ ├── HashSet
│ ├── LinkedHashSet
│ └── TreeSet
└── Queue (interface)
├── PriorityQueue
└── LinkedList
Additionally, the Map
interface represents a separate collection type that maps keys to values:
Map (interface)
├── HashMap
├── LinkedHashMap
├── TreeMap
└── Hashtable (legacy)
Core Interfaces
Let's examine the fundamental interfaces in the Collections Framework:
Collection Interface
The Collection
interface is the root of the collection hierarchy. It defines the basic methods that all collections should implement:
add()
- Adds an element to the collectionremove()
- Removes an element from the collectioncontains()
- Checks if the collection contains a specific elementsize()
- Returns the number of elements in the collectionisEmpty()
- Checks if the collection is emptyiterator()
- Returns an iterator over the collection elements
List Interface
A List
is an ordered collection that allows duplicate elements. Lists maintain insertion order and provide positional access to elements.
Set Interface
A Set
is a collection that cannot contain duplicate elements. Sets model the mathematical set abstraction and are useful for ensuring uniqueness.
Queue Interface
A Queue
represents a collection designed for holding elements prior to processing. Queues typically follow a FIFO (First-In-First-Out) order.
Map Interface
A Map
maps keys to values. It cannot contain duplicate keys, and each key can map to at most one value.
Common Implementations
ArrayList
ArrayList
is a resizable array implementation of the List
interface. It provides fast random access and is generally the default choice for lists.
import java.util.ArrayList;
import java.util.List;
public class ArrayListExample {
public static void main(String[] args) {
// Creating an ArrayList
List<String> fruits = new ArrayList<>();
// Adding elements
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// Displaying the list
System.out.println("Fruits list: " + fruits);
// Accessing elements by index
System.out.println("First fruit: " + fruits.get(0));
// Modifying elements
fruits.set(1, "Mango");
System.out.println("After modification: " + fruits);
// Removing elements
fruits.remove("Orange");
System.out.println("After removal: " + fruits);
// Checking size
System.out.println("Size of list: " + fruits.size());
}
}
Output:
Fruits list: [Apple, Banana, Orange]
First fruit: Apple
After modification: [Apple, Mango, Orange]
After removal: [Apple, Mango]
Size of list: 2
LinkedList
LinkedList
implements both List
and Queue
interfaces. It's implemented as a doubly-linked list and is efficient for insertions and deletions.
import java.util.LinkedList;
public class LinkedListExample {
public static void main(String[] args) {
LinkedList<String> tasks = new LinkedList<>();
// Adding elements
tasks.add("Send email");
tasks.addFirst("Review code"); // Adds at the beginning
tasks.addLast("Deploy application"); // Adds at the end
System.out.println("Tasks: " + tasks);
// Using LinkedList as a Queue
tasks.offer("Write documentation"); // Adds to the end
System.out.println("First task: " + tasks.peek()); // Views the first element
String completedTask = tasks.poll(); // Removes and returns the first element
System.out.println("Completed task: " + completedTask);
System.out.println("Remaining tasks: " + tasks);
}
}
Output:
Tasks: [Review code, Send email, Deploy application]
First task: Review code
Completed task: Review code
Remaining tasks: [Send email, Deploy application, Write documentation]
HashSet
HashSet
implements the Set
interface using a hash table. It doesn't guarantee any specific order of elements but provides constant time performance for basic operations.
import java.util.HashSet;
import java.util.Set;
public class HashSetExample {
public static void main(String[] args) {
Set<String> uniqueColors = new HashSet<>();
// Adding elements
uniqueColors.add("Red");
uniqueColors.add("Green");
uniqueColors.add("Blue");
uniqueColors.add("Red"); // Duplicate - will be ignored
System.out.println("Unique colors: " + uniqueColors);
System.out.println("Contains Blue? " + uniqueColors.contains("Blue"));
// Removing elements
uniqueColors.remove("Green");
System.out.println("After removal: " + uniqueColors);
}
}
Output:
Unique colors: [Red, Green, Blue]
Contains Blue? true
After removal: [Red, Blue]
TreeSet
TreeSet
implements SortedSet
interface and keeps elements sorted according to their natural order or a specified comparator.
import java.util.TreeSet;
import java.util.Set;
public class TreeSetExample {
public static void main(String[] args) {
Set<String> sortedNames = new TreeSet<>();
// Adding elements (will be automatically sorted)
sortedNames.add("Charlie");
sortedNames.add("Alice");
sortedNames.add("Bob");
sortedNames.add("David");
System.out.println("Sorted names: " + sortedNames);
// TreeSet with custom ordering using Comparator
Set<Integer> descendingNumbers = new TreeSet<>((a, b) -> b - a);
descendingNumbers.add(5);
descendingNumbers.add(2);
descendingNumbers.add(8);
descendingNumbers.add(1);
System.out.println("Descending numbers: " + descendingNumbers);
}
}
Output:
Sorted names: [Alice, Bob, Charlie, David]
Descending numbers: [8, 5, 2, 1]
HashMap
HashMap
implements the Map
interface using a hash table and provides constant-time performance for basic operations.
import java.util.HashMap;
import java.util.Map;
public class HashMapExample {
public static void main(String[] args) {
Map<String, Integer> userScores = new HashMap<>();
// Adding key-value pairs
userScores.put("Alice", 95);
userScores.put("Bob", 87);
userScores.put("Charlie", 91);
System.out.println("User scores: " + userScores);
// Getting values by key
System.out.println("Bob's score: " + userScores.get("Bob"));
// Modifying values
userScores.put("Bob", 92); // Updates existing value
System.out.println("Bob's updated score: " + userScores.get("Bob"));
// Checking if a key exists
System.out.println("Contains David? " + userScores.containsKey("David"));
// Iterating through key-value pairs
System.out.println("All scores:");
for (Map.Entry<String, Integer> entry : userScores.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
Output:
User scores: {Bob=87, Alice=95, Charlie=91}
Bob's score: 87
Bob's updated score: 92
Contains David? false
All scores:
Bob: 92
Alice: 95
Charlie: 91
Common Collection Operations
Sorting Collections
The Collections
utility class provides methods to sort and manipulate collections:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SortingExample {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("David");
names.add("Alice");
names.add("Charlie");
names.add("Bob");
System.out.println("Original list: " + names);
// Sorting with natural order
Collections.sort(names);
System.out.println("Sorted list: " + names);
// Sorting in reverse order
Collections.reverse(names);
System.out.println("Reverse sorted: " + names);
// Shuffling the list
Collections.shuffle(names);
System.out.println("Shuffled list: " + names);
}
}
Output:
Original list: [David, Alice, Charlie, Bob]
Sorted list: [Alice, Bob, Charlie, David]
Reverse sorted: [David, Charlie, Bob, Alice]
Shuffled list: [Bob, David, Alice, Charlie]
Converting Between Collection Types
You can easily convert between different collection types:
import java.util.*;
public class CollectionConversionExample {
public static void main(String[] args) {
// Creating a list
List<String> fruitsList = new ArrayList<>();
fruitsList.add("Apple");
fruitsList.add("Banana");
fruitsList.add("Apple"); // Duplicate
System.out.println("List: " + fruitsList);
// Converting list to set (removes duplicates)
Set<String> fruitsSet = new HashSet<>(fruitsList);
System.out.println("Set: " + fruitsSet);
// Converting set to list
List<String> uniqueList = new ArrayList<>(fruitsSet);
System.out.println("List from Set: " + uniqueList);
// Converting to array
String[] fruitsArray = uniqueList.toArray(new String[0]);
System.out.println("Array: " + Arrays.toString(fruitsArray));
// Converting array to list
List<String> listFromArray = Arrays.asList(fruitsArray);
System.out.println("List from Array: " + listFromArray);
}
}
Output:
List: [Apple, Banana, Apple]
Set: [Apple, Banana]
List from Set: [Apple, Banana]
Array: [Apple, Banana]
List from Array: [Apple, Banana]
Practical Examples
Managing a Task List
This example demonstrates how to use collections to create a simple task management application:
import java.util.*;
class Task {
private String id;
private String description;
private boolean completed;
public Task(String id, String description) {
this.id = id;
this.description = description;
this.completed = false;
}
public String getId() { return id; }
public String getDescription() { return description; }
public boolean isCompleted() { return completed; }
public void setCompleted(boolean completed) { this.completed = completed; }
@Override
public String toString() {
return String.format("[%s] %s - %s",
id,
description,
completed ? "Completed" : "Pending");
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Task task = (Task) o;
return Objects.equals(id, task.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
public class TaskManagerExample {
public static void main(String[] args) {
// Using a Map to store tasks by ID
Map<String, Task> tasks = new HashMap<>();
// Adding tasks
tasks.put("T1", new Task("T1", "Complete Java Collections tutorial"));
tasks.put("T2", new Task("T2", "Prepare for Spring Boot course"));
tasks.put("T3", new Task("T3", "Review pull requests"));
System.out.println("All Tasks:");
for (Task task : tasks.values()) {
System.out.println(task);
}
// Marking a task as completed
Task task = tasks.get("T1");
if (task != null) {
task.setCompleted(true);
System.out.println("\nAfter completing task T1:");
System.out.println(task);
}
// Creating lists of completed and pending tasks
List<Task> completedTasks = new ArrayList<>();
List<Task> pendingTasks = new ArrayList<>();
for (Task t : tasks.values()) {
if (t.isCompleted()) {
completedTasks.add(t);
} else {
pendingTasks.add(t);
}
}
System.out.println("\nCompleted Tasks:");
for (Task t : completedTasks) {
System.out.println(t);
}
System.out.println("\nPending Tasks:");
for (Task t : pendingTasks) {
System.out.println(t);
}
}
}
Output:
All Tasks:
[T1] Complete Java Collections tutorial - Pending
[T2] Prepare for Spring Boot course - Pending
[T3] Review pull requests - Pending
After completing task T1:
[T1] Complete Java Collections tutorial - Completed
Completed Tasks:
[T1] Complete Java Collections tutorial - Completed
Pending Tasks:
[T2] Prepare for Spring Boot course - Pending
[T3] Review pull requests - Pending
Building a Simple Cache with Collections
Here's an example of a simple cache implementation using Java Collections:
import java.util.*;
class LRUCache<K, V> {
private final int capacity;
private final Map<K, V> cache;
public LRUCache(int capacity) {
this.capacity = capacity;
// LinkedHashMap with access order mode
this.cache = new LinkedHashMap<K, V>(capacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
};
}
public V get(K key) {
return cache.get(key);
}
public void put(K key, V value) {
cache.put(key, value);
}
@Override
public String toString() {
return cache.toString();
}
}
public class CacheExample {
public static void main(String[] args) {
// Create a cache with capacity 3
LRUCache<String, String> userCache = new LRUCache<>(3);
// Adding items to cache
System.out.println("Adding items to cache...");
userCache.put("user1", "John Doe");
userCache.put("user2", "Jane Smith");
userCache.put("user3", "Bob Johnson");
System.out.println("Cache: " + userCache);
// Accessing an item (moves it to the end - most recently used)
System.out.println("\nAccessing user1...");
System.out.println("user1: " + userCache.get("user1"));
System.out.println("Cache: " + userCache);
// Adding new item that exceeds capacity
System.out.println("\nAdding user4 (exceeds capacity)...");
userCache.put("user4", "Alice Brown");
System.out.println("Cache: " + userCache);
// user2 was the least recently used, so it was evicted
System.out.println("\nTrying to access user2 (should be evicted)...");
System.out.println("user2: " + userCache.get("user2"));
}
}
Output:
Adding items to cache...
Cache: {user1=John Doe, user2=Jane Smith, user3=Bob Johnson}
Accessing user1...
user1: John Doe
Cache: {user2=Jane Smith, user3=Bob Johnson, user1=John Doe}
Adding user4 (exceeds capacity)...
Cache: {user3=Bob Johnson, user1=John Doe, user4=Alice Brown}
Trying to access user2 (should be evicted)...
user2: null
Collection Interfaces in Spring Framework
Spring extensively uses Java Collections in its core functionality:
-
Dependency Injection: Spring uses collections to inject multiple beans:
java@Autowired
private List<UserService> userServices; -
Configuration Properties: Spring Boot uses collections to configure multiple values:
java@ConfigurationProperties(prefix = "app")
public class AppProperties {
private List<String> allowedOrigins;
private Map<String, String> featureFlags;
// getters and setters
} -
Database Operations: Spring Data repositories return collection types:
javapublic interface UserRepository extends JpaRepository<User, Long> {
List<User> findByLastName(String lastName);
Set<User> findByActiveTrue();
}
Best Practices
-
Choose the right collection type:
- For ordered data with duplicates, use
List
- For unique elements, use
Set
- For key-value pairs, use
Map
- For ordered data with duplicates, use
-
Consider thread safety: Standard collections are not thread-safe. For concurrent access, use classes from the
java.util.concurrent
package likeConcurrentHashMap
orCopyOnWriteArrayList
. -
Use generics: Always specify the type parameter for type-safety:
List<String>
instead of rawList
. -
Use interfaces as variable types: Declare variables with interface types for flexibility:
List<String>
instead ofArrayList<String>
. -
Be cautious with modification: Some collection views and utility methods (like
Arrays.asList()
) return unmodifiable or fixed-size collections. -
Mind your performance: Different collection implementations have different performance characteristics:
HashMap
- O(1) for basic operationsTreeMap
- O(log n) for basic operationsArrayList
- O(1) for random access, O(n) for insertions/deletions at arbitrary positionsLinkedList
- O(1) for insertions/deletions at ends, O(n) for random access
Summary
Java Collections Framework provides a robust set of interfaces and implementations for storing and processing collections of objects. Understanding how to use the different collection types effectively is essential for Java development and particularly important when working with the Spring Framework.
In this guide, we explored:
- The core collection interfaces:
List
,Set
,Queue
, andMap
- Common implementations like
ArrayList
,HashSet
, andHashMap
- Operations for sorting, searching, and converting collections
- Practical examples of collections in real applications
- How Spring Framework utilizes collections
By mastering Java Collections, you'll be able to write more efficient and cleaner code, which is especially valuable when developing Spring applications.
Additional Resources
- Java Collections Framework Documentation
- Baeldung - Java Collections Guide
- Java Collection Cheat Sheet
Exercises
- Create a program that reads a text file and counts the frequency of each word using appropriate collections.
- Implement a simple course registration system where students can register for courses, using collections to track enrollments.
- Write a method that takes a list of integers and returns a new list containing only the unique even numbers, sorted in descending order.
- Create a custom implementation of a stack using an ArrayList.
- Build a simple contact management application that allows users to add, remove, and search for contacts using appropriate collections.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)