Java List indexOf: Syntax, Behavior, and Performance
java list indexof: How Java List.indexOf works: equals-based matching, the -1 return value, null handling, and performance tradeoffs between ArrayList and LinkedList.
For the common java list indexof lookup, the List interface provides indexOf, which returns the position of the first element that matches the argument, or -1 when no match exists. It is the most direct way to answer "where does this element appear in this list?" without writing a manual loop.
List<String> names = new ArrayList<>(List.of("ada", "grace", "linus")); int position = names.indexOf("grace"); System.out.println(position); // 1
The method is defined on the List interface, so every implementation — ArrayList, LinkedList, Vector, and unmodifiable lists — provides it. The behavior is consistent across implementations, but the cost of finding the index is not.
How indexOf Decides What Matches
indexOf does not compare references. It relies on the equals method of the elements in the list. For each element at position i, the implementation checks:
if (o == null ? get(i) == null : o.equals(get(i))) { return i; }
That is the exact contract from the List interface. When the argument is non-null, the argument's equals method is called against each element. When the argument is null, the implementation checks whether any element is null.
This means two elements are considered equal if their equals method says so, regardless of whether they are the same object instance. Two distinct String objects with the same characters will match:
String first = new String("ada"); String second = new String("ada"); List<String> list = new ArrayList<>(List.of(first)); int position = list.indexOf(second); // 0, not -1
The two strings are different objects, but String.equals compares content, so indexOf finds a match.
The Meaning of -1 and How It Relates to contains
When no element matches, indexOf returns -1. That is not a valid list index, so it is safe to use directly in conditionals:
int position = list.indexOf("missing"); if (position >= 0) { // element exists at position } else { // element does not exist }
A common mistake is to use the return value as a boolean directly:
if (list.indexOf("missing")) { // compile error: int cannot be converted to boolean }
Java does not treat nonzero integers as true, so this fails to compile. Compare the result against -1 or use contains when you only need to know whether the element exists and do not need its position. contains performs the same linear scan but returns a boolean, which reads more clearly when the index is irrelevant.
Using indexOf with Custom Objects
For a custom class, indexOf only works predictably if the class overrides equals (and, by contract, hashCode). Without an override, Object.equals compares references, so indexOf will only match the exact same instance.
public final class Task { private final int id; private final String title; public Task(int id, String title) { this.id = id; this.title = title; } @Override public boolean equals(Object other) { if (this == other) return true; if (!(other instanceof Task)) return false; Task that = (Task) other; return id == that.id && title.equals(that.title); } @Override public int hashCode() { return 31 * id + title.hashCode(); } }
With that override, this works:
List<Task> tasks = new ArrayList<>(); tasks.add(new Task(1, "review PR")); int position = tasks.indexOf(new Task(1, "review PR")); // 0
Without the override, the same call returns -1 because the new Task instance is not the same reference as the one stored in the list. If you are implementing value-based equality for domain objects, override both equals and hashCode; collections such as HashSet and HashMap depend on the hash contract even when indexOf itself does not use the hash.
indexOf with Null Elements
The method handles null arguments explicitly. If the list contains a null element, indexOf(null) returns its position:
List<String> list = new ArrayList<>(Arrays.asList("a", null, "b")); int position = list.indexOf(null); // 1
If the list has no null elements, it returns -1. The null branch in the interface contract exists specifically to avoid calling equals on a null argument, which would throw a NullPointerException.
Performance: ArrayList versus LinkedList
The cost of indexOf depends entirely on the list implementation. Both ArrayList and LinkedList scan elements sequentially, so the worst-case time is O(n). The difference is in the cost of each step.
ArrayList stores elements in a contiguous array, so get(i) is O(1) and the scan is a tight loop over array positions. LinkedList stores elements as nodes with next pointers, so advancing from one element to the next requires following a reference. The scan is still linear, but each step has more overhead and worse cache locality. For a large list where indexOf is called frequently, ArrayList is the better choice.
There is no built-in way to find an index faster than O(n) on a plain List. If you need repeated lookups by value, a HashMap keyed by the element gives O(1) average lookups:
Map<String, Integer> indexByValue = new HashMap<>(); for (int i = 0; i < list.size(); i++) { indexByValue.putIfAbsent(list.get(i), i); }
This trades memory for lookup speed and is worth considering only when the list is stable and lookups dominate.
lastIndexOf and Other Alternatives
The List interface also provides lastIndexOf, which scans from the end and returns the position of the last matching element:
List<String> list = new ArrayList<>(List.of("a", "b", "a")); int first = list.indexOf("a"); // 0 int last = list.lastIndexOf("a"); // 2
lastIndexOf has the same O(n) cost and the same equals-based matching rules.
For a sorted list, Collections.binarySearch finds an index in O(log n) time. The list must be sorted according to the same comparator or natural ordering used by the search. The return value is not always a valid index: when no match exists, it returns -(insertion point) - 1, a negative value that encodes where the element would be inserted. That encoding is useful for insertion logic but easy to misinterpret if you expect -1.
Common Mistakes and Edge Cases
One recurring mistake is assuming indexOf uses == for primitives or identity for objects. It does not; the contract is equals-based, so any class that does not override equals will behave by reference identity.
Another edge case is calling indexOf on a list while structurally modifying it. Like most List methods, indexOf is not atomic with respect to concurrent modification. If another thread adds or removes elements during the scan, the behavior depends on the implementation. ArrayList may throw ConcurrentModificationException in some paths, but indexOf does not use the mod-count check that iterators use, so the result can be stale or inconsistent. For concurrent access, use a thread-safe collection such as CopyOnWriteArrayList or synchronize access externally.
Finally, remember that indexOf returns the first match. If a list contains duplicate values, only the earliest position is returned. To find all positions, you need a loop:
String target = "ada"; List<Integer> positions = new ArrayList<>(); for (int i = 0; i < list.size(); i++) { if (target.equals(list.get(i))) { positions.add(i); } }
That loop is the only way to collect every occurrence, since neither indexOf nor lastIndexOf reports intermediate matches.