ArrayList
Steven J. Zeil
java.utils.ArrayList
provides an array-like generic container, rather similar to the aList
from your text and to the orderedSequence
class from earlier in these lecture notes. But ArrayList
has one crucial new behavior: it can “grow” to accommodate as many items as you actually try to put into it.
This makes it a very useful structure, both in itself and also as a basis for implementing many other ADTs.
1 New ArrayList functions
You can find the entire interface summarized here
As it happens, we don’t need to do a deep dive into the ArrayList
interface, because almost all of the functions it supplies are inherited from List
, which we have already covered. The exception is that ArrayList
, being a class instead of an interface, has constructors. That means that we can actually create new ArrayList
objects!
1.1 Constructors
public class ArrayList<E> implements List<E>, RandomAccess, Cloneable {
⋮
/**
* 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) { ... }
/**
* Constructs an empty list with an initial capacity of 10.
*/
public ArrayList() { ... }
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
public ArrayList(Collection<? extends E> c) { ... }
⋮
The first constructor introduces the idea of the capacity of an ArrayList
. We’ll explain what this is in just a bit.
The second constructor is very basic, as it takes no parameters at all:
ArrayList<String> seq = new ArrayList<String>();
assertEquals(0, seq.size());
The third allows the initialization of an ArrayList
with the data from some other Collection
:
List<String> inputs = List.of("abc", "def", "ghi");
ArrayList<String> seq = new ArrayList<String>(inputs);
assertEquals(3, seq.size());
assertEquals("def", seq.get(1));
assertIterableEqual(inputs, seq); // asserts that the two lists are equal, element by element
1.2 Size vs Capacity
As we will see, the way that an ArrayList
works is that it reserves space in an array, then slowly fills that array as you add elements to the list. If the array fills up entirely and you are still trying to add elements, then the ArrayList
allocates a bigger array, copies all of the existing data into the new array, and discards the old array.
The capacity of an ArrayList
is the number of elements that it can hold before it needs to expand by creating the new, larger array. “Capacity” is not at all the same thing as the “size” of the ArrayList
, the number of data elements currently in the list. In general, the size of an array list will always be less than or equal to its capacity.
You cannot ask an arrayList
what its capacity is, but you can control the capacity. The first constructor given earlier allows you to indicate the minimum capacity you want a new ArrayList
to have.
ArrayList<String> seq = new ArrayList<String>(4);
assertEquals(0, seq.size()); // size is 0, but capacity is at least 4
In addition, ArrayList
introduces two functions to control the capacity of an existing list:
/**
* Trims the capacity of this ArrayList instance to be the
* list's current size. An application can use this operation to minimize
* the storage of an ArrayList instance.
*/
public void trimToSize() { ... }
/**
* Increases the capacity of this ArrayList instance, if
* necessary, to ensure that it can hold at least the number of elements
* specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
public void ensureCapacity(int minCapacity) { ... }
We use trimToSize
to discard unused space held by an ArrayList
, potentially saving memory.
We use ensureCapacity
to make sure we already have enough storage to hold all of the data that we plan to add to the list, potentially speeding up our code because we won’t waste time allocating extra storage and copying data from the old array to the new one.
2 Performance
Although we don’t need to learn a lot of new functions to use an ArrayList
, it’s important that we know the complexity of the operations that it does provide.
ArrayList ops | ||
---|---|---|
Function | Complexity | Notes |
ArrayList(int initialCapacity) |
O(initialCapacity) | |
ArrayList() |
O(1) | |
ArrayList(Collection<? extends E> c) |
O(c.size()) | |
trimToSize() |
O(size()) | |
ensureCapacity(int minCapacity) |
O(size()) | |
int size() |
O(1) | |
boolean isEmpty() |
O(1) | |
boolean contains(Object o) |
O(size()) | |
int indexOf(Object o) |
O(size()) | |
int lastIndexOf(Object o) |
O(size()) | |
Object clone() |
O(size()) | |
T[] toArray(T[] a) |
O(size()) | |
E get(int index) |
O(1) | |
E set(int index, E element) |
O(1) | |
boolean add(E e) |
O(size()) | but amortizes to O(1) (see below) |
void add(int index, E element) |
O(size() - index + 1) | Adding to the end is still O(1) |
E remove(int index) |
O(size() - index + 1) | Removing from the end is O(1) |
boolean remove(Object o) |
O(size()) | |
clear() |
O(size()) | |
boolean addAll(Collection<? extends E> c) |
O(size() + c.size()) | |
boolean addAll(int index, Collection<? extends E> c) |
O(size() + c.size()) | |
boolean removeAll(Collection<?> c) |
O(size() * c.size()) | |
boolean retainAll(Collection<?> c) |
O(size() * c.size()) | |
Iterator<E> iterator()) |
O(1) | See iterator operations in next table |
ListIterator<E> listIterator()) |
O(1) | See iterator operations in next table |
sort() |
O(N log(N)) | where N is size() |
The operations with bold complexities will be discussed in more detail in the next section. (The complexity of the sort()
function will be discussed some weeks from now.)
In addition to the above operations that apply directly to an ArrayList
, we can obtain Iterator
s and ListIterator
s from an ArrayList
, and it’s important to know the complexity of their operations as well:
Iterator & ListIterator ops | ||
---|---|---|
Function | Complexity | Notes |
boolean hasNext() |
O(1) | |
boolean hasPrevious() |
O(1) | ListIterator only |
E next() |
O(1) | |
E previous() |
O(1) | ListIterator only |
remove() |
O(size()) | |
add(E) |
O(size()) | ListIterator only |
set(E) |
O(1) | ListIterator only |
3 Implementing the ArrayList
3.1 The Data Structure
The underlying data structure is quite simple:
public class ArrayList<E>
implements List<E>, RandomAccess, Cloneable {
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer.
*/
Object[] data; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
We have an array of data
elements, and an integer size
that tell us how many slots in that array are currently occupied.
3.2 Basic Operations
This makes many of the operations very easy to implement. For example, getting and setting values at an indexed position is done by applying the same index to the array data
.
// Returns the element at the specified position in this list.
public E get(int index) {
rangeCheck(index); // checks to be sure index is in 0..size-1
return (E) data[index];
}
// Replaces the element at the specified position in this list with
// the specified element.
public E set(int index, E element) {
rangeCheck(index);
E oldValue = (E) data[index];
data[index] = element;
return oldValue;
}
Other O(1) functions are also easy:
// Returns the number of elements in this list.
public int size() {
return size;
}
// Returns true if this list contains no elements.
public boolean isEmpty() {
return size == 0;
}
3.3 Inserting and removing by indexed position
One thing that programmers using ArrayList
must be constantly aware of is that, just because we can easily get
and set
at a position, inserting and removing from a position are much more expensive.
// Inserts the specified element at the specified position in this
// list. Shifts the element currently at that position (if any) and
// any subsequent elements to the right (adds one to their indices).
public void add(int index, E element) {
rangeCheckForAdd(index); // makes sure that index is in 0..size // O(1)
growIfNecessary(); // We'll cover this later. It's O(size())
System.arraycopy(data, index, data, index + 1,
size - index); // O(size() - index)
data[index] = element; // O(1)
size++; // O(1)
}
// Removes the element at the specified position in this list.
// Shifts any subsequent elements to the left (subtracts one from their
// indices).
public E remove(int index) {
rangeCheck(index); // O(1)
E oldValue = (E)data[index]; // O(1)
int numMoved = size - index - 1; // O(1)
if (numMoved > 0) // cond: O(1) total: O(numMoved)
System.arraycopy(data, index + 1, data, index,
numMoved); // O(numMoved)
data[--size] = null; // clear to let GC do its work // O(1)
return oldValue; // O(1)
}
The add
function uses an arraycopy
call to move elements out of the way, creating a “hole” at position index
where we can insert a new value. Because that arraycopy
call is O(size() - index), the entire function is O(size() - index).
The remove
function uses an arraycopy
call to shift elements down into the newly vacated “hole” resulting from our decision to remove an element. Because that arraycopy
call is O(size() - index), the entire function is O(size() - index).
You can run these functions as an animation if you want to see them in action.
3.4 add(e): Adding to the end of the list
However, adding to the end of an ArrayList
is expected to be cheaper. Ideally, we would like this to be O(1), but we aren’t going to quite get there. The complication comes from the idea that we want an ArrayList
to be able to grow to an arbitrary size as we continue to add elements.
How do we allow ArrayList
s to grow to arbitrary size?
We’ll split this into cases:
3.4.1 Adding to an Array That Has Unused Space

Let’s consider the operation of adding a new element to the end of this list,
myList.add(t5);
If there is room in the array, we just add our new data element.
// Appends the specified element to the end of this list.
public boolean add(E e) {
growIfNecessary();
data[size] = e;
++size;
return true;
}

That was easy!
3.4.2 Adding to an Array That is Full
We’ll see later what this does to the big-O complexity for add(E)
.
3.5 Coding add(E)
Now, let’s look at the code to accomplish all this.
Remember the basic steps:
-
If the data array is already filled to capacity, and we try to add more to it, we make another that is twice as big …
-
First the new data area is allocated.
-
Then the old data is copied into the new array.
-
The old array is replaced by hte new one, and the
size
field updated.
-
-
Now we can add the new element to the end of the array.
3.5.1 Are we Full? If so, Grow!
// Appends the specified element to the end of this list.
public boolean add(E e) {
growIfNecessary();
data[size] = e;
++size;
return true;
}
The growIfNecessary()
function will check this.
private void growIfNecessary() {
if (data.length <= size) { // if the array is full
ensureCapacity(Math.max(1, 2 * data.length)); // Replace by another, twice as big.
}
}
ensureCapacity
is one of the functions in the public interface.
3.5.2 ensureCapacity
// Increases the capacity of this ArrayList, if
// necessary, to ensure that it can hold at least the number of elements
// specified by the minimum capacity argument.
public void ensureCapacity(int minCapacity) {
if (minCapacity > size) { // Do we need more room? ➀
Object[] newStorage = new Object[minCapacity]; // Allocate ➁
System.arraycopy(data, 0, newStorage, 0, size); // Copy the existing data ➂
data = newStorage; // Replace the old array by this new larger one
}
}
- If the data array is already filled to capacity ➀,
- then we need to create a new array that is larger ➁
- and copy the data ➂ from the old array to the new one.
You can run the
add
function as an animation if you want to see it in action. Try adding to the end of anArrayList
until a new array allocation is triggered.
3.6 The Complexity of add(E)
add
calls growIfNecessary
, so we will need to know that function’s compelxity befire we can analyze add
.
growIfNecessary
calls ensureCapacity
, so we will need to that functions complexity first.
3.6.1 ensureCapacity
Remember that arraycopy
is O(length), whcere “length” is the fifth parameter to the call, the number of elements to copy.
That means that ensureCapacity
has a worst case complexity of O(size())
.
3.6.2 growIfNecessary
Now, look at growIfNecessary
:
private void growIfNecessary() {
if (data.length <= size) { // if the array is full
ensureCapacity(Math.max(1, 2 * data.length)); // Replace by another, twice as big.
}
}
If ensureCapacity
is in O(size())
, then growIfNecessary
is also in O(size())
.
3.6.3 add
Finally, return to the code for add
:
// Appends the specified element to the end of this list.
public boolean add(E e) {
growIfNecessary(); // O(size())
data[size] = e; // O(1)
++size; // O(1)
return true; // O(1)
}
add(E) is, worst-case, O(size())
3.6.4 add(E) is, worst-case, O(size())
That’s disappointing.
But not every add(E)
call takes O(size())
time.
- only when we have filled the array
- otherwise, it takes O(1) time
Let’s look at the issue from a slightly different point of view:
How long does it take to do a total of n
add(E)
operations, starting with an empty list?
3.7 Amortizing
Definition
amortize: to decrease (on average) over an extended period of time.
This term comes from the world of finance, where the cost of an initial high investment in equipment or facilities is often assessed (e.g., for tax purposes) at its equivalent annual cost over all the years that the equipment is in operation. For example, a $10,000 computer expected to have a working lifetime of 5 years may be said to have an amortized cost of $2,000 per year.
Similarly, we can ask how the total cost of doing multiple add(...)
calls can be distributed over time.
3.7.1 Doing N adds
Let k be the smallest integer such that N≤2k. For the sake of simplicity, we’ll do the analysis as if we were actually going to do 2k add calls.
- Let m be the current
size()
of the list.-
If m is a power of 2, say, 2i, then the array is full and we do O(m) work on the next call to
add(E)
. -
If m≠2i, then we do O(1) work on that next call.
-
We can then add up the total effort as
T(n)=k∑i=1(O(2i)+2i−1∑j=1O(1))=O(k∑i=1(2i))=O(1+2+4+…+2k)=O(2k+1−1)=O(2k+1)
The total effort is O(2k+1).
But we started with the definition of n saying that n=2k, so this total effort is O(2n)=O(n).
3.7.2 add(E) has an Amortized Worst Case of O(1)
So even though
-
an individual call to
add(E)
may be O(n), -
the total effort for all n
add
s used to build anArrayList
“from scratch” is also O(n)
We say that the amortized worst-case time of add(E)
is therefore O(1).
Whether or not the amortized cost is really what we want depends upon what kind of performance is important to us. If we are mainly interested in how some algorithm involving many add
s performs in totality, the amortized cost is appropriate. If, however, we are dealing with an interactive algorithm that does one add(E)
in between each prompt for user input, then the “real” O(n) worst case is more appropriate because it indicates the amount of time that the user might have to wait after submitting an input.
3.8 Using ensureCapacity() to get a True O(1) Worst Case
If we knew ahead of time how many elements would be placed into the ArrayList
, we can make all the add(E)
calls O(1) time. We would do this by calling ensureCapacity
to make sure there are enough slots without requesting more memory:
ArrayList<String> names;
System.out.println("How many names? ");
Scanner input = new Scanner(System.in);
int n = input.nextInt();
names.ensureCapacity (n);
for (int i = 0; i < n; ++i)
{
System.out.println("\nEnter name #" + i + ": ");
String aName = input.next();
names.add(aName);
}
3.9 Summary
So the true answer is that ArrayList.add(E)
does have a worst case of O(size()), but in special circumstances that cost may average (amortize) to O(1) over a sequence of n calls.
When doing copy-and-paste style annotations, you should always write the worst case on the line where an add
call resides. You can use the cheaper amortized cost when figuring the total cost of a loop containing that line.
Remember, too, that this amortized cost only applies to the add(E)
function, not to the add(int, E)
function for inserting into the interior of a list.