☕ Java
ArrayList
ArrayList is a resizable-array implementation of the List interface. It is the most commonly used collection in Java, providing dynamic sizing on top of a standard array. Elements are stored in contiguous memory, enabling O(1) random access by index. When the internal array is full, ArrayList automatically allocates a larger array and copies all elements. ArrayList is not thread-safe and preserves insertion order, allowing duplicates.
How ArrayList Works Internally
ArrayList maintains an internal Object[] called elementData. When you create an ArrayList with new ArrayList<>(), the internal array starts either empty or at a default initial capacity of 10, depending on the constructor used. Elements are stored in consecutive array slots from index 0 to size-1.
When you call add() and the size reaches the array's length, ArrayList grows. The growth strategy is to create a new array with capacity equal to the old capacity multiplied by 1.5 (specifically: oldCapacity + (oldCapacity >> 1)), copy all existing elements to the new array, and discard the old array. This amortised growth means that while any single add may be expensive (O(n) to copy), the average cost of an add over many operations is O(1) amortised — because you copy rarely, the cost amortises out.
The distinction between size (number of elements currently stored) and capacity (length of the internal array) is important. You can specify the initial capacity at construction time when you know approximately how many elements you will store — this avoids all intermediate resizing operations. If you will add 10,000 elements, creating new ArrayList<>(10_000) saves many copy operations compared to starting with capacity 10 and growing repeatedly.
Removal from the middle of an ArrayList requires shifting all elements after the removed position one slot to the left. This is an O(n) operation. Similarly, insertion at the beginning or middle requires shifting elements right, also O(n). This is the fundamental performance characteristic that distinguishes ArrayList from LinkedList: ArrayList wins for random access (O(1)) but loses for middle insertion and removal (O(n)).
Java
// ── Internal structure: ──────────────────────────────────────────────
// ArrayList<E> {
// transient Object[] elementData; // the backing array
// private int size; // number of elements stored
// }
//
// new ArrayList<>():
// elementData = {} (empty array, lazy-initialised to capacity 10 on first add)
// size = 0
//
// After adding "Alice", "Bob", "Charlie":
// elementData = ["Alice", "Bob", "Charlie", null, null, null, null, null, null, null]
// size = 3 (capacity = 10)
//
// After adding 8 more elements (total 11):
// grows: new capacity = 10 + (10 >> 1) = 15
// elementData = new Object[15]
// all 11 elements copied to new array
// ── Creating an ArrayList: ────────────────────────────────────────────
ArrayList<String> list1 = new ArrayList<>(); // default capacity
ArrayList<String> list2 = new ArrayList<>(50); // initial capacity 50
ArrayList<String> list3 = new ArrayList<>(
Arrays.asList("Alice", "Bob")); // from existing collection
// Preferred: declare as List interface:
List<String> names = new ArrayList<>();
// ── Capacity management: ──────────────────────────────────────────────
ArrayList<Integer> nums = new ArrayList<>(1000); // pre-sized
for (int i = 0; i < 1000; i++) nums.add(i);
// No intermediate resizing occurred — capacity was sufficient from the start.
// trimToSize() — release unused capacity:
ArrayList<String> trimmed = new ArrayList<>(100);
trimmed.add("a"); trimmed.add("b"); trimmed.add("c");
trimmed.trimToSize(); // shrinks internal array to size 3
// ensureCapacity() — hint that we'll add many elements:
ArrayList<String> growing = new ArrayList<>();
growing.ensureCapacity(500); // avoid resizing for up to 500 elementsCore ArrayList Methods
ArrayList implements the full List interface and adds a few ArrayList-specific methods. The List interface methods cover all common operations: positional access by index, searching for elements, subList views, and list iterators. ArrayList-specific methods include trimToSize() to reduce memory usage and ensureCapacity() to pre-allocate space.
The get(int index) and set(int index, E element) methods are O(1) because they directly compute the memory offset into the backing array. This is ArrayList's principal advantage over LinkedList.
The add(E element) method appends to the end and is amortised O(1). The add(int index, E element) method inserts at a position and is O(n) because of the element shift. The remove(int index) method removes by index and is O(n) for the shift. The remove(Object o) method searches for the first occurrence (O(n) scan) and removes it (O(n) shift for a total of O(n)).
indexOf() and lastIndexOf() perform a linear O(n) scan. contains() is also O(n) because it internally calls indexOf(). For large collections where frequent containment checks are needed, a HashSet is far more appropriate than an ArrayList — O(1) vs O(n).
The subList(fromIndex, toIndex) method returns a view — not a copy — of the ArrayList from fromIndex (inclusive) to toIndex (exclusive). Modifications to the subList are reflected in the original list and vice versa. The subList is backed by the original ArrayList, and structural modifications to the original list after obtaining the subList make it invalid.
Java
List<String> list = new ArrayList<>(
Arrays.asList("Alice", "Bob", "Charlie", "Dave", "Eve"));
// ── Positional access — O(1): ────────────────────────────────────────
System.out.println(list.get(0)); // Alice
System.out.println(list.get(4)); // Eve
list.set(1, "Barbara"); // replace index 1
System.out.println(list.get(1)); // Barbara
// ── Adding elements: ──────────────────────────────────────────────────
list.add("Frank"); // append — O(1) amortised
list.add(0, "Zara"); // insert at start — O(n) shift
list.addAll(List.of("Grace", "Henry")); // add all at end
list.addAll(2, List.of("X", "Y")); // insert all at index 2
// ── Removing elements: ────────────────────────────────────────────────
list.remove(0); // remove by index — O(n) shift
list.remove("Frank"); // remove first occurrence by value — O(n)
list.removeAll(List.of("X", "Y")); // remove all occurrences of these values
list.removeIf(s -> s.startsWith("G")); // remove all matching predicate
// ── Searching: ────────────────────────────────────────────────────────
System.out.println(list.indexOf("Alice")); // first occurrence index, -1 if absent
System.out.println(list.lastIndexOf("Alice")); // last occurrence index
System.out.println(list.contains("Alice")); // true/false
// ── Sorting: ─────────────────────────────────────────────────────────
list.sort(null); // natural order
list.sort(Comparator.reverseOrder()); // reverse order
list.sort(Comparator.comparing(String::length)
.thenComparing(Comparator.naturalOrder())); // multi-criteria
// ── subList — view, not copy: ─────────────────────────────────────────
List<String> sub = list.subList(1, 4); // [index 1, 4)
sub.set(0, "MODIFIED"); // reflects in original list
sub.clear(); // removes elements from original
// ── Iteration: ────────────────────────────────────────────────────────
for (String s : list) System.out.println(s); // for-each
list.forEach(System.out::println); // forEach
for (int i = 0; i < list.size(); i++) { // indexed
System.out.println(i + ": " + list.get(i));
}ArrayList Performance and Best Practices
Understanding ArrayList's time complexity guides correct usage. Random access and append are fast; middle insertion and removal are slow. When your access pattern is predominantly index-based reads and appends, ArrayList is the right choice. When your access pattern requires frequent insertion or removal at arbitrary positions, a LinkedList or a different data structure may be more appropriate.
Converting an ArrayList to other collection types and back is a common pattern. Creating a HashSet from an ArrayList deduplicates its contents. Creating a List from a Set produces an unordered (by set's perspective) list. Sorting an ArrayList in place with list.sort() is generally more efficient than copying to a sorted collection because it avoids the allocation overhead and benefits from TimSort's O(n log n) performance with good performance on partially-sorted data.
The ConcurrentModificationException is thrown when an ArrayList's structure is modified (elements added or removed) while iterating with an iterator or for-each loop. This is the fail-fast behaviour that detects concurrent modification. The correct approach for removal during iteration is to use iterator.remove(), use removeIf(), or collect indices to remove and remove them in a backward pass after iteration. Never call list.remove() inside a for-each loop.
For thread-safe list operations, use Collections.synchronizedList(new ArrayList<>()) for a synchronised wrapper, or CopyOnWriteArrayList for a copy-on-write list that is optimised for read-heavy concurrent access. Neither ConcurrentHashMap (which is a map) nor ConcurrentLinkedQueue (which is a queue) are substitutes for a thread-safe list — the concurrent list type in java.util.concurrent is CopyOnWriteArrayList.
Java
// ── Performance summary: ──────────────────────────────────────────────
//
// Operation ArrayList LinkedList
// ───────────────────────── ───────── ──────────
// get(index) O(1) O(n)
// set(index, value) O(1) O(n)
// add(value) — append O(1)* O(1)
// add(index, value) O(n) O(n)**
// remove(index) O(n) O(n)**
// remove(Object) O(n) O(n)
// contains(Object) O(n) O(n)
// size() O(1) O(1)
// iterator.next() O(1) O(1)
//
// * amortised O(1) — occasional O(n) resize
// ** O(1) if using ListIterator at known position
// ── Avoid ConcurrentModificationException: ───────────────────────────
List<String> items = new ArrayList<>(
Arrays.asList("a", "remove-me", "b", "remove-me", "c"));
// WRONG — throws ConcurrentModificationException:
// for (String s : items) {
// if (s.equals("remove-me")) items.remove(s);
// }
// CORRECT 1 — iterator.remove():
Iterator<String> it = items.iterator();
while (it.hasNext()) {
if (it.next().equals("remove-me")) it.remove();
}
// CORRECT 2 — removeIf() (cleanest):
items.removeIf(s -> s.equals("remove-me"));
// CORRECT 3 — backward index loop:
for (int i = items.size() - 1; i >= 0; i--) {
if (items.get(i).equals("remove-me")) items.remove(i);
}
// ── Common conversions: ───────────────────────────────────────────────
List<Integer> nums = new ArrayList<>(Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6));
// Deduplicate (to Set and back):
List<Integer> unique = new ArrayList<>(new LinkedHashSet<>(nums)); // preserves order
List<Integer> uniqueUnordered = new ArrayList<>(new HashSet<>(nums));
// Sort a copy without modifying original:
List<Integer> sorted = new ArrayList<>(nums);
Collections.sort(sorted);
// Convert to array:
Integer[] array = nums.toArray(Integer[]::new);
// Stream operations:
List<Integer> filtered = nums.stream()
.filter(n -> n > 3)
.distinct()
.sorted()
.collect(Collectors.toList());
System.out.println(filtered); // [4, 5, 6, 9]Related Topics in Collections Framework
Collections Overview
The Java Collections Framework (JCF) is a unified architecture for representing and manipulating groups of objects. It provides a set of interfaces that define the operations a collection must support, a set of abstract classes that provide partial implementations, and a set of concrete implementations optimised for different use cases. Every Java developer uses collections daily — lists for sequences, sets for uniqueness, maps for key-value pairs, and queues for ordering — and choosing the right implementation for the right use case is one of the most fundamental practical skills in Java.
Iterable
Iterable<E> is the root interface of the Java Collections hierarchy. Any class that implements Iterable can be used in a for-each loop. It declares a single abstract method: iterator(), which returns an Iterator<E> that the for-each loop uses to traverse the elements. Implementing Iterable is all that is required to make a custom data structure work with Java's enhanced for loop, the Stream API, and any method that accepts an Iterable.
Collection Interface
Collection<E> is the root interface of the main collection hierarchy, extending Iterable<E>. It defines the common operations that all collection types must support: adding elements, removing elements, checking containment, querying size, clearing, converting to an array, and bulk operations. List, Set, and Queue all extend Collection. Map does not extend Collection because a map operates on key-value pairs rather than individual elements.
LinkedList
LinkedList is a doubly-linked list implementation of both the List and Deque interfaces. Each element is stored in a separate Node object that holds the element value plus references to the previous and next nodes in the chain. LinkedList provides O(1) insertion and removal at the beginning and end of the list, making it suitable as a stack, queue, or deque. However, it provides O(n) random access by index, making it unsuitable for indexed access patterns.