Java ArrayList indexOf: Finding Element Positions
java arraylist indexof: Learn how to use ArrayList indexOf to find element positions, understand its equals-based search, handle nulls, and avoid performance pitfalls.
The java arraylist indexof method is the standard way to locate the first occurrence of an element in an ArrayList. It returns the zero-based index of the first matching element, or -1 if the element is not present. The method relies on equals to compare elements, not on reference identity. That single detail drives most of its behavior, including how it works with custom objects and how it handles null.
How indexOf Determines a Match
When you call indexOf(element), the ArrayList iterates through its internal array from index 0 upward. For each stored element, it calls element.equals(candidate). The first candidate for which that call returns true yields the index. If the loop completes without a match, the method returns -1.
This means the method is symmetric with respect to the argument: list.indexOf(obj) checks whether obj is equal to any element in the list, not whether the list contains the exact same object reference. For immutable types like String and the wrapper classes, equality is well-defined, so the behavior is intuitive.
Using indexOf with Strings and Wrappers
A typical use case is searching for a string in a list of strings:
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie")); int index = names.indexOf("Bob"); System.out.println(index); // 1
If the element does not exist, you get -1:
int missing = names.indexOf("David"); System.out.println(missing); // -1
The same logic applies to wrapper types like Integer and Double. Because these classes override equals to compare values, indexOf works as expected:
List<Integer> numbers = new ArrayList<>(List.of(10, 20, 30)); int pos = numbers.indexOf(20); // 1
Note that autoboxing converts the primitive 20 to an Integer before the call, so the comparison is value-based.
Custom Objects: The Role of equals
For custom classes, indexOf only works correctly if the class overrides equals (and ideally hashCode) to define meaningful equality. Without an override, Object.equals performs reference comparison, so two distinct instances with identical field values will not match.
Consider this class without an equals override:
public class User { private String email; public User(String email) { this.email = email; } } List<User> users = new ArrayList<>(); users.add(new User("alice@example.com")); User search = new User("alice@example.com"); int idx = users.indexOf(search); // -1, because references differ
To make indexOf find the user by email, override equals (and hashCode for consistency):
@Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof User)) return false; User user = (User) o; return email.equals(user.email); } @Override public int hashCode() { return email.hashCode(); }
Now users.indexOf(search) returns the correct index. The same principle applies to any domain object you store in an ArrayList.
Handling null Elements
ArrayList permits null elements. The indexOf method handles null gracefully by checking element == null before calling equals. This means you can search for null directly:
List<String> list = new ArrayList<>(); list.add("first"); list.add(null); list.add("third"); int nullIndex = list.indexOf(null); // 1
This is a common edge case that often surprises developers who assume indexOf will throw a NullPointerException. It will not, as long as the list itself contains null.
indexOf vs lastIndexOf
The ArrayList class also provides lastIndexOf, which searches backward from the end and returns the last occurrence. The two methods share the same equality logic but differ in traversal direction and the index returned.
| Method | Search direction | Returns |
|---|---|---|
indexOf | Forward from 0 | First matching index |
lastIndexOf | Backward from end | Last matching index |
If your list contains duplicates, indexOf gives the earliest position and lastIndexOf gives the latest. For lists without duplicates, both return the same index. Choose based on which occurrence you actually need.
Performance Considerations
indexOf performs a linear scan, so its time complexity is O(n) in the worst case. For a list of size n, the method may compare every element before finding a match or concluding that the element is absent. This is acceptable for small lists or infrequent lookups, but it becomes a bottleneck when you repeatedly search a large list.
If you need to look up elements by value frequently, consider using a HashMap to map values to their indices. Building the map costs O(n) once, and each lookup becomes O(1). For example:
List<String> list = List.of("a", "b", "c"); Map<String, Integer> indexMap = new HashMap<>(); for (int i = 0; i < list.size(); i++) { indexMap.put(list.get(i), i); } // Later: Integer idx = indexMap.get("b");
This tradeoff is worthwhile only if the list is large and lookups are frequent. For one-off searches, the simplicity of indexOf is preferable.
Another subtle point: indexOf does not depend on the list's capacity, only its size. The internal array may be larger than the logical size, but the method only iterates over the actual elements.
Common Pitfalls and Edge Cases
One frequent mistake is assuming indexOf uses reference equality. As discussed, it uses equals. If you store objects that do not override equals, you may get -1 even when a logically equal object exists. Always verify that your model classes implement value-based equality when you plan to search them.
Another edge case involves using indexOf on a sublist view. The subList method returns a view backed by the original list, and indexOf on that view operates on the sublist's range, not the whole list. The returned index is relative to the sublist, not the original list:
List<String> list = new ArrayList<>(List.of("a", "b", "c", "d")); List<String> sub = list.subList(1, 3); // ["b", "c"] int idx = sub.indexOf("c"); // 1, not 2
If you need the index in the original list, add the sublist's start offset.
Finally, remember that indexOf returns the first match. If your list contains duplicate values and you need all positions, you must iterate manually or use a loop that calls indexOf starting from the last found index plus one. For example:
List<String> list = new ArrayList<>(List.of("x", "y", "x", "z")); int fromIndex = 0; while (true) { int idx = list.indexOf("x"); if (idx == -1) break; System.out.println("Found at " + idx); fromIndex = idx + 1; if (fromIndex >= list.size()) break; }
This pattern is rarely needed, but it clarifies the semantics of indexOf when duplicates exist.
When to Avoid indexOf
If your primary need is to check whether an element exists rather than to find its position, contains is a clearer choice. It uses the same equality logic and returns a boolean, making the intent explicit. For frequent membership tests, a HashSet provides O(1) lookups and is more efficient than repeatedly calling indexOf on a large list.
For sorted lists, consider Collections.binarySearch, which runs in O(log n) time. However, binarySearch requires the list to be sorted and uses a different comparison mechanism (compareTo), so it is not a drop-in replacement for indexOf.
Choose indexOf when you need the actual position of an element and the list is small or the search is infrequent. For performance-critical code, measure the actual usage pattern and switch to a map-based index only when profiling shows that indexOf is a bottleneck.
Understanding how indexOf works under the hood—its reliance on equals, its linear scan, and its handling of null—lets you use it correctly and avoid surprising results in production code.