List Interface and ArrayList in Java
What does this code leave in the list: List<Integer> nums = new ArrayList<>(List.of(1, 2, 3)); nums.remove(1);? Most beginners expect the number 1 to disappear and get [2, 3], but the list becomes [1, 3]. The argument is an int, so Java calls remove(int index) and deletes the element at index 1. To remove the value itself, you need nums.remove(Integer.valueOf(1)).
The List interface in Java is an ordered collection that keeps elements in insertion order, allows duplicates (and, in most implementations, null), and gives access to every element by an index from 0 to size() - 1. ArrayList is the most widely used implementation of List: a resizable array that grows automatically when you add elements.
What is the List interface in Java
The List<E> interface from the java.util package describes a sequence of elements where every element has a position. It extends Collection, so it inherits add(), remove(), contains() and size() (see Collection Interface in Java), and adds operations that work with an index.
Every List follows the same rules:
- Order is kept: elements are returned in the order they were added or inserted.
- Duplicates are allowed: the same value can appear many times, unlike in a
Set. - Index access: numbering starts at zero, like in an array.
nullis allowed inArrayListandLinkedList, but not in lists created withList.of().
Most methods that List adds to Collection take or return an index: add(int, E), get(int), set(int, E), remove(int), indexOf(), lastIndexOf(), subList() and listIterator(). Where List sits among Set, Queue and Map is shown in Collection hierarchy in Java; the general idea of a list as a data structure is explained in Data Structures in Java.
List implementations: ArrayList, LinkedList, Vector, List.of()
The main implementations of List in Java are ArrayList (a resizable array), LinkedList (a doubly linked list), the legacy synchronized Vector, the thread-safe CopyOnWriteArrayList and the immutable lists returned by List.of(). For most tasks you choose ArrayList: it has the fastest index access and the smallest memory overhead.
| Implementation | Inside | get(i) | Insert/remove in the middle | Notes |
|---|---|---|---|---|
ArrayList | Object[] array | O(1) | O(n), elements are shifted | Default choice; not synchronized |
LinkedList | Doubly linked nodes | O(n) | O(1) at a known node, but finding the node is O(n) | Also implements Deque; rarely faster than ArrayList in practice |
Vector (and Stack) | Object[] array | O(1) | O(n) | Every method is synchronized; legacy since Java 1.0, avoid in new code |
CopyOnWriteArrayList | Array copied on every write | O(1) | O(n) plus an array copy | Thread-safe; good when reads dominate over writes |
List.of(), List.copyOf() | Immutable array | O(1) | Not allowed: UnsupportedOperationException | Java 9/10; null is forbidden |
Arrays.asList() | The original array | O(1) | Size cannot change; set() works | A fixed-size view of an array |
How the implementations compare across the whole framework is covered in Choosing an implementation, the rules for immutable lists in Immutable collections, and concurrent lists in Thread-safe collections in Java.
How to create an ArrayList and a List
You create an ArrayList with one of three constructors: new ArrayList<>() for an empty list, new ArrayList<>(capacity) for an empty list with a pre-sized internal array, and new ArrayList<>(collection) for a copy of another collection. For a ready-made immutable list use the factory List.of(...). By convention the variable is declared with the interface type List.
| Way | Result | When to use |
|---|---|---|
new ArrayList<>() | Empty modifiable list; the internal array of 10 slots is allocated on the first add() | The usual case |
new ArrayList<>(int capacity) | Empty list with an array of capacity slots; a negative value throws IllegalArgumentException | You roughly know how many elements are coming |
new ArrayList<>(Collection<? extends E> c) | A modifiable copy of the elements of c in its iteration order | You need an independent copy, or a modifiable list with initial values |
List.of(e1, e2, ...) | Immutable list (Java 9) | Constants, test data, method arguments |
import java.util.ArrayList;
import java.util.List;
public class ArrayListCreateExample {
public static void main(String[] args) {
List<String> empty = new ArrayList<>();
List<String> withCapacity = new ArrayList<>(100);
List<String> copy = new ArrayList<>(List.of("Java", "Kotlin"));
List<String> fixed = List.of("Java", "Kotlin");
copy.add("Scala");
System.out.println(empty.size()); // 0
System.out.println(withCapacity.size()); // 0 - capacity is 100, but there are no elements
System.out.println(copy); // [Java, Kotlin, Scala]
System.out.println(fixed); // [Java, Kotlin]
// fixed.add("Scala"); -> UnsupportedOperationException
}
} Output:
0
0
[Java, Kotlin, Scala]
[Java, Kotlin] Elements of an ArrayList must be objects. To store numbers use wrapper classes, as in List<Integer>; Java converts values automatically (autoboxing and unboxing). A primitive array cannot be passed to List.of() or Arrays.asList() directly — see how to convert an int array to a list.
List and ArrayList methods
The main ArrayList methods are: add() appends an element, get() reads by index, set() replaces, remove() deletes by index or by value, indexOf() and contains() search, and size() returns the number of elements. Some methods are declared in Collection and some in List itself; the table shows where each comes from and what happens in the edge case.
| Method | Declared in | What it does | Edge case |
|---|---|---|---|
boolean add(E e) | Collection | Appends to the end, returns true | List.of() — UnsupportedOperationException |
void add(int index, E e) | List | Inserts and shifts the following elements right | index == size() appends; index > size() — IndexOutOfBoundsException |
boolean addAll(Collection c)boolean addAll(int index, Collection c) | Collection / List | Adds all elements of c at the end or from position index | Returns true if the list changed; false for an empty c |
E get(int index) | List | Returns the element at the index | index < 0 or ≥ size() — IndexOutOfBoundsException |
E set(int index, E e) | List | Replaces an element and returns the old value | Never adds a position: set(0, e) on an empty list throws |
E remove(int index) | List | Removes the element at the index and returns it; the tail shifts left | For List<Integer>, remove(1) removes by index, not the number 1 |
boolean remove(Object o) | Collection | Removes the first occurrence of o | No match — false, no exception |
int indexOf(Object o)int lastIndexOf(Object o) | List | Index of the first / last occurrence | Not found — -1; compares with equals() |
boolean contains(Object o) | Collection | Checks whether the element is present | Linear search, O(n); List.of().contains(null) — NullPointerException |
int size(), boolean isEmpty() | Collection | Number of elements / whether the list is empty | Returns the number of elements, not the array capacity |
boolean removeIf(Predicate p) | Collection, Java 8 | Removes all elements that match the condition | Returns true if anything was removed |
boolean removeAll(Collection c)boolean retainAll(Collection c) | Collection | Removes the elements that are in c / keeps only them | All occurrences are affected, duplicates included |
void clear() | Collection | Removes all elements | Array capacity does not shrink (use trimToSize()) |
List<E> subList(int from, int to) | List | A view of elements from from to to - 1 | Changes show in both lists; from > to — IllegalArgumentException |
void sort(Comparator c) | List, Java 8 | Sorts the list in place | sort(null) uses natural ordering |
ListIterator<E> listIterator() | List | A bidirectional iterator | Lets you add and replace elements while iterating |
E getFirst(), E getLast(), List<E> reversed() | SequencedCollection, Java 21 | First / last element, reversed view | Empty list — NoSuchElementException (for getFirst() and getLast()); these methods do not compile on Java 17 |
The last row comes from the SequencedCollection interface added in Java 21: on older versions the first and last elements are list.get(0) and list.get(list.size() - 1). Why every List got these methods is explained in Collection hierarchy in Java.
Adding and replacing elements: add(), set(), addAll()
You add an element to an ArrayList with add(e), which appends it to the end, or with add(index, e), which inserts it at a position and shifts the rest to the right. The set(index, e) method adds nothing: it replaces an existing element and returns the old one. To add several elements at once use addAll().
import java.util.ArrayList;
import java.util.List;
public class ArrayListAddExample {
public static void main(String[] args) {
List<String> langs = new ArrayList<>();
langs.add("Java");
langs.add("Kotlin");
langs.add("Scala");
langs.add(1, "Groovy");
String old = langs.set(0, "Java 21");
System.out.println(old);
System.out.println(langs);
langs.addAll(List.of("Go", "Rust"));
System.out.println(langs);
langs.addAll(2, List.of("C", "D"));
System.out.println(langs);
System.out.println(langs.size());
}
} Output:
Java
[Java 21, Groovy, Kotlin, Scala]
[Java 21, Groovy, Kotlin, Scala, Go, Rust]
[Java 21, Groovy, C, D, Kotlin, Scala, Go, Rust]
8 "Groovy" took position 1 and pushed "Kotlin" and "Scala" to the right. set(0, ...) returned the old value "Java" and did not change the size. addAll(2, ...) inserted both new elements as a block starting at index 2.
How ArrayList works: capacity vs size
Inside, ArrayList keeps two things: an Object[] array and a counter size. The length of the array is the capacity; size is the number of elements actually added. When the array is full, ArrayList allocates a new one about 1.5 times larger and copies the elements over, invisibly for the programmer.
- Since Java 8,
new ArrayList<>()does not allocate memory at once: the array is empty until the firstadd()creates one with 10 slots. - After that, capacity grows by
old + old / 2: 10, 15, 22, 33, 49. - If you know the number of elements in advance, pass it to the constructor or call
ensureCapacity(n)to avoid repeated copying.trimToSize()releases unused slots. Both methods exist only on theArrayListclass, not on theListinterface.
The array is declared as Object[], not E[]: generic type information is erased at runtime, so the list stores plain object references, and primitives go in only through wrapper classes. A common mistake is to treat capacity as ready-to-use slots:
import java.util.ArrayList;
import java.util.List;
public class CapacityVsSizeExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>(10);
System.out.println(list.size());
list.set(0, "Java");
}
} Output:
0
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0 The number in the constructor is the capacity of the internal array. Until you call add(), the list is empty and every index is invalid. The right way is to call add("Java") first and use set() only to replace existing elements.
Which exception to expect
An array throws ArrayIndexOutOfBoundsException, while List methods throw IndexOutOfBoundsException, its parent class. You can add elements far beyond the current capacity; the exception appears only when you use an index below 0 or at or above size().
The structure of the array explains the speed of the operations:
| Operation | Complexity | Why |
|---|---|---|
get(i), set(i, e) | O(1) | Direct access to an array cell |
add(e) at the end | O(1) amortized | There is usually a free slot; rarely the array is copied in O(n) |
remove(size() - 1) | O(1) | Nothing to shift |
add(i, e), remove(i) at the start or in the middle | O(n) | The tail of the array is shifted with System.arraycopy() |
indexOf(), contains(), remove(Object) | O(n) | Linear search that calls equals() |
If these notations are new to you, see Big O notation.
Removing elements: remove(int), remove(Object), removeIf()
You remove an element from an ArrayList by index with remove(int index), by value with remove(Object o) (only the first occurrence goes), or by condition with removeIf(). After a removal from the middle, the elements to the right shift one position to the left. For a List<Integer> the choice between the two remove() overloads depends on the argument type.
import java.util.ArrayList;
import java.util.List;
public class ArrayListRemoveExample {
public static void main(String[] args) {
List<Integer> nums = new ArrayList<>(List.of(10, 20, 30, 20));
nums.remove(1);
System.out.println(nums);
nums.remove(Integer.valueOf(20));
System.out.println(nums);
System.out.println(nums.removeIf(n -> n > 20));
System.out.println(nums);
nums.remove(10);
}
} Output:
[10, 30, 20]
[10, 30]
true
[10]
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index 10 out of bounds for length 1 The first call removed the element at index 1 (the first 20). The second call removed the value 20 because the argument was an Integer object, so the remove(Object) overload was chosen. removeIf() removed 30 in one pass. The last line shows the overload problem from the start of the lesson: remove(10) is an index, and a list of one element has no index 10.
Group removal works the same way for any collection: removeAll(c) removes every occurrence of the elements found in c, and retainAll(c) keeps only them. Their return values and the removeIf() contract are shown in Collection Interface in Java.
Searching a list: indexOf(), lastIndexOf(), contains()
The indexOf(o) method returns the index of the first occurrence of an element in a List, lastIndexOf(o) returns the index of the last one, and both return -1 when the element is absent. contains(o) only answers yes or no. All three scan the list from the start (O(n)) and compare elements with equals().
import java.util.ArrayList;
import java.util.List;
public class ArrayListSearchExample {
public static void main(String[] args) {
List<String> langs = new ArrayList<>(List.of("Java", "Kotlin", "Java", "Scala"));
System.out.println(langs.indexOf("Java"));
System.out.println(langs.lastIndexOf("Java"));
System.out.println(langs.indexOf("Go"));
System.out.println(langs.contains("Kotlin"));
System.out.println(langs.contains("java"));
}
} Output:
0
2
-1
true
false The last result is false because string comparison is case-sensitive. For your own classes the search works only if they override the equals() method; otherwise indexOf() compares references and will not find an object with the same field values. If you search often and order does not matter, a HashSet is faster: its contains() is O(1) on average.
Iterating a List in Java
You can iterate a List in Java in three common ways: a for loop over indexes (when you need the position), a for-each loop (the shortest and most readable), and the forEach() method with a lambda or a method reference (Java 8). For an ArrayList all three run in O(n).
import java.util.ArrayList;
import java.util.List;
public class ArrayListIterateExample {
public static void main(String[] args) {
List<String> langs = new ArrayList<>(List.of("Java", "Kotlin", "Scala"));
for (int i = 0; i < langs.size(); i++) {
System.out.println(i + ": " + langs.get(i));
}
for (String lang : langs) {
System.out.print(lang + " ");
}
System.out.println();
langs.forEach(System.out::println);
}
} Output:
0: Java
1: Kotlin
2: Scala
Java Kotlin Scala
Java
Kotlin
Scala Do not call list.add() or list.remove() inside a for-each loop: the next step of the loop usually fails with ConcurrentModificationException. To delete elements while iterating, use removeIf() (see removeIf() in the Collection lesson) or an explicit Iterator with its own remove() method. On a LinkedList, avoid the index loop: every get(i) there walks the chain of nodes, so the whole pass becomes O(n²).
Sorting a list: list.sort()
To sort a List in Java, call list.sort(comparator). Passing null sorts by natural ordering (the order defined by Comparable), and a Comparator sets any other order. The list is changed in place, so it must be modifiable: for a list from List.of() the call throws UnsupportedOperationException.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class ArrayListSortExample {
public static void main(String[] args) {
List<String> names = new ArrayList<>(List.of("Pete", "anna", "Kate", "Bob"));
names.sort(null);
System.out.println(names);
names.sort(String.CASE_INSENSITIVE_ORDER);
System.out.println(names);
names.sort(Comparator.comparing(String::length).thenComparing(Comparator.reverseOrder()));
System.out.println(names);
}
} Output:
[Bob, Kate, Pete, anna]
[anna, Bob, Kate, Pete]
[Bob, anna, Pete, Kate] Natural ordering of strings puts uppercase letters before lowercase ones, which is why "anna" ended up last. The third call sorts by length first and breaks ties in reverse natural order (so among the four-letter names "anna" comes first, then "Pete", then "Kate"). Collections.sort(list) does the same as list.sort(null); sorting arrays is described in Arrays.sort().
subList(): a view of part of a list
The subList(from, to) method returns a view of the elements from index from up to, but not including, to. It does not copy anything: writes through the view change the original list and the other way round.
List<Integer> nums = new ArrayList<>(List.of(1, 2, 3, 4, 5));
List<Integer> part = nums.subList(1, 4);
System.out.println(part);
part.set(0, 20);
System.out.println(nums);
part.clear();
System.out.println(nums);
nums.add(6);
System.out.println(part.size()); Output:
[2, 3, 4]
[1, 20, 3, 4, 5]
[1, 5]
Exception in thread "main" java.util.ConcurrentModificationException Clearing the view removed that range from the original list. After the original list was changed structurally by add(6), the old view became invalid. To get an independent piece, copy it: new ArrayList<>(nums.subList(1, 4)).
Converting between a List and an array
To convert a List to an array in Java, call list.toArray(new String[0]) or, since Java 11, list.toArray(String[]::new); to go back, copy the array into a new list: new ArrayList<>(Arrays.asList(array)). The three toArray() overloads and the ClassCastException from casting Object[] are covered in toArray() in the Collection lesson. A less known case is passing an array that is longer than the list. What does this code print?
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ListArrayConvertExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>(List.of("C", "A", "B"));
String[] exact = list.toArray(new String[0]);
System.out.println(Arrays.toString(exact));
String[] big = new String[5];
Arrays.fill(big, "X");
String[] result = list.toArray(big);
System.out.println(Arrays.toString(result));
System.out.println(result == big);
List<String> back = new ArrayList<>(Arrays.asList(exact));
back.add("D");
System.out.println(back);
}
} Output:
[C, A, B]
[C, A, B, null, X]
true
[C, A, B, D] If the array passed in is large enough, toArray() does not create a new one: it writes the elements into it, puts null into the cell right after the last element (index 3 here) and returns the same array. The last "X" stays untouched. The null marks where the list ended, but only if the list itself contains no nulls. Passing new String[0] is shorter and, on modern JVMs, no slower than new String[list.size()], so it is the recommended default. Without the new ArrayList<>(...) wrapper, Arrays.asList() would return a fixed-size list and add("D") would throw UnsupportedOperationException — see Immutable collections.
Frequently Asked Questions
What is the difference between an array and an ArrayList in Java?
The length of an array is fixed when it is created, while an ArrayList grows by itself. An array can hold primitives (int[]), while an ArrayList holds only objects (List<Integer> with autoboxing). The length of an array is the field length; the size of a list is the method size(). How to switch between them is shown in Converting between a List and an array.
Why declare List<String> list = new ArrayList<>() instead of ArrayList<String>?
Declaring the variable with the interface type lets you change the implementation in one place, for example to LinkedList, without touching the rest of the code. Methods that accept List also work with any implementation. Use the class type only when you need methods that exist only there, such as ensureCapacity() or trimToSize().
How do I create a list of lists in Java?
Declare it as List<List<Integer>> matrix = new ArrayList<>();, add each inner list separately with matrix.add(new ArrayList<>()), and read an element with matrix.get(row).get(col). Do not build it with Collections.nCopies(3, new ArrayList<>()): all three positions hold the same inner list, so after matrix.get(0).add(1) the whole structure prints [[1], [1], [1]].
How do I get the last element of a List?
Use list.get(list.size() - 1), or list.getLast() on Java 21 and later. Check the list for emptiness first: on an empty ArrayList the first call throws IndexOutOfBoundsException: Index -1 out of bounds for length 0, and getLast() throws NoSuchElementException.
How do I convert a List<String> to a single string?
String.join(", ", list) turns [Java, Kotlin] into Java, Kotlin, while list.toString() keeps the brackets: [Java, Kotlin]. String.join() accepts only CharSequence elements, so for a List<Integer> it does not compile; use list.stream().map(String::valueOf).collect(Collectors.joining(", ")) instead.
Comments