Search⌘ K
AI Features

Introduction to Collections

Explore the Java Collections Framework to understand how to store and manipulate dynamic groups of objects. Learn about core interfaces like List, Set, Queue, Deque, and Map, and how generics enforce type safety. Gain the ability to choose and use the right collection types for different data needs with a focus on coding to interfaces.

While arrays are fast and efficient, they have significant limitations. Arrays have a fixed size once created and lack built-in methods for common tasks such as searching, sorting, or removing elements. In real-world software, data is dynamic, users join and leave, shopping carts grow and shrink, and tasks queue up for processing.

To handle this dynamism without rewriting complex logic each time, Java provides the collections framework. This framework offers a unified architecture for storing and manipulating groups of objects, allowing us to focus on our application’s logic rather than the low-level details of data storage.

The purpose of the framework

The Java Collections Framework (JCF) is a set of interfaces and classes located in the java.util package. Its primary goal is to provide a standard way to handle groups of objects. Before the framework existed, developers often wrote their own ad-hoc classes to store data, making code difficult to share and reuse.

The framework is built around a hierarchy of interfaces. These interfaces define what a collection can do (add, remove, check size) without strictly defining how it does it. This abstraction allows us to swap different underlying implementations (like a linked list vs. a resizable array) with minimal code changes.

Here is the high-level structure of the framework:

Iterable <Interface>
└── Collection <Interface>
├── List <Interface> (Ordered, allows duplicates)
├── Set <Interface> (Unordered, unique elements)
└── Queue <Interface> (Processing order, e.g., FIFO)
Map <Interface> (Separate hierarchy for Key-Value pairs)

At the top of the hierarchy (for most types) sits the Collection interface, which extends Iterable. This means that most collections can be traversed using the enhanced for loop we learned earlier. Notice that Map sits apart from the rest; we will explain why shortly.

The Collection interface

The java.util.Collection interface is the root interface for the entire hierarchy, excluding maps. It defines the most common behaviors that all groups of objects share, regardless of whether they are ordered, sorted, or unique.

Because Collection is an interface, we cannot create an object of it directly (e.g., new Collection() is invalid). Instead, we use it as a reference type for concrete implementations like ArrayList or HashSet. This is a powerful application of polymorphism.

Interfaces vs. implementations

You will notice that we often import two things: an interface (like Collection or List) and an implementation (like ArrayList).

  • The interface (Collection, List): This acts like a contract. It says, I promise I can add, remove, and count items. However, it contains no code to actually do those things. Because it is abstract, you cannot create an instance of an interface. Writing new Collection() will cause a compilation error.

  • The implementation (ArrayList, HashSet): This is the actual class that contains the code. It fulfills the contract defined by the interface.

To create a collection, we use the interface as the variable type and the class as the object type.

Java 25
import java.util.ArrayList;
import java.util.Collection;
public class CollectionExample {
public static void main(String[] args) {
// <String> is a Generic. It tells the compiler this collection
// can ONLY hold Strings. This ensures type safety.
Collection<String> supplies = new ArrayList<>();
supplies.add("Notebook");
supplies.add("Pen");
// The interface guarantees these methods exist
System.out.println("Size: " + supplies.size());
System.out.println("Contains Pen? " + supplies.contains("Pen"));
}
}
  • Line 8: We refer to supplies as a Collection. We instantiate it as an ArrayList (a specific type of list).

  • Lines 10–15: We use standard methods like add, size, and contains to manipulate the data. These methods are guaranteed to exist because they are part of the interface contract.

  • Generics (<String>): This syntax ensures we don’t accidentally add an Integer to our String collection. It is a mandatory best practice in modern Java.

Note: In professional development, we prefer defining variables by their interface (e.g., Collection or List) rather than their implementation (ArrayList). This is called coding to an interface. This gives us the flexibility to swap the underlying data structure later without breaking our code.

Core Collection interfaces: List and Set

While Collection defines standard behavior, its sub-interfaces define more specific rules for how data is organized. The two most commonly used sub-interfaces are List and Set.

List

A List is an ordered collection (sometimes called a sequence). Lists allow duplicate elements and give us precise control over where each element is inserted. We can access elements by their integer index (position), similar to arrays.

  • Interface: List

  • Common implementation: ArrayList (Resizes dynamically like an array)

Set

A Set is a collection that cannot contain duplicate elements. It models the mathematical set abstraction. If we try to add a duplicate element to a Set, the operation simply fails (returns false) or ignores the addition. Sets generally do not guarantee a specific order of elements.

  • Interface: Set

  • Common implementation: HashSet (Uses hashing for fast uniqueness checks)

Let’s compare List and Set to see how they handle the same data differently.

Java 25
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class ListVsSet {
public static void main(String[] args) {
// List: Ordered, allows duplicates
List<String> shoppingList = new ArrayList<>();
shoppingList.add("Milk");
shoppingList.add("Bread");
shoppingList.add("Milk"); // Duplicate allowed
System.out.println("List: " + shoppingList);
// Set: Unordered, unique elements
Set<String> uniqueItems = new HashSet<>();
uniqueItems.add("Milk");
uniqueItems.add("Bread");
uniqueItems.add("Milk"); // Duplicate ignored
System.out.println("Set: " + uniqueItems);
}
}
  • Lines 1–4: We introduce java.util.List and java.util.Set, the sub-interfaces. We also import java.util.HashSet, a specific Set implementation that uses hashing for fast lookups.

  • Line 9: We instantiate a List using ArrayList.

  • Line 12: Adding “Milk” a second time works fine in a List.

  • Line 14: The output shows two entries for “Milk” in the insertion order.

  • Line 17: We instantiate a Set using HashSet.

  • Line 20: The Set detects that “Milk” is already present and silently ignores the addition to ensure uniqueness.

  • Line 22: The output shows only one “Milk”, which means duplicates were not added.

