Java List get: Usage, Performance, and Edge Cases
java list get: Learn how to use Java List.get, what it costs on different implementations, and how to avoid IndexOutOfBoundsException.
java list get requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When you need to retrieve an element at a specific position from a Java List, the get method is the standard API. Its behavior and cost depend on the underlying implementation, and a careless call can throw IndexOutOfBoundsException. This article covers the contract of get, how it behaves on ArrayList and LinkedList, its performance characteristics, and when iteration is a better choice.
The get Method and Its Contract
The List interface declares E get(int index). The method returns the element at the specified position, with zero-based indexing. The contract states that it throws IndexOutOfBoundsException if the index is out of range, meaning index < 0 or index >= size(). This is a checked-at-runtime condition, not a compile-time one, so you must guard against invalid indices in your code.
List<String> names = new ArrayList<>(); names.add("Alice"); names.add("Bob"); String first = names.get(0); // "Alice"
Because get is part of the List interface, any implementation must honor this contract. However, the internal mechanism differs significantly between implementations, which directly affects performance.
How get Behaves on ArrayList vs LinkedList
ArrayList stores elements in a contiguous array. get(index) performs a direct array access after a bounds check, so it runs in constant time, O(1). The JVM translates the index into a memory offset, making it extremely fast.
LinkedList stores elements in a doubly-linked chain. To reach index i, the implementation traverses from the head or tail, whichever is closer, until it finds the node. This makes get a linear-time operation, O(n), because each call walks the list. For large lists, repeated get calls in a loop can cause severe performance degradation.
List<Integer> numbers = new LinkedList<>(); for (int i = 0; i < 100000; i++) { numbers.add(i); } // This loop is O(n^2) because each get traverses the list for (int i = 0; i < numbers.size(); i++) { int value = numbers.get(i); }
The same loop on an ArrayList is O(n) overall because each get is constant time. Understanding this difference is crucial when you need to access elements by index frequently.
Index Bounds and IndexOutOfBoundsException
The most common error with java list get is passing an invalid index. The exception is thrown immediately, and it includes a message like Index 5 out of bounds for length 3. The check happens before any element is accessed, so you cannot accidentally read memory outside the list.
Consider a scenario where the list size changes between a size check and a get call. In single-threaded code, this is usually a logic error. In concurrent code, another thread may modify the list, making the index stale. The List interface does not guarantee thread safety, so you must synchronize externally or use a thread-safe implementation like CopyOnWriteArrayList if concurrent modification is possible.
A safer pattern is to check the index explicitly before calling get:
if (index >= 0 && index < list.size()) { return list.get(index); } else { return null; // or throw a custom exception }
This avoids the exception when you expect invalid indices as part of normal flow. If an invalid index indicates a programming error, letting the exception propagate is usually better.
Performance Cost of get on Different Implementations
The performance of get is determined by the data structure, not by the method signature. For ArrayList, the cost is a bounds check plus an array read. For LinkedList, the cost is a traversal that can involve many pointer dereferences and cache misses.
In practice, the difference becomes visible when you access elements in a loop. A common mistake is using indexed get on a LinkedList when iteration would be far more efficient. The List interface also provides an Iterator or a for-each loop that avoids repeated traversal.
// Efficient for LinkedList for (String name : names) { System.out.println(name); }
The for-each loop uses the list's iterator(), which advances node by node without restarting from the head. For ArrayList, iteration and indexed access have similar performance, but iteration is still preferable because it is more readable and less error-prone.
If you need random access by index, ArrayList is the appropriate choice. If your primary operations are insertion and deletion at the ends or iteration, LinkedList may be suitable, but its get is rarely the right tool.
When to Use get Instead of Iteration
Use get when you need a specific element by its position, such as the first or last element, or when you are implementing algorithms that require random access, like binary search. In those cases, ArrayList is the natural fit.
Avoid using get in a loop when you do not actually need the index. The for-each loop is clearer and avoids the performance trap on LinkedList. If you need the index for another purpose, such as comparing adjacent elements, an indexed loop is acceptable, but be aware of the implementation cost.
// Need index for comparing neighbors for (int i = 0; i < list.size() - 1; i++) { if (list.get(i).compareTo(list.get(i + 1)) > 0) { // out of order } }
In this case, get is necessary because you need two specific positions. The performance is fine if the list is an ArrayList. If you are unsure about the implementation type, you can convert the list to an ArrayList once before the loop, but that adds a copy cost.
Handling Null Elements and Empty Lists
A List can contain null elements. Calling get on an index that holds null returns null, which is valid. This can lead to NullPointerException if you immediately dereference the result. Always check for null if the list may contain null values, or use Objects.requireNonNull when the contract forbids nulls.
An empty list has size() == 0. Any call to get with any index will throw IndexOutOfBoundsException. There is no special getOrDefault method on List (unlike Map). If you need a default value for a missing index, you must check the size first or use a helper method.
public static <T> T getOrDefault(List<T> list, int index, T defaultValue) { if (index >= 0 && index < list.size()) { return list.get(index); } return defaultValue; }
This utility is useful when you are reading data from a list that may be shorter than expected, such as parsing command-line arguments or configuration values.
Choosing a List Implementation Based on Access Patterns
Your choice of List implementation should align with how you access elements. If you frequently call get by index, ArrayList is the only reasonable choice. LinkedList offers O(1) insertion at the beginning and end, but its get is O(n), making it unsuitable for random access.
For mixed workloads, consider the dominant operation. If you mostly iterate and occasionally need the first or last element, LinkedList can be efficient, but you can also use ArrayDeque for those operations if you do not need the List interface. When in doubt, ArrayList is the default because its overall performance is predictable and its memory footprint is compact.
The get method is a simple API, but its behavior is tightly coupled to the underlying data structure. Knowing how your list is implemented helps you avoid performance pitfalls and write code that behaves consistently under load.