Java Collections Framework

In this post, we’ll explore the most common collections in Java, including when and why to use specific implementations.

Bryan Aguilar
8 min readOct 9, 2024

When working with data structures in Java, the Collections Framework plays a vital role in managing and manipulating groups of objects efficiently. Whether you’re organizing a list of items, storing key-value pairs, or maintaining unique elements, Java provides specialized interfaces like List Set Queueand Map to suit different use cases.

However, choosing the right implementation — such as ArrayList, HashSet, or PriorityQueue — can make a significant difference in how you handle data. Each collection type is optimized for particular operations, whether it’s quick lookups, maintaining sorted order, or ensuring uniqueness.

In this post, we’ll explore the most common collections in Java, including when and why to use specific implementations. Whether you’re new to Java or looking to deepen your understanding of collections, this guide will help you choose the right data structures and iterate over them efficiently.

In Java, the Collections Framework provides a set of interfaces and classes to store and manipulate data. The main collection types include:

List

Stores elements in an ordered sequence and allows duplicates. You can access elements by their index. Use when you need an ordered collection where duplicates are allowed, and you might need to access elements by index. Common implementations are:

ArrayList

A resizable array. Fast for random access, but slow for inserting or deleting elements in the middle.

Use Case: Storing a list of items that rarely change, but where you need fast access by index.

Example: A list of products displayed on an e-commerce page, where the list is fetched once and displayed (i.e., frequent lookups but rare insertions/removals).

List<String> products = new ArrayList<>();
products.add("Laptop");
products.add("Smartphone");
// Fast access by index
String firstProduct = products.get(0);

LinkedList

A doubly linked list. Efficient for inserting or deleting elements from any position, but slower for random access.

Use Case: When you need frequent additions/removals from both ends of the list (such as when managing a queue).

Example: Implementing a task scheduler where tasks are added to the end and removed from the front.

LinkedList<String> tasks = new LinkedList<>();
tasks.add("Task 1");
tasks.addFirst("Urgent Task");
tasks.removeLast(); // Efficient removal

Set

Collection that cannot contain duplicate elements and does not guarantee order (unless using a sorted variant). Use when you want to store unique elements and don’t need duplicate entries. Common implementations are:

HashSet

No guarantee of iteration order. Best for checking existence and avoiding duplicates.

Use Case: When you need to store unique elements and don’t care about the order.

Example: Storing a list of unique user IDs to quickly check if a user has already registered.

Set<String> userIds = new HashSet<>();
userIds.add("user123");
userIds.add("user456");
boolean isRegistered = userIds.contains("user123"); // Fast look-up

TreeSet

Elements are sorted based on natural order or a custom comparator.

Use Case: When you need to store unique elements in a sorted order.

Example: Maintaining a sorted list of scores in a leaderboard for a game.

Set<Integer> scores = new TreeSet<>();
scores.add(500);
scores.add(800);
scores.add(300);
System.out.println(scores); // Output: [300, 500, 800]

LinkedHashSet

Maintains the insertion order.

Use Case: When you need unique elements, but want to preserve the order in which they were added.

Example: Storing a set of user preferences where the insertion order matters.

Set<String> preferences = new LinkedHashSet<>();
preferences.add("Dark Mode");
preferences.add("Notifications");
System.out.println(preferences); // Output in insertion order

Queue

It is used to represent a collection designed for holding elements before processing. Usually operates in FIFO (First-In-First-Out) order. Use when you need to process elements in a specific order, typically for tasks like scheduling. Common implementations are:

PriorityQueue

Orders elements according to their natural order or a custom comparator (does not guarantee FIFO).

Use Case: When you need elements processed in a specific order, based on priority rather than just FIFO.

Example: Implementing a to-do list where tasks with higher priority (lower number) should be processed first.

Queue<Task> taskQueue = new PriorityQueue<>(Comparator.comparingInt(Task::getPriority));
taskQueue.add(new Task("Task 1", 3)); // Priority 3
taskQueue.add(new Task("Urgent Task", 1)); // Priority 1
Task nextTask = taskQueue.poll(); // Urgent Task gets processed first

LinkedList as a Queue

Can also be used as a Queue, implementing both List and Queue.

Use Case: When you need a simple FIFO queue.

Example: Handling customer service requests, where the first request in the list should be processed first.

Queue<String> serviceRequests = new LinkedList<>();
serviceRequests.add("Request 1");
serviceRequests.add("Request 2");
String nextRequest = serviceRequests.poll(); // Removes and returns "Request 1"

Map

It is a collection that maps keys to values. A key cannot be duplicated, but values can. Use when you want to store key-value pairs and need fast lookups by key. Common implementations are:

HashMap

Best for general-purpose key-value mapping with no guarantees about the order.

Use Case: Fast look-up of key-value pairs where order is not important.

Example: Storing user data (e.g., user IDs mapped to user profiles) in an application where the order of entries does not matter.

Map<String, UserProfile> userMap = new HashMap<>();
userMap.put("user123", new UserProfile("John Doe"));
UserProfile user = userMap.get("user123"); // Fast access by key

TreeMap

Maintains keys in sorted order.

Use Case: Maintaining sorted key-value pairs where you need fast access and sorted order.

Example: Storing items by their prices, where you want to retrieve items in price order.

Map<Double, String> priceToItemMap = new TreeMap<>();
priceToItemMap.put(19.99, "Book");
priceToItemMap.put(9.99, "Pen");
priceToItemMap.put(49.99, "Headphones");
System.out.println(priceToItemMap); // Output: {9.99=Pen, 19.99=Book, 49.99=Headphones}

LinkedHashMap

Maintains the order of insertion or access order.

