Processing math: 100%

ArrayList

Steven J. Zeil

Last modified: Jun 11, 2025
Contents:

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 Iterators and ListIterators 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 ArrayLists 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

 

If the data array is already filled to capacity, and we try to add more to it,

myList.add(t4);

we make another that is twice as big …

first prev1 of 5next last

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:

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
        }
    }

You can run the add function as an animation if you want to see it in action. Try adding to the end of an ArrayList 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.

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 N2k. For the sake of simplicity, we’ll do the analysis as if we were actually going to do 2k add calls.

We can then add up the total effort as

T(n)=ki=1(O(2i)+2i1j=1O(1))=O(ki=1(2i))=O(1+2+4++2k)=O(2k+11)=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

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 adds 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.