☕ 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
LinkedHashMap
LinkedHashMap extends HashMap and maintains a doubly-linked list running through all its entries, preserving the order in which entries were inserted. Iteration over a LinkedHashMap always returns entries in insertion order. Optionally, it can be constructed in access-order mode where entries are ordered by most-recently accessed, making it the perfect foundation for implementing a Least Recently Used (LRU) cache. LinkedHashMap has slightly higher memory overhead than HashMap due to the extra linked list pointers.
TreeMap
TreeMap is a Red-Black tree implementation of the NavigableMap interface. It stores key-value pairs in sorted order — either the natural ordering of keys (requiring them to implement Comparable) or a custom Comparator provided at construction. All basic operations (get, put, remove, containsKey) are O(log n). TreeMap provides rich navigation operations: finding the closest key, extracting sub-maps, and headMap/tailMap views.
Hashtable
Hashtable is a legacy hash table implementation that predates the Collections Framework. Like Vector for lists, Hashtable was Java's original hash map from Java 1.0. It is thread-safe because all its methods are synchronised, but it does not permit null keys or null values. Hashtable has been retrofitted to implement the Map interface but should never be used in new code — HashMap is the non-concurrent replacement and ConcurrentHashMap is the concurrent replacement.
ConcurrentHashMap
ConcurrentHashMap is a thread-safe, high-performance hash map optimised for concurrent access. Unlike Hashtable and synchronised HashMap (which use a single lock), ConcurrentHashMap uses a more sophisticated concurrency mechanism — in Java 8+, it uses CAS (compare-and-swap) operations and per-bucket synchronisation — allowing multiple threads to read and write to different parts of the map simultaneously without blocking each other. It does not permit null keys or null values.