Use Case: Fast access of key-value pairs with predictable iteration order (insertion or access order).

Example: Implementing a cache that removes the oldest entry when a certain size is reached (LRU cache).

Map<String, String> cache = new LinkedHashMap<>(16, 0.75f, true) {
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
return size() > 5; // Maximum of 5 elements
}
};
cache.put("a", "Value A");
cache.put("b", "Value B");

When to Use Specific Types

  • HashMap/HashSet: When you need fast access and don’t care about the order of elements.
  • TreeMap/TreeSet: When you need sorted data.
  • LinkedHashMap/LinkedHashSet: When you need to maintain the insertion order.
  • ArrayList: When you need fast random access and don’t frequently insert or remove elements in the middle.
  • LinkedList: When you need to frequently insert or remove elements from the list.

Iterator Overview

In Java, iterators provide a way to traverse through collections. Each collection type has its own way of iterating over its elements, but they all share common techniques. I’ll explain iterators and show you the best ways to loop through each type of collection.

An Iterator is an object that allows you to iterate over a collection, one element at a time, without needing to know how the collection is implemented. The Iterator interface provides three key methods:

  • hasNext(): Returns true if there are more elements to iterate.
  • next(): Returns the next element in the iteration.
  • remove(): Removes the last element returned by the iterator (optional operation).

List (ArrayList, LinkedList)

You can use an Iterator or enhanced for loop (foreach) to iterate through lists.

// Using iterator

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

Iterator<String> iterator = list.iterator();

while (iterator.hasNext()) {
String fruit = iterator.next();
System.out.println(fruit);
}

// Using Enhanced for Loop (foreach)

for (String fruit : list) {
System.out.println(fruit);
}

// Using forEach Method (Java 8+): With lambda expressions

list.forEach(fruit -> System.out.println(fruit));

Set (HashSet, TreeSet, LinkedHashSet)

Since Set collections do not have indexes, iterators are particularly useful here.

// using iterators

Set<String> set = new HashSet<>();

set.add("Apple");
set.add("Banana");
set.add("Cherry");

Iterator<String> iterator = set.iterator();

while (iterator.hasNext()) {
String fruit = iterator.next();
System.out.println(fruit);
}

// Using Enhanced for Loop (foreach)

for (String fruit : set) {
System.out.println(fruit);
}

// Using forEach Method (Java 8+)

set.forEach(fruit -> System.out.println(fruit));

Queue (PriorityQueue, LinkedList as a Queue)

Queues are often processed in FIFO order, and you can iterate over them using the same techniques.

// using iterators

Queue<String> queue = new LinkedList<>();

queue.add("Apple");
queue.add("Banana");
queue.add("Cherry");

Iterator<String> iterator = queue.iterator();

while (iterator.hasNext()) {
String fruit = iterator.next();
System.out.println(fruit);
}

// Using Enhanced for Loop (foreach)

for (String fruit : queue) {
System.out.println(fruit);
}

// Using forEach Method (Java 8+)

queue.forEach(fruit -> System.out.println(fruit));

Map (HashMap, TreeMap, LinkedHashMap)

Map collections do not directly implement Iterator. However, you can iterate over the key set, value set, or entry set.

// Using Iterator on Entry Set

Map<String, String> map = new HashMap<>();

map.put("1", "Apple");
map.put("2", "Banana");
map.put("3", "Cherry");

Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();

while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}

// Using Enhanced for Loop (foreach) on Entry Set

for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}

// Iterating Over Keys

for (String key : map.keySet()) {
System.out.println("Key: " + key + ", Value: " + map.get(key));
}

// Using forEach Method (Java 8+)

map.forEach((key, value) -> System.out.println("Key: " + key + ", Value: " + value));

When to Use Iterators

  • Remove Elements While Iterating: If you need to remove elements while looping, the best way is using an iterator and calling the remove() method. Removing elements inside an enhanced for loop will throw a ConcurrentModificationException.
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next();
if (fruit.equals("Banana")) {
iterator.remove(); // Safe removal
}
}
  • Custom Traversal Logic: If you need custom traversal logic (like skipping elements, handling duplicates, etc.), using an iterator provides more control than the enhanced for loop.

Best Ways to Loop Through Collections

  • For Lists: Use the enhanced for loop (clean and simple), unless you need index-based access, where a traditional for loop would be better.
  • For Sets: Use the enhanced for loop or iterator. Since Sets don’t have indices, these are your best options.
  • For Queues: The enhanced for loop or iterator works well to iterate over elements.
  • For Maps: Iterate over the entry set for both keys and values, either with an enhanced for loop or forEach.
  • Use iterators when: You need to remove elements while iterating or require fine-grained control.
  • Use enhanced for loop when: You just need to loop through the collection without modifying it.
  • Use forEach method (Java 8+): When you want concise syntax with lambda expressions.

Java’s Collections Framework offers a wide variety of data structures to efficiently store and manipulate data, each tailored to specific needs. Whether you require fast lookups with a HashMap, the uniqueness of a HashSet, or the flexibility of a LinkedList, understanding the strengths of each implementation helps you write optimized, maintainable code. Iterators add another layer of power, allowing you to traverse and modify collections in a controlled manner.

Don’t hesitate to dive into Java’s collections and iterators in your next project. Start experimenting with different structures to see how they can simplify your code and boost performance.

Don’t forget to visit my website.

Thank you for reading this article!

If you have any questions, don’t hesitate to ask me. My inbox will always be open. Whether you have a question or just want to say hello, I will do my best to answer it!

--

--

Bryan Aguilar

Senior Software Engineer · Systems Engineer · Full Stack Developer · Enthusiastic Data Analyst & Data Scientist | https://www.bryan-aguilar.com