Java ArrayList Insert: Methods and Performance
java arraylist insert: How to insert elements into a Java ArrayList with add(E) and add(int, E), covering positional insertion, edge cases, and performance tradeoffs.
Inserting an element into a Java ArrayList comes down to two overloads of the add method defined on the List interface. The first, add(E element), appends the element to the end of the list. The second, add(int index, E element), inserts the element at a specified position and shifts every subsequent element one index to the right. When developers search for java arraylist insert, the positional overload is usually the one they need, because it changes the existing order of the collection rather than simply extending it.
Both methods accept null as a valid element, because ArrayList does not enforce non-null constraints. The positional overload throws IndexOutOfBoundsException if the index is negative or greater than the current size of the list.
Appending with add(E)
The single-argument add(E element) method appends the element to the end of the list and returns true. Although the return value is defined by the Collection interface, ArrayList always returns true because it never rejects elements on the basis of capacity or duplication rules.
List<String> tasks = new ArrayList<>(); tasks.add("compile"); tasks.add("test"); tasks.add("deploy");
After these calls, tasks contains ["compile", "test", "deploy"] in insertion order. Appending is the most common insert operation because it matches the natural growth pattern of a list that accumulates results, reads input, or collects intermediate values.
The amortized cost of appending is constant time. The ArrayList maintains an internal Object[] array with extra capacity beyond the current size. When the array is full, the list allocates a larger array and copies the existing elements into it. The growth policy is implementation-defined, but the standard behavior is to increase capacity by roughly 50% when the current array is exhausted. That geometric growth keeps the average append cost at O(1) even though individual appends occasionally trigger a full copy.
Inserting at a Specific Index with add(int, E)
The two-argument overload add(int index, E element) places the element at the given index and shifts all elements at that index and beyond one position to the right. The valid range for index is 0 through size(), inclusive. Inserting at size() is equivalent to appending.
List<String> steps = new ArrayList<>(); steps.add("parse input"); steps.add("validate"); steps.add("save"); steps.add(1, "normalize"); // steps is now: [parse input, normalize, validate, save]
The new element occupies the requested index, and every element that was previously at that index or later moves one position right. The element that was at index 1, "validate", is now at index 2.
If the index is outside the valid range, the method throws IndexOutOfBoundsException. This includes negative indices and any index greater than size(). A common mistake is passing list.size() - 1 when the intent is to insert at the end; that places the element before the last element rather than after it. Use list.size() for an append-equivalent insert.
What Happens Internally When You Insert
The positional insert is not a simple assignment. The ArrayList is backed by a plain Object[] array, and inserting at an interior index requires shifting the tail of the array to make room.
The implementation first checks the index range, then ensures the backing array has sufficient capacity, and finally moves the elements from the insertion index through size() - 1 one position to the right using System.arraycopy. Only after the shift completes does the list assign the new element at the insertion index and increment its size counter.
// Conceptual behavior, not the actual JDK source System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++;
This explains why inserting at the front of a large list is expensive: every element must be copied one slot to the right. The cost is proportional to the number of elements after the insertion point. Inserting at index 0 on a list of one million elements copies roughly one million references.
The shift operation copies references, not the objects themselves, so the objects are not duplicated or re-created. The memory cost is a single additional reference slot in the backing array, plus the temporary cost of the copy operation.
Performance Cost of Positional Insertion
The runtime cost of add(int index, E element) is O(n - index), where n is the current size of the list. That means appending at the end is amortized O(1), inserting in the middle is O(n/2) on average, and inserting at the front is O(n).
The same shifting cost applies when you remove an element from an interior position, because the list must close the gap by moving elements left. If your code performs many front insertions on a large list, the cumulative cost becomes quadratic, which is usually the first sign that ArrayList is the wrong collection for the workload.
For frequent insertions at the front, ArrayDeque supports addFirst in amortized constant time. A LinkedList also supports constant-time insertion at either end, but its node-based structure has poor cache locality and higher per-element memory overhead, so it is rarely the best choice unless you also need constant-time removal of interior elements via an iterator.
A practical alternative is to reverse the insertion order: if you need to prepend many elements, collect them in reverse and append, then reverse the final list once. That changes the workload from many O(n) shifts to a single O(n) reversal.
Common Edge Cases and Mistakes
The most frequent error with positional insertion is confusing add(int, E) with set(int, E). The set method replaces the element at the given index and returns the old value. It does not change the size of the list. add shifts elements and increases the size by one.
List<Integer> values = new ArrayList<>(List.of(1, 2, 3)); values.set(1, 9); // values: [1, 9, 3], size stays 3 values.add(1, 7); // values: [1, 7, 9, 3], size becomes 4
Another edge case is inserting into a list while iterating over it with a for loop or an enhanced for loop. Modifying the list during iteration causes a ConcurrentModificationException because the iterator detects structural changes. If you need to insert while iterating, use an explicit ListIterator and its add method, which inserts the element at the iterator's current position without invalidating the iteration.
List<String> items = new ArrayList<>(List.of("a", "c")); ListIterator<String> it = items.listIterator(); it.next(); // position after "a" it.add("b"); // inserts "b" before "c" // items: [a, b, c]
The ListIterator.add method inserts immediately before the element that would be returned by the next call to next, and it does not throw ConcurrentModificationException because the iterator itself performs the modification.
When ArrayList Insert Is the Wrong Choice
The decision to use ArrayList for insert-heavy workloads depends on where the insertions happen and how large the list grows. If insertions are concentrated at the end, ArrayList is the right choice. If insertions happen at the front or middle of a list that routinely holds thousands of elements, the shifting cost becomes the dominant factor.
A LinkedList appears attractive because its add(int, E) method does not shift elements; it only rewires node pointers. However, reaching the insertion index requires a traversal from one of the ends, which is O(n) on average. The actual insertion is O(1) once the position is found, but the traversal cost usually dominates. For large lists, LinkedList is often slower than ArrayList even for middle insertions because of cache misses during traversal.
For a deque-like workload with insertions at both ends, ArrayDeque is the strongest candidate. It supports addFirst and addLast in amortized constant time, and it has better memory locality than LinkedList. It does not support indexed access, so it is only appropriate when you do not need to read or write elements by position.
The general rule is to measure the access pattern. If the code reads elements by index far more often than it inserts at interior positions, ArrayList remains the better default. If interior insertion is the dominant operation and the list is large, restructure the algorithm to append in reverse order, or switch to a collection designed for that access pattern.