1. Overview
Collections in Java are based on a couple of core interfaces and more than a dozen implementation classes. The wide selection of different implementations can sometimes lead to confusion.
Deciding on which collection type to use for a particular use case is not a trivial task. That decision can have a great impact on our code readability and performance.
Instead of explaining all types of collections in a single article, we’ll explain three of the most common ones: ArrayList, LinkedList, and HashMap. In this tutorial, we’ll look at how they store data, their performance, and recommend when to use them.
2. Collections
A collection is simply a Java object that groups other objects together. The Java Collections Framework contains a set of data structures and algorithms for representing and manipulating collections. If applied correctly, the provided data structures help reduce programming effort and increase performance.
2.1. Interfaces
The Java Collections Framework contains four basic interfaces: List, Set, Map, and Queue. It is important to understand the intended usage of these interfaces before looking at the implementation classes.
Let’s have a quick look at three of the four core interfaces that we’ll use in this article:
- The List interface is dedicated to storing ordered collections of objects. It allows us to positionally access and inserts new elements, as well as save duplicate values
- The Map interface supports a key-value pair mapping of the data. To access a certain value, we need to know its unique key
- The Queue interface enables the storage of data based on the first-in-first-out order. Similar to a real-world queue line
HashMap implements the Map interface. The List interface is implemented by both ArrayList and LinkedList. LinkedList additionally implements the Queue interface.
2.2. List vs. Map
A common antipattern we sometimes encounter is trying to maintain order using a map. Thus, not making use of other collection types more suitable for the job.
Just because we can solve many problems with a single collection type doesn’t mean we should.
Let’s look at a bad example, where we use a map to save data based on the positional key:
Map<Integer, String> map = new HashMap<>();
map.put(1, "Daniel");
map.put(2, "Marko");
for (String name : map.values()) {
assertThat(name).isIn(map.values());
}
assertThat(map.values()).containsExactlyInAnyOrder("Daniel", "Marko");
When we iterate through the map values, we’re not guaranteed to retrieve them in the same order we put them in. That is simply because a map wasn’t designed for maintaining the order of elements.
We can rewrite this example in a much more readable way using a list. Lists are ordered by definition, so we can iterate through the items in the same order that we inserted them:
List<String> list = new ArrayList<>();
list.add("Daniel");
list.add("Marko");
for (String name : list) {
assertThat(name).isIn(list);
}
assertThat(list).containsExactly("Daniel", "Marko");
Maps are designed for quick access and search based on unique keys. When we want to maintain order or work with position-based indexes, lists are a natural choice.
3. ArrayList
ArrayList is the most commonly used implementation of the List interface in Java. It is based on built-in arrays but can dynamically grow and shrink as we add or remove elements.
We use indexes that start from zero to access list elements. We can insert a new element either at the end, or the specific position of the list:
List<String> list = new ArrayList<>();
list.add("Daniel");
list.add(0, "Marko");
assertThat(list).hasSize(2);
assertThat(list.get(0)).isEqualTo("Marko");
To remove an element from the list, we need to provide the object reference or its index:
List<String> list = new ArrayList<>(Arrays.asList("Daniel", "Marko"));
list.remove(1);
assertThat(list).hasSize(1);
assertThat(list).doesNotContain("Marko");
3.1. Performance
ArrayList provides us with dynamic arrays in Java. Although slower than the built-in arrays, ArrayList helps us save some programming effort and improve code readability.
When we talk about time complexity, we make use of the Big-O notation. The notation describes how the time to perform the algorithm grows with the size of the input.
ArrayList allows random access since arrays are based on indexes. That means that accessing any element always takes a constant time O(1).
Adding new elements also takes O(1) time, except when adding an element on a specific position/index, or, worst-case scenario, when a new array has to be created and all the elements copied to it, then it takes O(n). Checking if a specific element exists in the given list runs in linear O(n) time.
The same is true for the removal of elements. We need to iterate the entire array to find the element selected for removal.
3.2. Usage
Whenever we’re unsure what collection type to use, it’s probably a good idea to start with an ArrayList. Keep in mind that accessing items based on indexes will be very fast. However, searching for items based on their value or adding/removing items at a specific position will be expensive.
Using ArrayList makes sense when it is important to maintain the same order of items, and quick access time based on the position/index is an important criterion.
Avoid using ArrayList when the order of items is not important. Also, try to avoid it when items often need to be added to a specific position. Likewise, bear in mind that ArrayList may not be the best option when searching for specific item values is an important requirement, especially if the list is large.
4. LinkedList
LinkedList is a doubly-linked list implementation. Implementing both the List and Deque (an extension of Queue) interfaces. Unlike ArrayList, when we store data in a LinkedList, every element maintains a link to the previous one.
Besides standard List insertion methods, LinkedList supports additional methods that can add an element at the beginning or the end of the list:
LinkedList<String> list = new LinkedList<>();
list.addLast("Daniel");
list.addFirst("Marko");
assertThat(list).hasSize(2);
assertThat(list.getLast()).isEqualTo("Daniel");
This list implementation also offers methods for removing elements from the beginning or at the end of the list:
LinkedList<String> list = new LinkedList<>(Arrays.asList("Daniel", "Marko", "David"));
list.removeFirst();
list.removeLast();
assertThat(list).hasSize(1);
assertThat(list).containsExactly("Marko");
The implemented Deque interface provides queue-like methods for retrieving, adding, and deleting elements:
LinkedList<String> list = new LinkedList<>();
list.push("Daniel");
list.push("Marko");
assertThat(list.poll()).isEqualTo("Marko");
assertThat(list).hasSize(1);
4.1. Performance
A LinkedList consumes a bit more memory than an ArrayList since every node stores two references to the previous and next element.
The insertion, addition, and removal operations are faster in a LinkedList because there is no resizing of an array done in the background. When a new item is added somewhere in the middle of the list, only references in surrounding elements need to change.
LinkedList supports O(1) constant-time insertion at any position in the collection. However, it is less efficient at accessing items in a specific position, taking O(n) time.
Removing an element also takes O(1) constant-time, since we just need to modify a few pointers. Checking if a specific element exists in the given list takes O(n) linear time, same as for an ArrayList.
4.2. Usage
Most of the time we can use ArrayList as the default List implementation. However, in certain use-cases, we should make use of LinkedList. Those include when we prefer constant insertion and deletion time, over constant access time, and effective memory usage.
Using LinkedList makes sense when maintaining the same order of items and quick insertion time (adding and removing items at any position) is an important criterion.
Like an ArrayList, we should avoid using LinkedList when the order of items is not important. LinkedList is not the best option when fast access time or searching for items is an important requirement.
5. HashMap
Unlike ArrayList and LinkedList, HashMap implements the Map interface. That means that every key is mapped to exactly one value. We always need to know the key to retrieve the corresponding value from the collection:
Map<String, String> map = new HashMap<>();
map.put("123456", "Daniel");
map.put("654321", "Marko");
assertThat(map.get("654321")).isEqualTo("Marko");
Similarly, we can only delete a value from the collection using its key:
Map<String, String> map = new HashMap<>();
map.put("123456", "Daniel");
map.put("654321", "Marko");
map.remove("654321");
assertThat(map).hasSize(1);
5.1. Performance
One might ask, why not simply use a List and get rid of the keys all together? Especially since HashMap consumes more memory for saving keys and its entries are not ordered. The answer lies in the performance benefits for searching elements.
HashMap is very efficient at checking if a key exists or retrieving a value based on a key. Those operations take O(1) on average.
Adding and removing elements from a HashMap based on a key takes O(1) constant-time. Checking for an element without knowing the key takes linear time O(n), as it’s necessary to loop over all the elements.
5.2. Usage
Along with ArrayList, HashMap is one of the most frequently used data structures in Java. Unlike different list implementations, HashMap makes use of indexing to perform a jump to a specific value, making the search time constant, even for large collections.
Using HashMap makes sense only when unique keys are available for the data we want to store. We should use it when searching for items based on a key and quick access time is an important requirement.
We should avoid using HashMap when it is important to maintain the same order of items in a collection.
6. Conclusion
In this article, we explored three common collection types in Java: ArrayList, LinkedList, and HashMap. We looked at their performance for adding, removing, and searching for items. Based on that, we provided recommendations on when to apply each of them in our Java applications.
In the examples, we covered only basic methods for adding and removing items. For a more detailed look at each implementation API, please visit our dedicated ArrayList, ArrayList, and HashMap articles.
As always, the complete source code is available over on GitHub.