Java ArrayList
I guess you're here because you want to learn how to use the ArrayList in your Java code. You're in the right place!
As explained in the Java documentation, ArrayList is:
- A resizable array: implementing the List interface,
- Mutable: objects can be added and/or removed,
- Not Thread-safe: ArrayList is not suitable for concurrent access. See Thread Safety for more information.
Let's explore how to use an ArrayList through simple code examples!
How Array List Works¶
An ArrayList
is based on the concept of Dynamic Array. (also known as growable, resizable array)
The array is grown as the number elements grows to always keep some space left for future insertions.
Type Hierarchy¶
The ArrayList inherits from:
java.util.AbstractList
,java.util.Object
,- and implements java.util.List, java.util.Collection.
List is a subtype of Collection with guaranteed order of insertion.
Properties¶
The Java ArrayList
has the following properties:
- Can contain duplicate elements (i.e. elements which are equals),
- Maintains the order of insertion,
- is not synchronized (thus not thread-safe),
- Random access (through an integer index) are fast (0(1)) because it's backed by an array,
- Element removal is slow because elements must be shifted if any element is removed (to stay contiguous).
Methods¶
The following table lists ArrayList
most commonly used methods:
Method | Description |
---|---|
void add(int index, Object element) | Inserts an element at a given index |
boolean addAll(Collection c) | Appends all the elements contained in the passed collection |
void clear() | Empties the list |
int lastIndexOf(Object o) | Returns last index of given object, or -1 if not found |
Object[] toArray() | Creates an array with a copy of all objects within the list |
boolean add(Object o) | Appends the passed object to the list |
boolean addAll(int index, Collection c) | Inserts all elements at the given index |
Object clone() | Creates a shallow copy of the list (elements within are not cloned) |
int indexOf(Object o) | Index of the object being passed, or -1 if not found |
int size() | Number of elements the list contains |
Comparison with Arrays¶
What makes an ArrayList very convenient is the fact it's a resizable array. Arrays is a primitive structure designed to hold a fixed number of objects. It's possible but quite difficult to resize an array (the code is usually pretty ugly too). For this reason, an ArrayList
is much more convenient than an Array
when you need to add/remove objects without knowing the exact collection size.
package com.octoperf;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ArrayListTest {
@Test
public void shouldArray() {
final String[] arr = new String[0];
assertTrue(arr.length == 0);
}
}
In the code above, we show how to create an empty array in Java. The code asserts the array length is zero. Now, let's see how the code differs when using an ArrayList
.
Here is an extract from Java ArrayList source code:
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
/**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
As you can see, by default, an ArrayList
is constructed with a capacity of 10
elements. Should you add more than 10 elements, the ArrayList is automatically resized to fit them. Wonderful isn't it?
Algorithmic Complexity¶
Before we dive into the code, let's see the most common operations this List
performs algorithmically speaking. We're going to use the Big O-Notation to describe code complexity here.
Operation | Complexity |
---|---|
add(object o) | 0(1) |
add(object o, int index) | 0(n) |
remove(int index) | 0(1) |
remove(object o) | 0(n) |
size() | 0(1) |
contains(object o) | 0(n) |
Being based on an internal array, ArrayList is best suited for storing a limited number of elements (because increasingly growing and resizing hurts performance). Searching is not optimal, Sets should be preferred in that case. (0(log n) because Sets are sorted, Lists are not)
Constructors¶
package com.octoperf;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ArrayListTest {
@Test
public void shouldCreate() {
final List<String> list = new ArrayList<>();
assertTrue(list.isEmpty());
assertFalse(list.iterator().hasNext());
}
}
In the code above, we instantiate a new ArrayList
, which is empty by default. Remember, it's designed to hold 10 elements by default. (and it will grow automatically if more than 10 are added)
You can also create an ArrayList
with a default initial capacity:
@Test
public void shouldCreate() {
final List<String> list = new ArrayList<>(50);
assertTrue(list.isEmpty());
assertFalse(list.iterator().hasNext());
}
In the example above, the list has an internal array of capacity 50
. The constructor argument is the internal array size. Beware of trying to optimize internal array size when instantiating a list. It's a common pitfall! Some developers tend to try to optimize the list size based on their guesses. It usually leads to overestimated list sizes consuming more memory. Or to underestimated list sizes leading to unusually high list internal array resizes.
Example: You create an ArrayList of size 1. You put 32 items in this list. It needs to grow to sizes: 2, 4, 8, 16 and 32. That's 5 resizes with full array copy! With default
10
size, it would have grown to: 20, 40. That's 2 resizes only.
Remember, the list resizes itself when the internal array capacity is not enough. Each time the capacity limit is reached, the size is grown by a factor of 1.5:
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
int newCapacity = oldCapacity + (oldCapacity >> 1);
is equivalent of multiplying the old capacity by 1.5. (1 bit shift to the right)
On each resize, all List elements are copied into a larger array.
You can also create a list by passing another Collection. It creates a copy of the passed collection:
@Test
public void shouldCopy() {
final List<String> another = new ArrayList<>();
another.add("Hello World!");
final List<String> list = new ArrayList<>(another);
assertFalse(list.isEmpty());
assertTrue(list.contains("Hello World!"));
another.clear();
assertTrue(list.contains("Hello World!"));
}
The created List contains now contains a copy of all the elements the passed collection contains. Even if you clear the original collection, the element remains in the ArrayList
.
Adding Elements¶
Elements can be added at the beginning, at the end one by one or multiples elements at once.
@Test
public void shouldAddElements() {
final List<String> list = new ArrayList<>();
list.add("John Smith");
list.add("John Doe");
list.add(0, "Donald Trump");
list.addAll(ImmutableList.of("Barack Obama"));
System.out.println(list);
}
The output is:
[Donald Trump, John Smith, John Doe, Barack Obama]
@Test
public void shouldAddStream() {
final List<String> list = new ArrayList<>();
ImmutableList.of("John Smith", "John Doe").forEach(list::add);
System.out.println(list);
}
The output is:
[John Smith, John Doe]
The code above shows adding elements to the list using Java Streams and Lambdas expressions.
Removing Elements¶
Removing elements is pretty straight forward:
@Test
public void shouldRemoveElements() {
final List<String> list = new ArrayList<>();
list.add("John Smith");
list.add("John Doe");
list.remove("John Smith");
System.out.println(list);
}
The following code prints:
[John Doe]
As with the add
operations, multiple elements can be removed at once.
Iterating¶
@Test
public void shouldIterate() {
final List<String> list = new ArrayList<>();
list.add("John Smith");
list.add("John Doe");
// Foreach loop
for (final String name : list) {
System.out.println(name);
}
// Traditional for loop
for (int i = 0; i < list.size; i++) {
System.out.println(list.get(i));
}
// Java Stream
list.forEach(System.out::println);
// Through iterator
final Iterator<String> it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
The following code shows 4 different ways to iterate over list elements:
- Foreach Loop: as
List
implements Iterable interface, lists can be iterated directly in for loops, - For Loop: traditional for loops using an integer index,
- Java Streams: using Stream API introduced in Java 8,
- Iterator: using list iterator.
These methods are roughly equivalent in term of performance. How you should iterate over the list depends on the context.
Searching¶
One should be aware that Lists are not very well suited for search operations, except for sorted lists.
Unsorted List¶
When searching an unsorted list, the algorithm is pretty inefficient: it scans the whole list to find the wanted element. If you need to perform frequent searchs in a collection, prefer a Set. The complexity of this operation is 0(n)
.
@Test
public void shouldSearch() {
final List<String> list = new ArrayList<>();
list.add("John Smith");
list.add("John Doe");
assertEquals(1, list.indexOf("John Doe"));
assertTrue(list.stream().anyMatch("John Doe"::equals));
}
Sorted List¶
Sorted lists can be searched using binary search algorithm. In this case, the complexity is 0(log n)
, which is much better than exhaustive search.
@Test
public void shouldSearch() {
final List<String> list = new ArrayList<>();
list.add("John Smith");
list.add("John Doe");
Collections.sort(list);
assertEquals(0, Collections.binarySearch(list, "John Doe"));
}
Best Use-Cases¶
ArrayList
is best suited when:
- Random Access: Seeking elements by index, the internal array allows a direct access to it,
- Holding a constant number of elements: this way, you avoid any internal array resize (which is costly),
- Evaluating List size: takes
O(1)
.
ArrayList
performs badly when:
- Inserting / removing elements at specific index: the internal array must be grown / shrunk, and existing elements must be shifted,
Alternatives¶
ArrayList
is not the only implementation of the Java List interface. In fact there is also:
- LinkedList: Doubly-linked list implementation,
- Vector: synchronized implementation based on a growable array of objects,
- Stack: specialization of a Vector with LIFO strategy (Last In, First Out).
Depending on the nature of the operations frequently executed on the list instance, you should consider using one of these alternatives.
LinkedList
is best suited for many add and remove operations: its doubly linked chain makes it easy to do so (simply reassigning pointers).
Vector
is best suited in code executed concurrently by multiple threads because it's synchronized. This data structure is pretty old (since JDK 1.0) and not used much these days.
Stack
is great when you need to pop elements in the reverse order they have been pushed.