Java Collection Framework: A Practical Guide with Examples
JAVA DEVELOPMENT · COLLECTIONS AND DATA STRUCTURES
Java Collection Framework: A Practical Guide with Examples
Understand List, Set, Queue, and Map—and choose the right collection for reliable, efficient Java applications.
Choosing the wrong collection can turn a simple lookup into repeated scanning, remove information you needed, or produce results in an unexpected order.
The Java Collection Framework, officially called the
Java Collections Framework, provides standard interfaces and
implementations for managing groups of objects. Understanding it
means knowing more than how to call add() and
remove().
This guide explains the main collection types, their performance characteristics, and how to choose between them. You will also learn practical techniques for removing duplicates, counting values, protecting collection contents, and handling shared data safely.
1. What is the Java Collection Framework?
The framework provides three main building blocks:
-
Interfaces, such as
List,Set, andMap, define behavior. -
Implementations, such as
ArrayList,HashSet, andHashMap, provide concrete data structures. - Algorithms and utilities support operations such as sorting, searching, and wrapping collections.
Most core types belong to java.util. Specialized
concurrent collections belong to java.util.concurrent.
Unlike arrays, which have a fixed length after creation, many collection implementations grow as elements are added. However, not every collection supports modification. Some are fixed-size or unmodifiable.
Collection vs. Collections vs. Map
| Name | Meaning | Example |
|---|---|---|
| Collection | Root interface for groups of individual elements | Collection<String> |
| Collections | Utility class with static helper methods | Collections.sort(names) |
| Map | Interface for key-value associations | Map<String, Integer> |
Remember: Map belongs to the framework but does
not extend Collection. It exposes collection views through
keySet(), values(), and
entrySet().
2. Core interfaces in the Java Collection Framework
List: Preserve position and allow duplicates
A List stores elements in a defined sequence and provides index-based access. Common implementations allow duplicate values.
Use a list when position matters, such as displaying search results or retaining imported records in their original order.
List<String> stores = new ArrayList<>(); // Create a resizable list.
stores.add("Delhi"); // Add the first store.
stores.add("Mumbai"); // Add the second store.
stores.add("Delhi"); // Keep the duplicate.
System.out.println(stores.get(0)); // Print Delhi.
ArrayList vs. LinkedList
ArrayList uses a resizable backing array. It provides constant-time indexed access and amortized constant-time appends. Inserting near the beginning usually requires shifting existing elements.
LinkedList requires traversal to reach an indexed position. Therefore, “linked lists are faster for insertions” is incomplete advice: locating the insertion point can dominate the operation.
For ordinary lists that are frequently read and appended to, start with ArrayList. Choose another implementation when your access pattern justifies it.
Set: Maintain unique elements
A Set represents distinct elements. The implementation determines ordering:
| Implementation | Ordering behavior | Typical purpose |
|---|---|---|
| HashSet | No guaranteed iteration order | Membership checks |
| LinkedHashSet | Insertion order | Deduplication while retaining order |
| TreeSet | Sorted order | Sorted unique values and range queries |
For example, preserve the first occurrence of each store:
List<String> stores = Arrays.asList("Delhi", "Mumbai", "Delhi"); // Input.
Set<String> unique = new LinkedHashSet<>(stores); // Remove duplicates.
System.out.println(unique); // [Delhi, Mumbai]
A TreeSet uses natural ordering or a supplied comparator. Its basic
operations take O(log n) time. Be careful: elements that compare
as zero are treated as duplicates, even if their
equals() results differ.
Queue and Deque: Process pending work
A Queue holds elements awaiting processing. Many queues use first-in, first-out ordering, but this is not universal: PriorityQueue selects elements according to priority.
A Deque supports insertion and removal at both ends. ArrayDeque is useful for local queues and stacks.
Deque<String> files = new ArrayDeque<>(); // Create a queue.
files.offerLast("cashsheet-001.csv"); // Add the first file.
files.offerLast("cashsheet-002.csv"); // Add the second file.
System.out.println(files.pollFirst()); // Process the first file.
pollFirst() returns null when empty.
removeFirst() throws an exception instead. Select the
method that matches how your application handles an empty queue.
Map: Retrieve values by key
A Map associates each unique key with a value. Values may repeat.
- HashMap: Lookup without an ordering requirement.
- LinkedHashMap: Predictable insertion-order iteration by default.
- TreeMap: Sorted keys.
Map<String, Integer> totals = new HashMap<>(); // Store counts by store.
totals.put("STORE-101", 12); // Add a mapping.
totals.put("STORE-101", 15); // Replace its value.
System.out.println(totals.get("STORE-101")); // Print 15.
HashMap provides expected constant-time get() and put() operations when hashes are distributed appropriately. It does not guarantee iteration order.
3. How to choose the right collection
Start with the operation your application performs most often.
| Requirement | Starting choice | Reason |
|---|---|---|
| Access elements by index | ArrayList | Constant-time indexed access |
| Check whether a value exists | HashSet | Expected constant-time membership checks |
| Remove duplicates while preserving order | LinkedHashSet | Uniqueness with insertion order |
| Retrieve a value using an identifier | HashMap | Efficient key-based lookup |
| Maintain sorted unique values | TreeSet | Ordering and navigation |
| Add and remove at both ends | ArrayDeque | Efficient endpoint operations |
| Share key-value data across threads | ConcurrentHashMap | Concurrent access support |
Performance also depends on memory, data size, hashing quality, and the mix of operations.
For example, repeatedly calling contains() on a large ArrayList scans its elements each time. Building a HashSet first can improve repeated membership checks, at the cost of extra storage.
4. Practical example: Count files by restaurant
Suppose a processing service receives restaurant identifiers from uploaded cash-sheet records. You need the number of records for each restaurant.
This Java 8-compatible example uses the Stream API:
import java.util.*; // Import collection types.
import java.util.stream.Collectors; // Import grouping utilities.
public class RestaurantCounts { // Define the executable class.
public static void main(String[] args) { // Start execution.
List<String> stores = Arrays.asList(
"R101", "R102", "R101", "R103", "R102"); // Input records.
Map<String, Long> counts = stores.stream() // Read each identifier.
.collect(Collectors.groupingBy(
store -> store, // Group by identifier.
LinkedHashMap::new, // Preserve first-seen order.
Collectors.counting())); // Count each group.
System.out.println(counts); // Display the grouped counts.
}
}
Expected output
{R101=2, R102=2, R103=1}
The flow is straightforward:
- Read each restaurant identifier.
- Find or create its group.
- Increase the group’s count.
- Return a map in first-seen order.
Under normal hashing assumptions, processing takes expected O(n) time and O(k) extra space, where k is the number of distinct restaurants.
The collection stores data; the stream describes how to process it. They serve different purposes.
5. Common mistakes and how to avoid them
Incorrect equality for custom objects
Hash-based collections depend on both equals() and hashCode().
If two objects are logically equal, they must return the same hash code. Different objects may still share a hash code; equality checks distinguish them.
Avoid changing fields used by these methods while an object is stored as a map key or set element. Otherwise, lookup may fail because its hash no longer matches its original storage location.
Confusing unmodifiable with deeply immutable
List.of(), introduced in Java 9, creates an unmodifiable list. Java 10 added List.copyOf().
List<String> markets = List.of("IN", "UK"); // Create an unmodifiable list.
These factories reject null elements. Adding, removing, or replacing elements is unsupported.
However, they do not freeze mutable objects stored inside the list. Protecting the list structure and protecting each element are separate concerns.
Removing elements during iteration
Directly removing elements from an ArrayList inside an enhanced for loop can cause ConcurrentModificationException.
For straightforward filtering, use:
stores.removeIf(String::isEmpty); // Remove empty strings from a mutable list.
This requires a list that supports removal. The fixed-size list returned by Arrays.asList() does not support structural changes.
Assuming thread safety covers every workflow
ArrayList and HashMap are not synchronized.
For shared counters, a ConcurrentHashMap can perform a per-key update atomically:
import java.util.concurrent.ConcurrentHashMap; // Import the concurrent map.
ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>(); // Shared map.
counts.merge("R101", 1, Integer::sum); // Atomically initialize or increment.
A separate get() followed by put() is not an atomic increment, even on a concurrent map. Also, thread-safe map operations do not automatically protect mutable objects stored as values.
6. Put collection selection into practice
Choose a collection by answering four questions: Do duplicates matter? Must order be preserved? How will elements be accessed? Will multiple threads modify the data?
Start with ArrayList, HashSet, or HashMap for common requirements. Move to ordered, sorted, or concurrent alternatives when needed. Then verify behavior with representative data, especially equality, ordering, modification support, and performance.
Comments
Post a Comment