ArrayList is one of the most commonly used data structures in Java. Understanding its internal implementation is crucial for writing efficient code.
1. ArrayList Class Structure
Basic Class Declaration
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { // Default initial capacity private static final int DEFAULT_CAPACITY = 10; // Shared empty array instance used for empty instances private static final Object[] EMPTY_ELEMENTDATA = {}; // The array buffer where elements are stored transient Object[] elementData; // The size of the ArrayList (number of elements it contains) private int size; // ... other methods and constructors } 2. Internal Data Structure
Core Fields
// Simplified view of ArrayList internals public class ArrayListInternalView<E> { // The actual array that stores elements private transient Object[] elementData; // Current number of elements in the list private int size; // Default capacity when no initial capacity specified private static final int DEFAULT_CAPACITY = 10; // Empty array for empty instances private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; } 3. Constructors and Initialization
Constructor Implementations
import java.util.Arrays; public class ArrayListInternalsDemo { // Simulating ArrayList's internal array private transient Object[] elementData; private int size; private static final int DEFAULT_CAPACITY = 10; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // Constructor 1: Default constructor public ArrayListInternalsDemo() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; this.size = 0; System.out.println("Default constructor: elementData = " + Arrays.toString(elementData) + ", size = " + size); } // Constructor 2: With initial capacity public ArrayListInternalsDemo(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; this.size = 0; } else if (initialCapacity == 0) { this.elementData = new Object[0]; this.size = 0; } else { throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); } System.out.println("Capacity constructor: elementData.length = " + elementData.length + ", size = " + size); } // Constructor 3: From existing collection public ArrayListInternalsDemo(java.util.Collection<? extends E> c) { Object[] a = c.toArray(); this.size = a.length; if (size != 0) { this.elementData = Arrays.copyOf(a, size, Object[].class); } else { this.elementData = new Object[0]; } System.out.println("Collection constructor: elementData = " + Arrays.toString(elementData) + ", size = " + size); } public static void main(String[] args) { System.out.println("=== ArrayList Constructor Examples ==="); // Default constructor ArrayListInternalsDemo<String> list1 = new ArrayListInternalsDemo<>(); // Capacity constructor ArrayListInternalsDemo<String> list2 = new ArrayListInternalsDemo<>(20); // Collection constructor java.util.List<String> existingList = Arrays.asList("A", "B", "C"); ArrayListInternalsDemo<String> list3 = new ArrayListInternalsDemo<>(existingList); } } 4. Add Operation - Internal Working
add(E element) Method
public class ArrayListAddOperation { private Object[] elementData; private int size; private static final int DEFAULT_CAPACITY = 10; public ArrayListAddOperation() { this.elementData = new Object[DEFAULT_CAPACITY]; this.size = 0; } // Main add method public boolean add(E e) { System.out.println("Adding element: " + e); System.out.println("Before add - size: " + size + ", capacity: " + elementData.length); // Ensure capacity ensureCapacityInternal(size + 1); // Add element at the end elementData[size++] = e; System.out.println("After add - size: " + size + ", capacity: " + elementData.length); System.out.println("Current array: " + Arrays.toString(Arrays.copyOf(elementData, size))); return true; } // Internal capacity management private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { // Overflow-conscious code if (minCapacity - elementData.length > 0) { grow(minCapacity); } } // The core growth mechanism private void grow(int minCapacity) { System.out.println(">>> Growing array from " + elementData.length + " to accommodate " + minCapacity); int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5 times old capacity System.out.println("Old capacity: " + oldCapacity + ", New capacity (1.5x): " + newCapacity); if (newCapacity - minCapacity < 0) { newCapacity = minCapacity; System.out.println("Adjusted new capacity to minCapacity: " + newCapacity); } if (newCapacity - MAX_ARRAY_SIZE > 0) { newCapacity = hugeCapacity(minCapacity); } // Copy elements to new larger array elementData = Arrays.copyOf(elementData, newCapacity); System.out.println(">>> Array grown to new capacity: " + newCapacity); } private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; } private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; public static void main(String[] args) { ArrayListAddOperation<String> list = new ArrayListAddOperation<>(); // Add elements to demonstrate growth for (int i = 1; i <= 15; i++) { list.add("Element-" + i); System.out.println("---"); } } } add(int index, E element) Method
public class ArrayListAddAtIndex { private Object[] elementData; private int size; private static final int DEFAULT_CAPACITY = 5; // Smaller for demonstration public ArrayListAddAtIndex() { this.elementData = new Object[DEFAULT_CAPACITY]; this.size = 0; } // Add at specific index public void add(int index, E element) { rangeCheckForAdd(index); System.out.println("Adding '" + element + "' at index " + index); System.out.println("Before: " + Arrays.toString(Arrays.copyOf(elementData, size))); ensureCapacityInternal(size + 1); // Shift elements to the right to make space System.arraycopy(elementData, index, elementData, index + 1, size - index); // Insert the new element elementData[index] = element; size++; System.out.println("After: " + Arrays.toString(Arrays.copyOf(elementData, size))); System.out.println("Size: " + size + ", Capacity: " + elementData.length); System.out.println("---"); } private void rangeCheckForAdd(int index) { if (index > size || index < 0) { throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); } } private void ensureCapacityInternal(int minCapacity) { if (minCapacity - elementData.length > 0) { grow(minCapacity); } } private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) { newCapacity = minCapacity; } elementData = Arrays.copyOf(elementData, newCapacity); System.out.println(">>> Array grown from " + oldCapacity + " to " + newCapacity); } public static void main(String[] args) { ArrayListAddAtIndex<String> list = new ArrayListAddAtIndex<>(); // Add some initial elements list.add(0, "First"); list.add(1, "Third"); list.add(1, "Second"); // Insert in middle list.add(0, "Zeroth"); // Insert at beginning list.add(4, "Fifth"); // Insert at end list.add(2, "Middle"); // Insert in middle causing shift } } 5. Get and Set Operations
Internal Working of get() and set()
public class ArrayListGetSet { private Object[] elementData; private int size; public ArrayListGetSet() { this.elementData = new Object[10]; this.size = 0; } public void add(E e) { if (size == elementData.length) { elementData = Arrays.copyOf(elementData, elementData.length * 2); } elementData[size++] = e; } // Get operation - O(1) complexity @SuppressWarnings("unchecked") public E get(int index) { System.out.println("Getting element at index " + index); rangeCheck(index); E element = (E) elementData[index]; System.out.println("Returning: " + element); return element; } // Set operation - O(1) complexity @SuppressWarnings("unchecked") public E set(int index, E element) { System.out.println("Setting element at index " + index + " to: " + element); rangeCheck(index); E oldValue = (E) elementData[index]; elementData[index] = element; System.out.println("Old value: " + oldValue); return oldValue; } private void rangeCheck(int index) { if (index >= size) { throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); } } public int size() { return size; } public static void main(String[] args) { ArrayListGetSet<String> list = new ArrayListGetSet<>(); // Add elements for (int i = 0; i < 5; i++) { list.add("Item-" + i); } System.out.println("=== Get Operations ==="); list.get(0); // First element list.get(2); // Middle element list.get(4); // Last element System.out.println("\n=== Set Operations ==="); list.set(1, "Updated-Item-1"); list.set(3, "Updated-Item-3"); // Try invalid access try { list.get(10); // Should throw exception } catch (IndexOutOfBoundsException e) { System.out.println("Expected exception: " + e.getMessage()); } } } 6. Remove Operation - Internal Working
remove(int index) Method
public class ArrayListRemoveOperation { private Object[] elementData; private int size; private static final int DEFAULT_CAPACITY = 10; public ArrayListRemoveOperation() { this.elementData = new Object[DEFAULT_CAPACITY]; this.size = 0; } public void add(E e) { if (size == elementData.length) { elementData = Arrays.copyOf(elementData, elementData.length * 2); } elementData[size++] = e; } // Remove by index @SuppressWarnings("unchecked") public E remove(int index) { System.out.println("Removing element at index " + index); System.out.println("Before remove: " + getArraySnapshot()); rangeCheck(index); E oldValue = (E) elementData[index]; int numMoved = size - index - 1; if (numMoved > 0) { // Shift elements to the left to fill the gap System.arraycopy(elementData, index + 1, elementData, index, numMoved); } // Clear the last element and reduce size elementData[--size] = null; System.out.println("After remove: " + getArraySnapshot()); System.out.println("Removed value: " + oldValue); System.out.println("---"); return oldValue; } // Remove by object public boolean remove(Object o) { System.out.println("Removing object: " + o); System.out.println("Before remove: " + getArraySnapshot()); if (o == null) { for (int index = 0; index < size; index++) { if (elementData[index] == null) { fastRemove(index); System.out.println("After remove: " + getArraySnapshot()); return true; } } } else { for (int index = 0; index < size; index++) { if (o.equals(elementData[index])) { fastRemove(index); System.out.println("After remove: " + getArraySnapshot()); return true; } } } System.out.println("Object not found: " + o); return false; } // Fast remove without bounds check private void fastRemove(int index) { int numMoved = size - index - 1; if (numMoved > 0) { System.arraycopy(elementData, index + 1, elementData, index, numMoved); } elementData[--size] = null; } private void rangeCheck(int index) { if (index >= size) { throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); } } private String getArraySnapshot() { return Arrays.toString(Arrays.copyOf(elementData, size)); } public static void main(String[] args) { ArrayListRemoveOperation<String> list = new ArrayListRemoveOperation<>(); // Add elements for (int i = 0; i < 6; i++) { list.add("Element-" + i); } System.out.println("=== Remove by Index ==="); list.remove(2); // Remove middle list.remove(0); // Remove first list.remove(2); // Remove last (after previous removes) System.out.println("=== Remove by Object ==="); list.remove("Element-1"); list.remove("Non-Existent"); // Try to remove non-existent element } } 7. Iteration and ModCount
Internal Iterator Implementation
import java.util.*; public class ArrayListIteratorInternal { private Object[] elementData; private int size; protected transient int modCount = 0; // Modification count public ArrayListIteratorInternal() { this.elementData = new Object[10]; this.size = 0; } public void add(E e) { modCount++; if (size == elementData.length) { elementData = Arrays.copyOf(elementData, elementData.length * 2); } elementData[size++] = e; } public E remove(int index) { modCount++; rangeCheck(index); @SuppressWarnings("unchecked") E oldValue = (E) elementData[index]; int numMoved = size - index - 1; if (numMoved > 0) { System.arraycopy(elementData, index + 1, elementData, index, numMoved); } elementData[--size] = null; return oldValue; } // Custom iterator implementation public Iterator<E> iterator() { return new Itr(); } // Internal Iterator class private class Itr implements Iterator<E> { int cursor = 0; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount; public boolean hasNext() { System.out.println("hasNext() - cursor: " + cursor + ", size: " + size); return cursor != size; } @SuppressWarnings("unchecked") public E next() { System.out.println("next() called - cursor: " + cursor); checkForComodification(); int i = cursor; if (i >= size) { throw new NoSuchElementException(); } Object[] elementData = ArrayListIteratorInternal.this.elementData; if (i >= elementData.length) { throw new ConcurrentModificationException(); } cursor = i + 1; lastRet = i; return (E) elementData[lastRet]; } public void remove() { System.out.println("Iterator remove() called"); if (lastRet < 0) { throw new IllegalStateException(); } checkForComodification(); try { ArrayListIteratorInternal.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; // Reset expected modification count } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } final void checkForComodification() { if (modCount != expectedModCount) { System.out.println("Concurrent modification detected! modCount: " + modCount + ", expectedModCount: " + expectedModCount); throw new ConcurrentModificationException(); } } } private void rangeCheck(int index) { if (index >= size) { throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); } } public static void main(String[] args) { ArrayListIteratorInternal<String> list = new ArrayListIteratorInternal<>(); // Add elements for (int i = 0; i < 5; i++) { list.add("Item-" + i); } System.out.println("=== Normal Iteration ==="); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); } System.out.println("\n=== Iteration with Remove ==="); iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); if ("Item-2".equals(item)) { iterator.remove(); System.out.println("Removed: " + item); } } System.out.println("\n=== Concurrent Modification Example ==="); iterator = list.iterator(); try { while (iterator.hasNext()) { String item = iterator.next(); System.out.println(item); if ("Item-1".equals(item)) { list.add("New-Item"); // This will cause ConcurrentModificationException } } } catch (ConcurrentModificationException e) { System.out.println("Caught ConcurrentModificationException as expected"); } } } 8. Performance Analysis
Time Complexity Analysis
import java.util.*; public class ArrayListPerformanceAnalysis { private static final int TEST_SIZE = 100000; public static void main(String[] args) { System.out.println("ArrayList Performance Analysis"); System.out.println("Test Size: " + TEST_SIZE + " elements"); System.out.println("====================================="); // Add operations testAddOperations(); // Access operations testAccessOperations(); // Search operations testSearchOperations(); // Remove operations testRemoveOperations(); // Memory usage testMemoryUsage(); } public static void testAddOperations() { System.out.println("\n=== Add Operations ==="); // Add at end List<Integer> list = new ArrayList<>(); long start = System.nanoTime(); for (int i = 0; i < TEST_SIZE; i++) { list.add(i); } long end = System.nanoTime(); System.out.printf("Add at end: %.2f ms%n", (end - start) / 1_000_000.0); // Add at beginning list.clear(); start = System.nanoTime(); for (int i = 0; i < 1000; i++) { // Smaller size for beginning adds list.add(0, i); } end = System.nanoTime(); System.out.printf("Add at beginning (1000 elements): %.2f ms%n", (end - start) / 1_000_000.0); // Add in middle list.clear(); for (int i = 0; i < 1000; i++) { list.add(i); } start = System.nanoTime(); for (int i = 0; i < 100; i++) { list.add(500, i); } end = System.nanoTime(); System.out.printf("Add in middle (100 operations): %.2f ms%n", (end - start) / 1_000_000.0); } public static void testAccessOperations() { System.out.println("\n=== Access Operations ==="); List<Integer> list = new ArrayList<>(); for (int i = 0; i < TEST_SIZE; i++) { list.add(i); } // Random access long start = System.nanoTime(); for (int i = 0; i < TEST_SIZE; i++) { list.get(i); } long end = System.nanoTime(); System.out.printf("Sequential access: %.2f ms%n", (end - start) / 1_000_000.0); // Random indices access Random random = new Random(); start = System.nanoTime(); for (int i = 0; i < 10000; i++) { list.get(random.nextInt(TEST_SIZE)); } end = System.nanoTime(); System.out.printf("Random access (10000 operations): %.2f ms%n", (end - start) / 1_000_000.0); } public static void testSearchOperations() { System.out.println("\n=== Search Operations ==="); List<Integer> list = new ArrayList<>(); for (int i = 0; i < TEST_SIZE; i++) { list.add(i); } // Search for existing element long start = System.nanoTime(); boolean found = list.contains(TEST_SIZE / 2); long end = System.nanoTime(); System.out.printf("Contains (middle element): %.2f ms, Found: %b%n", (end - start) / 1_000_000.0, found); // Search for non-existing element start = System.nanoTime(); found = list.contains(TEST_SIZE + 1); end = System.nanoTime(); System.out.printf("Contains (non-existing): %.2f ms, Found: %b%n", (end - start) / 1_000_000.0, found); // Index of operation start = System.nanoTime(); int index = list.indexOf(TEST_SIZE / 2); end = System.nanoTime(); System.out.printf("IndexOf (middle element): %.2f ms, Index: %d%n", (end - start) / 1_000_000.0, index); } public static void testRemoveOperations() { System.out.println("\n=== Remove Operations ==="); // Remove from end List<Integer> list = new ArrayList<>(); for (int i = 0; i < TEST_SIZE; i++) { list.add(i); } long start = System.nanoTime(); for (int i = TEST_SIZE - 1; i >= 0; i--) { list.remove(i); } long end = System.nanoTime(); System.out.printf("Remove from end: %.2f ms%n", (end - start) / 1_000_000.0); // Remove from beginning list.clear(); for (int i = 0; i < 1000; i++) { list.add(i); } start = System.nanoTime(); while (!list.isEmpty()) { list.remove(0); } end = System.nanoTime(); System.out.printf("Remove from beginning (1000 elements): %.2f ms%n", (end - start) / 1_000_000.0); } public static void testMemoryUsage() { System.out.println("\n=== Memory Usage ==="); Runtime runtime = Runtime.getRuntime(); // Force garbage collection System.gc(); long memoryBefore = runtime.totalMemory() - runtime.freeMemory(); List<Integer> list = new ArrayList<>(); for (int i = 0; i < TEST_SIZE; i++) { list.add(i); } long memoryAfter = runtime.totalMemory() - runtime.freeMemory(); long memoryUsed = memoryAfter - memoryBefore; System.out.printf("Memory used by ArrayList with %d elements: %.2f MB%n", TEST_SIZE, memoryUsed / (1024.0 * 1024.0)); // Show capacity vs size // Note: We can't access internal capacity directly, but we can estimate System.out.println("Size: " + list.size()); System.out.println("Estimated capacity growth pattern: 10 → 15 → 22 → 33 → ..."); } } 9. Capacity Growth Pattern
Visualizing Capacity Growth
import java.lang.reflect.Field; import java.util.ArrayList; public class ArrayListCapacityGrowth { public static void main(String[] args) throws Exception { System.out.println("ArrayList Capacity Growth Pattern"); System.out.println("================================="); ArrayList<Integer> list = new ArrayList<>(); System.out.println("Initial state:"); printCapacityAndSize(list); // Add elements and track capacity growth for (int i = 1; i <= 100; i++) { list.add(i); // Print when capacity changes if (i == 1 || i == 10 || i == 11 || i == 16 || i == 17 || i == 25 || i == 26 || i == 38 || i == 39 || i == 58 || i == 59 || i == 88 || i == 89) { printCapacityAndSize(list); } } System.out.println("\nFinal state:"); printCapacityAndSize(list); System.out.println("\nGrowth Pattern Summary:"); System.out.println("10 → 15 (1.5x)"); System.out.println("15 → 22 (1.5x)"); System.out.println("22 → 33 (1.5x)"); System.out.println("33 → 49 (1.5x)"); System.out.println("49 → 73 (1.5x)"); System.out.println("73 → 109 (1.5x)"); } // Helper method to get capacity using reflection private static int getCapacity(ArrayList<?> list) throws Exception { Field elementDataField = ArrayList.class.getDeclaredField("elementData"); elementDataField.setAccessible(true); Object[] elementData = (Object[]) elementDataField.get(list); return elementData.length; } private static void printCapacityAndSize(ArrayList<?> list) throws Exception { System.out.printf("Size: %2d, Capacity: %2d%n", list.size(), getCapacity(list)); } } Summary
Key Internal Mechanisms:
- Backing Array: ArrayList uses
Object[] elementDatato store elements - Size Tracking:
int sizetracks the number of elements - Capacity Growth: Grows by 1.5x when needed (
newCapacity = oldCapacity + (oldCapacity >> 1)) - Default Capacity: 10 when created with default constructor
- Fast Access: O(1) for get/set operations due to array backing
- ModCount: Tracks structural modifications for fail-fast iterators
Time Complexity:
- add(E element): Amortized O(1)
- add(int index, E element): O(n)
- get(int index): O(1)
- set(int index, E element): O(1)
- remove(int index): O(n)
- remove(Object o): O(n)
- contains(Object o): O(n)
Memory Characteristics:
- Overhead: Each ArrayList has array overhead + size field
- Wasted Space: Can have unused capacity in the backing array
- Trim:
trimToSize()can reduce wasted space
Best Practices:
- Use initial capacity constructor if size is known
- Prefer
add(element)overadd(index, element)when possible - Use
ArrayListfor frequent access,LinkedListfor frequent insertions/deletions - Consider
trimToSize()if memory is critical and no more adds expected
Understanding ArrayList's internal working helps in writing efficient code and choosing the right collection for specific use cases.