Java List Methods: A Practical Guide
java list methods: Explore the core methods of java.util.List, their behavior, and how to choose between ArrayList and LinkedList for your use case.
When working with Java collections, the List interface is one of the most frequently used abstractions. Understanding java list methods helps you write cleaner, more predictable code. This article covers the essential methods, their runtime behavior, and the tradeoffs between common implementations.
The List Interface and Its Core Methods
The List interface extends Collection and defines an ordered sequence of elements. Unlike sets, lists allow duplicate elements and provide positional access. The methods you call on a List reference are the same regardless of the underlying implementation, but their performance characteristics differ significantly.
Every List implementation must support the standard operations: adding, removing, reading, searching, and iterating. The interface also includes methods for bulk operations, sublist views, and list-specific iterators. Knowing which method to use in a given situation reduces unnecessary code and avoids subtle bugs.
Adding and Removing Elements
Adding elements is straightforward with add(E element) and add(int index, E element). The first appends to the end; the second inserts at a specific position, shifting subsequent elements to the right. For example:
List<String> names = new ArrayList<>(); names.add("Alice"); names.add(0, "Bob"); // inserts at index 0
Removing works through remove(int index) or remove(Object o). The index-based version returns the removed element, while the object-based version returns a boolean indicating whether an equal element was found. When removing by index, all elements after that index shift left. This shift is the reason ArrayList removal near the beginning is slower than removal from the end.
clear() removes all elements, and removeAll(Collection<?> c) removes every element that appears in the given collection. These bulk operations are convenient but can be expensive on large lists because they often require scanning the entire list.
Reading and Searching Elements
Positional access is the primary strength of a list. get(int index) returns the element at that position, and set(int index, E element) replaces it. For an ArrayList, these operations run in constant time because the backing array provides direct indexing. For a LinkedList, they run in linear time because the list must be traversed from the head or tail.
Searching for an element uses indexOf(Object o) and lastIndexOf(Object o), which return the first or last occurrence index, or -1 if not found. These methods perform a linear scan, so their cost is proportional to the list size. If you frequently search by value, a Set or Map is usually a better choice than a list.
The contains(Object o) method also performs a linear scan. It is inherited from Collection and returns a boolean. For large lists, repeated contains calls can become a performance bottleneck. If membership checks are common, consider using a HashSet for the lookup while keeping the list for ordering.
Iterating Over a List
There are several ways to iterate over a list. The enhanced for loop is the most readable:
for (String name : names) { System.out.println(name); }
Behind the scenes, this uses an Iterator obtained from iterator(). The List interface also provides listIterator(), which supports bidirectional traversal and modification during iteration. You can move forward with next(), backward with previous(), and replace the last returned element with set(E element).
When you need to remove elements while iterating, use the iterator's remove() method instead of calling list.remove() directly. Direct removal during a for-each loop throws ConcurrentModificationException because the iterator's expected mod count no longer matches the list's actual modification count. The iterator's remove() method updates both consistently.
If you need to process elements in parallel, the parallelStream() method from the Collection interface can be used, but be careful with shared state. Lists are not thread-safe by default, and parallel streams do not change that.
Sorting and Reordering
Sorting a list is common, and the List interface includes a default sort(Comparator<? super E> c) method. It sorts the list in place using an efficient algorithm. For example:
List<Integer> numbers = new ArrayList<>(List.of(3, 1, 2)); numbers.sort(Comparator.naturalOrder());
The method modifies the list directly and does not return a new list. If you need a sorted copy, create a new list first and then sort it.
For reversing the order, Collections.reverse(List<?> list) works on any list. Shuffling is done with Collections.shuffle(List<?> list). These are utility methods from java.util.Collections rather than instance methods, but they are essential for list manipulation.
Converting Between List and Array
The toArray() method converts a list to an array. The no-argument version returns an Object[], which is often not what you want. The parameterized version toArray(T[] a) returns an array of the specified type:
String[] array = names.toArray(new String[0]);
Using new String[0] is a common idiom. The list creates a new array of the same type if the provided array is too small. Some developers prefer new String[names.size()] for a slight performance benefit, but modern JVMs make the zero-length version equally efficient in most cases.
Converting an array to a list is done with Arrays.asList(T... a). Note that this returns a fixed-size list backed by the array. You cannot add or remove elements, but you can call set() to modify existing elements. If you need a fully modifiable list, wrap it in a new ArrayList:
List<String> modifiable = new ArrayList<>(Arrays.asList(array));
Performance: ArrayList vs LinkedList
The two most common List implementations are ArrayList and LinkedList. Their performance characteristics differ in ways that affect which methods you should use.
ArrayList uses a resizable array. get(int index) and set(int index, E element) run in constant time because they directly index the array. Adding to the end is amortized constant time, but inserting or removing at an arbitrary index requires shifting elements, which is linear in the number of elements after the index.
LinkedList uses doubly-linked nodes. Adding and removing at either end is constant time, and inserting at a known position is constant time if you already hold the node. However, get(int index) requires traversing from the head or tail, so it runs in linear time. This makes LinkedList unsuitable for random access patterns.
In practice, ArrayList is the better default for most use cases. It has better cache locality, lower memory overhead, and faster iteration. LinkedList only makes sense when you frequently insert or delete at the beginning of the list and you do not need random access. Even then, ArrayDeque is often a better choice for queue-like behavior.
Thread Safety and Synchronized Lists
Neither ArrayList nor LinkedList is thread-safe. If multiple threads modify a list concurrently, you must synchronize access externally. The Collections.synchronizedList(List<T>) method returns a thread-safe wrapper that synchronizes every method. For example:
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
This wrapper synchronizes individual method calls, but compound operations like contains followed by add are not atomic. You must synchronize on the list object manually for such sequences. Also, iterating over a synchronized list requires manual synchronization because the iterator is not thread-safe.
For concurrent read-heavy scenarios, CopyOnWriteArrayList is an alternative. It copies the entire backing array on every modification, making writes expensive but reads lock-free. This is useful when reads vastly outnumber writes, such as in event listener lists.
When choosing a list implementation, consider the access pattern and concurrency requirements. A plain ArrayList with external synchronization is often sufficient for single-threaded or externally synchronized code. CopyOnWriteArrayList provides better read concurrency at the cost of write performance. The right choice depends on your specific workload, not on a general rule.
The List interface also includes subList(int fromIndex, int toIndex), which returns a view of a portion of the list. Modifications to the sublist affect the original list. This can be useful for range operations, but be aware that structural modifications to the original list invalidate the sublist and may cause ConcurrentModificationException.
Understanding java list methods goes beyond memorizing signatures. It involves knowing when to use each method, how the implementation affects runtime behavior, and how to avoid common pitfalls like concurrent modification or unexpected fixed-size views. With this foundation, you can write list code that is both efficient and maintainable.