Written By:
Nestor Rubio
Last Updated:
December 5th, 2023
You might have been in a lecture, a discussion, an internet rabbit hole, or in the middle of some sort of interstellar daydream when you came across the term “ArrayList”. I hope that no matter where you’re coming from, I can help clear this term up for you so that you can start understanding and implementing this powerful data structure.
Please note: If you’re not already familiar with arrays, I highly recommend gaining a solid understanding of them before diving into ArrayLists, as arrays serve as the building blocks for ArrayLists.
With that disclaimer out of the way, we can start with a simple question and work our way forward to in-depth understanding.
What is an ArrayList and Why Use It?
The concept of the ArrayList was introduced to me as the merging of typical Array behaviors with a solution to its primary drawback: the limitation on the number of items it can store.
It’s well established that once you make an Array of a certain size, you’re pretty much stuck with that amount. Wouldn’t it be nice if we could just declare an array, use it like an array, but NEVER worry about the size limit? Maybe this mysterious array could just keep expanding in size? Luckily, there is a solution and it comes in the form of the ArrayList. With the foundation of an array but the behavior of self-regulation, the ArrayList is an idea which will definitely expand the limits of your problem-solving capabilities.
The ArrayList is often defined as a class or structure (depending on the language), which typically makes use of a couple of things: an inner array, a capacity, a size, and several methods or functions defining the behavior of the ArrayList.
The inner array was a given since it needs to store the data somewhere and neither of us feels like rewriting how memory gets stored and moved around in our computer’s registers. The capacity keeps track of the temporary limit of how many items you can store in any moment. The size variable is used to keep track of how many items are currently being stored in the ArrayList. Miscellaneous methods, such as add, remove, isEmpty, clear, and others, are often optional helper functions and methods which make solving the problem at hand much easier and oftentimes makes the code much easier to read and interpret.
How to use it?
It is worth noting that not all languages require the introduction of the ArrayList data structure, many like Python use lists or a similar idea which essentially acts as what we describe the ArrayList to be and you can add to them without worry. The code samples below are written in C and Java, showing the two common implementations of ArrayLists: using the data structure straight out of the box, or creating it yourself. Both are fun but in my experience, it is always nice to build a data structure from scratch first, so you have a more in-depth understanding of some of the operations which aren’t so clear in the out-of-the-box versions.
The code used in both examples will be commented to ensure it is clear what is happening at each line.
From Scratch
- C
- Java
#include <stdio.h>
#include <stdlib.h>
// defining "ArrayList" to not say "struct ArrayList"
typedef struct ArrayList ArrayList;
// constant values
const int INITIAL_CAPACITY = 10;
// declaring the struct to use
struct ArrayList {
// inner array, in this case it's integers
int* array;
// inner size and capacity
int size, capacity;
};
// function to create an ArrayList
ArrayList* newArrayList() {
// allocate the memory for the ArrayList
ArrayList* ret = malloc(sizeof(ArrayList));
ret->array = malloc(INITIAL_CAPACITY * sizeof(int));
ret->capacity = INITIAL_CAPACITY;
ret->size = 0;
return ret;
}
void resize(ArrayList* list) {
if(list == NULL) return;
// the new capacity is calculated the same way Java's
// ArrayList class calculates its new capacity
int newCapacity = ((list->capacity * 3)/2 + 1);
// reallocate memory in the inner array and expand the size of it
list->array = realloc(list->array, newCapacity * sizeof(int));
list->capacity = newCapacity;
}
// function to add an element to the end of an ArrayList
void add(ArrayList* list, int value) {
if(list == NULL) return;
// if list size is at capacity, resize it
if(list->size == list->capacity) resize(list);
// add the value to the end of the list, increasing size by one
list->array[list->size] = value;
list->size++;
}
// function to remove an element from the list
void removeAtIndex(ArrayList* list, int index) {
if(list == NULL) return;
// if the index is out of bounds, return
if(index < 0 || index >= list->size) return;
// if index is the last element, decrease size by one and return
if(index == list->size - 1) {
list->size -= 1;
return;
}
// shift all elements after the index to the left by one
// basically overriding the element at index
for(int i = index; i < list->size - 1; i++) {
list->array[i] = list->array[i + 1];
}
// decrease size by one, reflecting the removed item
list->size -= 1;
}
// function returning -1 if null, 1 if empty and 0 if not
int isEmpty(ArrayList* list) {
if(list == NULL) return -1;
// if this looks weird, give ternary operators a quick search
return list->size == 0 ? 1 : 0;
}
// function to clear the list
void clear(ArrayList* list) {
if(list == NULL) return;
// free the allocated memory in the array
free(list->array);
// reallocate the memory
list->array = malloc(INITIAL_CAPACITY * sizeof(int));
list->size = 0;
list->capacity = INITIAL_CAPACITY;
}
int itemAt(ArrayList* list, int index) {
if(list == NULL) return -1;
if(index < 0 || index >= list->size) return -1;
return list->array[index];
}
int main(void) {
ArrayList* list = newArrayList();
// add 20 items into list (0-19), note that starting at a capacity
// for 10 items, the capacity and size will grow accordingly
for(int i = 0; i < 20; i++){
add(list, i);
}
// remove a couple of items
removeAtIndex(list, 0);
removeAtIndex(list, 18);
removeAtIndex(list,5);
// if list is not empty (it isn't) then print the statement
if(isEmpty(list) == 0) {
printf("Hope ArrayLists make more sense now!\n");
}
}
public class ArrayList {
// defining constant values
final private static int INITIAL_CAPACITY = 10;
// defining necessary variables
private int[] array;
private int capacity, size;
// constructor method
public ArrayList() {
this.array = new int[INITIAL_CAPACITY];
this.capacity = INITIAL_CAPACITY;
this.size = 0;
}
// getter for size & no setter because size only grows with adding an item
public int getSize() {
return this.size;
}
public int getItemAtIndex(int index) {
if(index < 0 || index >= this.size) {
throw new IndexOutOfBoundsException("Index out of bounds");
// Note: errors are not necessary but could be something else to look into for extra learning if you are not familiar with them yet
}
return this.array[index];
}
/*
No getter for capacity or the inner list because those should be abstracted away from the user. We should not let them worry about how the object works on the inside
*/
// add item to arraylist
public void add(int item) {
// if array is full, expand array
if (this.size == this.capacity) {
this.expand();
}
// add item to array
this.array[this.size] = item;
// increment size
this.size += 1;
}
// remove item at index in arraylist
public void removeItemAtIndex(int index) {
// if index is out of bounds, throw error
if (index < 0 || index >= this.size) {
throw new IndexOutOfBoundsException("Index out of bounds");
}
// shift all items to the left, overriding item at index
for (int i = index; i < this.size - 1; i++) {
this.array[i] = this.array[i + 1];
}
// decrement size
this.size--;
}
// function returning whether list is empty or not
public boolean isEmpty() {
return this.size == 0;
//return this.array.length == 0 could also be used but both values will be equal
}
// resize the list to allow for extra space
// method is private because users shouldn't worry about this operation
private void expand() {
// declare new capacity for the list
int newCapacity = ((this.capacity * 3)/2 + 1);
// create new array with size of newCapacity
int[] newArray = new int[newCapacity];
// copy all items from old array into new array
for(int i = 0; i < this.size; i++) {
newArray[i] = this.array[i];
}
// assign array to be this new array and capacity to be new capacity
this.array = newArray;
this.capacity = newCapacity;
}
// clear list of all items
public void clear() {
// essentially restore to constructor state
this.array = new int[INITIAL_CAPACITY];
this.capacity = INITIAL_CAPACITY;
this.size = 0;
}
// main method to test methods
public static void main(String[] args) {
ArrayList list = new ArrayList();
// add 20 items into list (0-19), note that starting at a capacity
// for 10 items, the capacity and size will grow accordingly
for(int i = 0; i < 20; i++){
list.add(i);
}
System.out.println(list.getItemAtIndex(2)); // -> 2
System.out.println(list.getItemAtIndex(17)); // -> 17
list.removeItemAtIndex(0); // removes value 0 at index 0
System.out.println(list.getItemAtIndex(0)); // -> 1
}
}
Out-of-the-Box
// need to import the ArrayList in order to use it
import java.util.ArrayList;
public class ArrayListOutOfTheBox {
public static void main(String[] args) {
// Arraylists can be of any non-primitive data type
ArrayList<Integer> nums = new ArrayList<Integer>();
ArrayList<String> strings = new ArrayList<String>();
// can add freely
nums.add(10);
nums.add(20);
strings.add("Hello");
strings.add("world");
// can get an item by index
System.out.println(nums.get(1) + " " + strings.get(0));
// prints "20 Hello"
//can get sizes
System.out.println(nums.size() + " " + strings.size());
// prints "2 2"
// can delete items by index
nums.remove(0);
strings.remove(1);
// can get size and check if empty
System.out.println("nums has " + nums.size() + " so is it empty? " + nums.isEmpty()); // -> "nums has 1 so is it empty? false"
System.out.println("strings has " + nums.size() + " so is it empty? " + nums.isEmpty()); // -> "strings has 1 so is it empty? false"
// clearing is easy, just reassign
nums = new ArrayList<Integer>();
strings = new ArrayList<String>();
}
}
Performance Analysis
If you have not explored the idea of algorithm analysis, I recommend reading my post about exactly that topic here. From this, you will know plenty on Big O notation and how it is being used in the analysis of ArrayList function or method performance below.
- Get Item
- By Indexing
- O(1) – With the inner array being used, indexing is always constant time.
- By Value
- O(n) – If you are retrieving an item by a condition or by looking for a specific value, it could require looking at all elements in the list once.
- By Indexing
- Add Item
- To End
- O(1) – This is considered constant time because despite the possibility of needing to do a O(n) resize of the list, it becomes less and less likely with each expansion, meaning it can pretty much be considered constant.
- At Specific Index
- O(n) – Assuming you select any spot that isn’t the end of the list, all of the values to the right of the index would need to be shifted over by one index, meaning we could iterate a portion of all elements in the list.
- To End
- Delete Item
- O(n) – More often than not, you will be deleting an item from anywhere in the list but the end and if this is the case then the rest of the ArrayList’s values would need to be shifted to accommodate making it a linear time operation on average. If removing from end, it is O(1) since no items get moved around.
- Size
- Through Counting
- O(n) – Incrementing a counter from 0 to the size of the list for each item you see is a linear time operation.
- By Reading Integer
- O(1) – Remember that a size integer is stored to know the size of the list at any time. Reading and returning this integer value is a constant time operation.
- Through Counting
- isEmpty
- O(1) – All that really needs to be checked is the size of the list which is a constant time operation. Could also check the first index to see if an item is stored or not.
More To Learn
Possibilities
The Out-of-the-Box section showed some intro-level methods which can be used in the ArrayList class. There is a lot more that can be done with this class than I show here. If you wish to learn more about these possibilities, I strongly recommend visiting the official Oracle documentation here to see great documentation on this class and many others.
If you see a cool method you want to add to an ArrayList implementation from scratch, then give it a shot and consider it a challenge. It’ll help you grow and better understand the ArrayList as a whole.
Do I Even Have It?
As I mentioned before, not all languages need a notion of an ArrayList because they may already use Lists which are their own resizable structure, and not need to expand on the idea of an array. Whatever language you use, consider looking around and seeing whether the language needs ArrayLists to solve any issues or if there’s other ways to solve the resizable container issue.
Conclusion
We’ve explored the concept of an ArrayList from the problem it solves to how to create and use it. We’ve even seen an overview of the performance of several common methods available in an ArrayList. You now have the tools to go and solve problems where an array would be okay, but a resizable array is ideal.
I now bestow upon you the ArrayList as a data structure to add to your programming tool belt. Best of luck!