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. Writingnew 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.
Line 8: We refer to
suppliesas aCollection. We instantiate it as anArrayList(a specific type of list).Lines 10–15: We use standard methods like
add,size, andcontainsto 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:
ListCommon 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:
SetCommon implementation:
HashSet(Uses hashing for fast uniqueness checks)
Let’s compare List and Set to see how they handle the same data differently.
Lines 1–4: We introduce
java.util.Listandjava.util.Set, the sub-interfaces. We also importjava.util.HashSet, a specificSetimplementation that uses hashing for fast lookups.Line 9: We instantiate a
ListusingArrayList.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
SetusingHashSet.Line 20: The
Setdetects 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.
Lines 1–4: We import
DequeandQueue(interfaces),ArrayDeque(an efficient resizable array implementation of Deque), andLinkedList(which implements both List and Queue).Line 10: We create a
QueueusingLinkedList. 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 returnsnullinstead of crashing.Line 19:
peek()looks at the next element without removing it.Line 23: We create a
DequeusingArrayDeque.Lines 26–27:
push()inserts an element at the head (front) of the deque. This is different fromadd();pushputs 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.
Lines 1–2: We import
java.util.Map, the interface for key-value mappings, andjava.util.HashMap, the most common implementation that pairs keys to values using a hash table.Line 7: We declare a
MapmappingStringkeys toIntegervalues.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.
Line 7:
<String>enforces thatuserNamescan 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
Stringwithout 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.