In your Java applications, you’ll typically work with various types of objects. And you might want to perform operations like sorting, searching, and iterating on these objects.
Prior to the introduction of the Collections framework in JDK 1.2, you would’ve used Arrays and Vectors to store and manage a group of objects. But they had their own share of drawbacks.
The Java Collections Framework aims to overcome these issues by providing high-performance implementations of common data structures. These allow you to focus on writing the application logic instead of focusing on low-level operations.
Then, the introduction of Generics in JDK 1.5 significantly improved the Java Collections Framework. Generics let you enforce type safety for objects stored in a collection, which enhances the robustness of your applications. You can read more about Java Generics here.
In this article, I will guide you through how to use the Java Collections Framework. We’ll discuss the different types of collections, such as Lists, Sets, Queues, and Maps. I’ll also provide a brief explanation of their key characteristics such as:
-
Internal mechanisms
-
Handling of duplicates
-
Support for null values
-
Ordering
-
Synchronization
-
Performance
-
Key methods
-
Common implementations
We’ll also walk through some code examples for better understanding, and I’ll touch on the Collections utility class and its usage.
Table of Contents:
Understanding the Java Collections Framework
According to the Java documentation, “A collection is an object that represents a group of objects. A collections framework is a unified architecture for representing and manipulating collections.”
In simple terms, the Java Collections Framework helps you manage a group of objects and perform operations on them efficiently and in an organized way. It makes it easier to develop applications by offering various methods to handle groups of objects. You can add, remove, search, and sort objects effectively using the Java Collections Framework.
Collection Interfaces
In Java, an interface specifies a contract that must be fulfilled by any class that implements it. This means the implementing class must provide concrete implementations for all the methods declared in the interface.
In the Java Collections Framework, various collection interfaces like Set
, List
, and Queue
extend the Collection
interface, and they must adhere to the contract defined by the Collection
interface.
Decoding the Java Collections Framework Hierarchy
Check out this neat diagram from this article that illustrates the Java Collection Hierarchy:
We’ll start from the top and work down so you can understand what this diagram is showing:
-
At the root of the Java Collections Framework is the
Iterable
interface, which lets you iterate over the elements of a collection. -
The
Collection
interface extends theIterable
interface. This means it inherits the properties and behavior of theIterable
interface and adds its own behavior for adding, removing, and retrieving elements. -
Specific interfaces such as
List
,Set
, andQueue
further extend theCollection
interface. Each of these interfaces has other classes implementing their methods. For example,ArrayList
is a popular implementation of theList
interface,HashSet
implements theSet
interface, and so on. -
The
Map
interface is part of the Java Collections Framework, but it does not extend theCollection
interface, unlike the others mentioned above. -
All the interfaces and classes in this framework are part of the
java.util
package.
Note: A common source of confusion in the Java Collections Framework revolves around the difference between Collection
and Collections
. Collection
is an interface in the framework, while Collections
is a utility class. The Collections
class provides static methods that perform operations on the elements of a collection.
Java Collection Interfaces
By now, you’re familiar with the different types of collections that form the foundation of the collections framework. Now we’ll take a closer look at the List
, Set
, Queue
, and Map
interfaces.
In this section, we’ll discuss each of these interfaces while exploring their internal mechanisms. We’ll examine how they handle duplicate elements and whether they support the insertion of null values. We’ll also understand the ordering of elements during insertion and their support for synchronization, which deals with the concept of thread safety. Then we’ll walk through a few key methods of these interfaces and conclude by reviewing common implementations and their performance for various operations.
Before we begin, let’s talk briefly about Synchronization and Performance.
-
Synchronization controls access to shared objects by multiple threads, ensuring their integrity and preventing conflicts. This is crucial for maintaining thread safety.
-
When choosing a collection type, one important factor is its performance during common operations like insertion, deletion, and retrieval. Performance is usually expressed using Big-O notation. You can learn more about it here.
Lists
A List
is an ordered or sequential collection of elements. It follows zero-based indexing, allowing the elements to be inserted, removed, or accessed using their index position.
-
Internal mechanism: A
List
is internally supported by either an array or a linked list, depending on the type of implementation. For example, anArrayList
uses an array, while aLinkedList
uses a linked list internally. You can read more aboutLinkedList
here. AList
dynamically resizes itself upon the addition or removal of elements. The indexing-based retrieval makes it a very efficient type of collection. -
Duplicates: Duplicate elements are allowed in a
List
, which means there can be more than one element in aList
with the same value. Any value can be retrieved based on the index at which it is stored. -
Null: Null values are also allowed in a
List
. Since duplicates are permitted, you can also have multiple null elements. -
Ordering: A
List
maintains insertion order, meaning the elements are stored in the same order they are added. This is helpful when you want to retrieve elements in the exact order they were inserted. -
Synchronization: A
List
is not synchronized by default, which means it doesn’t have a built-in way to handle access by multiple threads at the same time. -
Key methods: Here are some key methods of a
List
interface:add(E element)
,get(int index)
,set(int index, E element)
,remove(int index)
, andsize()
. Let’s look at how to use these methods with an example program.import java.util.ArrayList; import java.util.List; public class ListExample { public static void main(String[] args) { // Create a list List<String> list = new ArrayList<>(); // add(E element) list.add("Apple"); list.add("Banana"); list.add("Cherry"); // get(int index) String secondElement = list.get(1); // "Banana" // set(int index, E element) list.set(1, "Blueberry"); // remove(int index) list.remove(0); // Removes "Apple" // size() int size = list.size(); // 2 // Print the list System.out.println(list); // Output: [Blueberry, Cherry] // Print the size of the list System.out.println(size); // Output: 2 } }
-
Common implementations:
ArrayList
,LinkedList
,Vector
,Stack
-
Performance: Typically, insert and delete operations are fast in both
ArrayList
andLinkedList
. But fetching elements can be slow because you have to traverse through the nodes.
Operation | ArrayList | LinkedList |
Insertion | Fast at the end – O(1) amortized, slow at the beginning or middle- O(n) | Fast at the beginning or middle – O(1), slow at the end – O(n) |
Deletion | Fast at the end – O(1) amortized, slow at the beginning or middle- O(n) | Fast – O(1) if position is known |
Retrieval | Fast – O(1) for random access | Slow – O(n) for random access, as it involves traversing |
Sets
A Set
is a type of collection that does not allow duplicate elements and represents the concept of a mathematical set.
-
Internal mechanism: A
Set
is internally backed by aHashMap
. Depending on the implementation type, it is supported by either aHashMap
,LinkedHashMap
, or aTreeMap
. I have written a detailed article about howHashMap
works internally here. Be sure to check it out. -
Duplicates: Since a
Set
represents the concept of a mathematical set, duplicate elements are not allowed. This ensures that all elements are unique, maintaining the integrity of the collection. -
Null: A maximum of one null value is allowed in a
Set
because duplicates are not permitted. But this does not apply to theTreeSet
implementation, where null values are not allowed at all. -
Ordering: Ordering of elements in a
Set
depends on the type of implementation.-
HashSet
: Order is not guaranteed, and elements can be placed in any position. -
LinkedHashSet
: This implementation maintains the insertion order, so you can retrieve the elements in the same order they were inserted. -
TreeSet
: Elements are inserted based on their natural order. Alternatively, you can control the insertion order by specifying a custom comparator.
-
-
Synchronization: A
Set
is not synchronized, meaning you might encounter concurrency issues, like race conditions, which can affect data integrity if two or more threads try to access aSet
object simultaneously -
Key methods: Here are some key methods of a
Set
interface:add(E element)
,remove(Object o)
,contains(Object o)
, andsize()
. Let’s look at how to use these methods with an example program.import java.util.HashSet; import java.util.Set; public class SetExample { public static void main(String[] args) { // Create a set Set<String> set = new HashSet<>(); // Add elements to the set set.add("Apple"); set.add("Banana"); set.add("Cherry"); // Remove an element from the set set.remove("Banana"); // Check if the set contains an element boolean containsApple = set.contains("Apple"); System.out.println("Contains Apple: " + containsApple); // Get the size of the set int size = set.size(); System.out.println("Size of the set: " + size); } }
-
Common implementations:
HashSet
,LinkedHashSet
,TreeSet
-
Performance:
Set
implementations offer fast performance for basic operations, except for aTreeSet
, where the performance can be relatively slower because the internal data structure involves sorting the elements during these operations.
Operation | HashSet | LinkedHashSet | TreeSet |
Insertion | Fast – O(1) | Fast – O(1) | Slower – O(log n) |
Deletion | Fast – O(1) | Fast – O(1) | Slower – O(log n) |
Retrieval | Fast – O(1) | Fast – O(1) | Slower – O(log n) |
Queues
A Queue
is a linear collection of elements used to hold multiple items before processing, usually following the FIFO (first-in-first-out) order. This means elements are added at one end and removed from the other, so the first element added to the queue is the first one removed.
-
Internal mechanism: The internal workings of a
Queue
can differ based on its specific implementation.-
LinkedList
– uses a doubly-linked list to store elements, which means you can traverse both forward and backward, allowing for flexible operations. -
PriorityQueue
– is internally backed by a binary heap, which is very efficient for retrieval operations. -
ArrayDeque
– is implemented using an array that expands or shrinks as elements are added or removed. Here, elements can be added or removed from both ends of the queue.
-
-
Duplicates: In a
Queue
, duplicate elements are permitted, allowing multiple instances of the same value to be inserted -
Null: You cannot insert a null value into a
Queue
because, by design, some methods of aQueue
return null to indicate that it is empty. To avoid confusion, null values are not allowed. -
Ordering: Elements are inserted based on their natural order. Alternatively, you can control the insertion order by specifying a custom comparator.
-
Synchronization: A
Queue
is not synchronized by default. But, you can use aConcurrentLinkedQueue
or aBlockingQueue
implementation for achieving thread safety. -
Key methods: Here are some key methods of a
Queue
interface:add(E element)
,offer(E element)
,poll()
, andpeek()
. Let’s look at how to use these methods with an example program.import java.util.LinkedList; import java.util.Queue; public class QueueExample { public static void main(String[] args) { // Create a queue using LinkedList Queue<String> queue = new LinkedList<>(); // Use add method to insert elements, throws exception if insertion fails queue.add("Element1"); queue.add("Element2"); queue.add("Element3"); // Use offer method to insert elements, returns false if insertion fails queue.offer("Element4"); // Display queue System.out.println("Queue: " + queue); // Peek at the first element (does not remove it) String firstElement = queue.peek(); System.out.println("Peek: " + firstElement); // outputs "Element1" // Poll the first element (retrieves and removes it) String polledElement = queue.poll(); System.out.println("Poll: " + polledElement); // outputs "Element1" // Display queue after poll System.out.println("Queue after poll: " + queue); } }
-
Common implementations:
LinkedList
,PriorityQueue
,ArrayDeque
-
Performance: Implementations like
LinkedList
andArrayDeque
are usually quick for adding and removing items. ThePriorityQueue
is a bit slower because it inserts items based on the set priority order.
Operation | LinkedList | PriorityQueue | ArrayDeque |
Insertion | Fast at the beginning or middle – O(1), slow at the end – O(n) | Slower – O(log n) | Fast – O(1), Slow – O(n), if it involves resizing of the internal array |
Deletion | Fast – O(1) if position is known | Slower – O(log n) | Fast – O(1), Slow – O(n), if it involves resizing of the internal array |
Retrieval | Slow – O(n) for random access, as it involves traversing | Fast – O(1) | Fast – O(1) |
Maps
A Map
represents a collection of key-value pairs, with each key mapping to a single value. Although Map
is part of the Java Collection framework, it does not extend the java.util.Collection
interface.
-
Internal mechanism: A
Map
works internally using aHashTable
based on the concept of hashing. I have written a detailed article on this topic, so give it a read for a deeper understanding. -
Duplicates: A
Map
stores data as key-value pairs. Here, each key is unique, so duplicate keys are not allowed. But duplicate values are permitted. -
Null: Since duplicate keys are not allowed, a
Map
can have only one null key. As duplicate values are permitted, it can have multiple null values. In theTreeMap
implementation, keys cannot be null because it sorts the elements based on the keys. However, null values are allowed. -
Ordering: Insertion order of a
Map
varies on the implementation:-
HashMap
– the insertion order is not guaranteed as they are determined based on the concept of hashing. -
LinkedHashMap
– the insertion order is preserved and you can retrieve the elements back in the same order that they were added into the collection. -
TreeMap
– Elements are inserted based on their natural order. Alternatively, you can control the insertion order by specifying a custom comparator.
-
-
Synchronization: A
Map
is not synchronized by default. But you can useCollections.synchronizedMap()
orConcurrentHashMap
implementations for achieving thread safety. -
Key methods: Here are some key methods of a
Map
interface:put(K key, V value)
,get(Object key)
,remove(Object key)
,containsKey(Object key)
, andkeySet()
. Let’s look at how to use these methods with an example program.import java.util.HashMap; import java.util.Map; import java.util.Set; public class MapMethodsExample { public static void main(String[] args) { // Create a new HashMap Map<String, Integer> map = new HashMap<>(); // put(K key, V value) - Inserts key-value pairs into the map map.put("Apple", 1); map.put("Banana", 2); map.put("Orange", 3); // get(Object key) - Returns the value associated with the key Integer value = map.get("Banana"); System.out.println("Value for 'Banana': " + value); // remove(Object key) - Removes the key-value pair for the specified key map.remove("Orange"); // containsKey(Object key) - Checks if the map contains the specified key boolean hasApple = map.containsKey("Apple"); System.out.println("Contains 'Apple': " + hasApple); // keySet() - Returns a set view of the keys contained in the map Set<String> keys = map.keySet(); System.out.println("Keys in map: " + keys); } }
-
Common implementations:
HashMap
,LinkedHashMap
,TreeMap
,Hashtable
,ConcurrentHashMap
-
Performance:
HashMap
implementation is widely used mainly due to its efficient performance characteristics depicted in the below table.
Operation | HashMap | LinkedHashMap | TreeMap |
Insertion | Fast – O(1) | Fast – O(1) | Slower – O(log n) |
Deletion | Fast – O(1) | Fast – O(1) | Slower – O(log n) |
Retrieval | Fast – O(1) | Fast – O(1) | Slower – O(log n) |
Collections Utility Class
As highlighted at the beginning of this article, the Collections
utility class has several useful static methods that let you perform commonly used operations on the elements of a collection. These methods help you reduce the boilerplate code in your application and lets you focus on the business logic.
Here are some key features and methods, along with what they do, listed briefly:
-
Sorting:
Collections.sort(List<T>)
– this method is used to sort the elements of a list in ascending order. -
Searching:
Collections.binarySearch(List<T>, key)
– this method is used to search for a specific element in a sorted list and return its index. -
Reverse order:
Collections.reverse(List<T>)
– this method is used to reverse the order of elements in a list. -
Min/Max Operations:
Collections.min(Collection<T>)
andCollections.max(Collection<T>)
– these methods are used to find the minimum and maximum elements in a collection, respectively. -
Synchronization:
Collections.synchronizedList(List<T>)
– this method is used to make a list thread-safe by synchronizing it. -
Unmodifiable Collections:
Collections.unmodifiableList(List<T>)
– this method is used to create a read-only view of a list, preventing modifications.
Here’s a sample Java program that demonstrates various functionalities of the Collections
utility class:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CollectionsExample {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
numbers.add(5);
numbers.add(3);
numbers.add(8);
numbers.add(1);
// Sorting
Collections.sort(numbers);
System.out.println("Sorted List: " + numbers);
// Searching
int index = Collections.binarySearch(numbers, 3);
System.out.println("Index of 3: " + index);
// Reverse Order
Collections.reverse(numbers);
System.out.println("Reversed List: " + numbers);
// Min/Max Operations
int min = Collections.min(numbers);
int max = Collections.max(numbers);
System.out.println("Min: " + min + ", Max: " + max);
// Synchronization
List<Integer> synchronizedList = Collections.synchronizedList(numbers);
System.out.println("Synchronized List: " + synchronizedList);
// Unmodifiable Collections
List<Integer> unmodifiableList = Collections.unmodifiableList(numbers);
System.out.println("Unmodifiable List: " + unmodifiableList);
}
}
This program demonstrates sorting, searching, reversing, finding minimum and maximum values, synchronizing, and creating an unmodifiable list using the Collections
utility class.
Conclusion
In this article, you’ve learned about the Java Collections Framework and how it helps manage groups of objects in Java applications. We explored various collection types like Lists, Sets, Queues, and Maps and gained insight into some of the key characteristics and how each of these types supports them.
You learned about performance, synchronization, and key methods, gaining valuable insights into choosing the right data structures for your needs.
By understanding these concepts, you can fully utilize the Java Collections Framework, allowing you to write more efficient code and build robust applications.
If you found this article interesting, feel free to check out my other articles on freeCodeCamp and connect with me on LinkedIn.
Source: freeCodeCamp Programming Tutorials: Python, JavaScript, Git & MoreÂ