Processing order: Queue and Deque

Beyond simple storage, we often need collections that handle processing order, such as “First-In-First-Out” (FIFO) or “Last-In-First-Out” (LIFO). This is where Queue and Deque come in.

Queue

A Queue holds elements prior to processing. It typically orders elements in a FIFO manner, like a line at a grocery store. We add elements to the back and remove them from the front.

Deque

A Deque (Double Ended Queue, pronounced “deck”) is more flexible. It supports inserting and removing elements at both ends. This allows it to function as both a standard queue (FIFO) and a stack (LIFO).

Here is how we use them to manage tasks.

Java 25
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Queue;
public class QueueAndDeque {
public static void main(String[] args) {
// Queue: First-In-First-Out (FIFO)
// LinkedList implements Queue, so we can use it here
Queue<String> printJobs = new LinkedList<>();
// add() puts elements at the back of the line
printJobs.add("Report.pdf");
printJobs.add("Photo.png");
// poll() removes the head (first item) of the line
String currentJob = printJobs.poll();
System.out.println("Printing: " + currentJob);
System.out.println("Next job: " + printJobs.peek());
// Deque: LIFO Stack behavior (Last-In-First-Out)
// ArrayDeque is an efficient Deque implementation
Deque<String> browserHistory = new ArrayDeque<>();
// push() adds elements to the FRONT (top of stack)
browserHistory.push("Home Page");
browserHistory.push("Profile Page");
// pop() removes the FRONT element (most recently pushed)
System.out.println("Going back from: " + browserHistory.pop());
System.out.println("Now at: " + browserHistory.peek());
}
}
  • Lines 1–4: We import Deque and Queue (interfaces), ArrayDeque (an efficient resizable array implementation of Deque), and LinkedList (which implements both List and Queue).

  • Line 10: We create a Queue using LinkedList. Elements are processed in the order they arrive.

  • Lines 13–14: add() inserts an element at the tail (end) of the queue. “Report.pdf” is first, so “Photo.png” queues up behind it.

  • Line 17: poll() retrieves and removes the head (first element) of the queue. If the queue is empty, it returns null instead of crashing.

  • Line 19: peek() looks at the next element without removing it.

  • Line 23: We create a Deque using ArrayDeque.

  • Lines 26–27: push() inserts an element at the head (front) of the deque. This is different from add(); push puts the new item first, pushing previous items down.

  • Line 30: pop() removes and returns the element most recently added (LIFO behavior).

The Map interface

As shown in our hierarchy diagram, the Map interface is unique because it does not extend Collection. Instead of storing single elements, a Map stores key-value pairs.

Each key maps to exactly one value. Keys must be unique; duplicate keys are not allowed, but values can be duplicated. Maps are essential for data structures such as dictionaries, caches, and settings registries, where we look up data by a unique identifier.

Since Map is not a Collection, we do not use methods like add(). Instead, we use put() to store associations and get() to retrieve them.

Java 25
import java.util.HashMap;
import java.util.Map;
public class MapExample {
public static void main(String[] args) {
// Map: Stores Key -> Value pairs
Map<String, Integer> scores = new HashMap<>();
// Associating keys with values
scores.put("Alice", 90);
scores.put("Bob", 85);
// Updating a value (Alice's key already exists)
scores.put("Alice", 95);
// Retrieving values
System.out.println("Alice's Score: " + scores.get("Alice"));
System.out.println("Charlie's Score: " + scores.get("Charlie")); // null
System.out.println("Total entries: " + scores.size());
}
}
  • Lines 1–2: We import java.util.Map, the interface for key-value mappings, and java.util.HashMap, the most common implementation that pairs keys to values using a hash table.

  • Line 7: We declare a Map mapping String keys to Integer values.

  • Lines 10–11: put() adds new entries.

  • Line 14: Calling put() with an existing key (“Alice”) overwrites the old value (90 becomes 95).

  • Line 17: get("Alice") returns the associated value, 95.

  • Line 18: Accessing a missing key returns null.

  • Line 20: The size() method counts the number of key-value pairs stored in the map.

Type safety with generics

In all our examples, we used angle brackets like <String> or <String, Integer>. These are generics. They tell the compiler exactly what type of objects the collection will hold.

If we omit the generics (which was common in very old Java versions), the collection defaults to holding Object types. This is dangerous because we could accidentally mix incompatible types (like adding a String and an Integer to the same list), leading to errors when we try to use the data later.

Generics enforce type safety. If we define a List<String>, the compiler will trigger an error if we try to add an integer to it.

Java 25
import java.util.ArrayList;
import java.util.List;
public class GenericsExample {
public static void main(String[] args) {
// Type-safe list: Only Strings allowed
List<String> userNames = new ArrayList<>();
userNames.add("Sarah");
// userNames.add(101); // COMPILER ERROR: Incompatible types
String name = userNames.get(0); // No casting needed
System.out.println("User: " + name);
}
}
  • Line 7: <String> enforces that userNames can only store strings.

  • Line 10: Uncommenting this line would stop the code from compiling, protecting us from runtime errors.

  • Line 12: Because the list is generic, we can retrieve the item directly as a String without explicit casting.

We have now established the high-level map of the Java Collections Framework. We understand that Collection is the root for groups of objects, while Map manages key-value pairs. We also know that interfaces like List, Set, Queue, and Deque dictate specific rules for storage and retrieval.