skip to content

How do List.indexOf and List.contains decide whether an element matches, and why does overriding equals matter?

level: middleimportance: must knowfreq 62%

answer

  1. Matches via equals, not ==
  2. Calls o.equals(element)
  3. Default equals = identity → lookups fail
  4. Override equals AND hashCode
  5. indexOf first, lastIndexOf last, O(n)

basics

~20 s

indexOf and contains find an element by comparing with equals, not by reference (==). For a custom class to be found, it must override equals correctly; otherwise the default equals compares object identity and lookups fail unless it is the exact same object.

solid answer

~40 s

List.contains(o) and List.indexOf(o) scan the list in order and use equals to test each element, specifically o.equals(element) (with a null-safe path so null matches null). They do not use ==. So whether a lookup succeeds depends entirely on the element type's equals implementation. The default Object.equals is reference equality, so two distinct-but-logically-equal objects (e.g. two new Point(1,2)) will not be found. To make value-based lookup work you must override equals (and, per the general contract, hashCode too, even though indexOf itself does not hash). indexOf returns the first matching index or -1; lastIndexOf returns the last; contains is essentially indexOf(o) >= 0. Both are O(n) linear scans.

code

java · 11 lines
java
record Point(int x, int y) {} // auto equals/hashCode by components

List<Point> list = new ArrayList<>();
list.add(new Point(1, 2));

boolean found = list.contains(new Point(1, 2)); // true (value equals)
int idx = list.indexOf(new Point(1, 2));        // 0
int missing = list.indexOf(new Point(9, 9));    // -1

// Without a value-based equals (plain class using Object.equals),
// contains(new Point(1,2)) would be false.

go deeper

for a junior

Knows contains/indexOf use equals, so a custom class must override equals to be found, and indexOf returns -1 when absent.

for a middle

Explains it calls o.equals(element) with null handling, knows the override-equals-and-hashCode rule, and that lookups are O(n).

for a senior

Discusses the full equals contract, why hashCode pairs with equals even though List does not use it, and when to switch to a HashSet for membership.

for a principal

Reasons about equality strategy across the codebase (value vs entity equality, records, immutability), API contracts, and performance/data-structure selection at scale.

## The two methods - `boolean contains(Object o)` — does the list hold an element equal to `o`? - `int indexOf(Object o)` — the index of the **first** element equal to `o`, or `-1` if none. `lastIndexOf(o)` returns the **last** such index. ## How matching is decided: equals, not == The `==` operator compares **references** (are these the very same object in memory). `equals` compares **logical value** *if the class defines it that way*. `indexOf`/`contains` use **equals**. The typical implementation is: ``` if (o == null) { for (int i = 0; i < size; i++) if (get(i) == null) return i; } else { for (int i = 0; i < size; i++) if (o.equals(get(i))) return i; } ``` Key points: it calls `o.equals(element)` (the **argument's** equals), and it handles `null` separately so searching for `null` works. ## Why overriding equals matters If your class does **not** override `equals`, it inherits `Object.equals`, which is just `==` (reference identity). Then: ``` class Point { int x, y; Point(int x,int y){this.x=x;this.y=y;} } List<Point> list = new ArrayList<>(); list.add(new Point(1, 2)); list.contains(new Point(1, 2)); // false! different object, default equals ``` The two `Point(1,2)` objects are logically equal but are different instances, so the default `equals` says they are not equal and the search fails. Override `equals` to compare fields: ``` @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Point p)) return false; return x == p.x && y == p.y; } @Override public int hashCode() { return Objects.hash(x, y); } ``` Now `contains(new Point(1,2))` returns `true`. ## The equals contract you must honor `equals` must be reflexive, symmetric, transitive, consistent, and `x.equals(null)` must be false. Also, the general contract says **if you override equals you must override hashCode** so that equal objects have equal hash codes. `indexOf`/`contains` on a `List` do not use `hashCode` (they scan linearly), but `HashSet`/`HashMap` do — so breaking that rule causes bugs elsewhere. Records (`record Point(int x, int y)`) generate a correct `equals`/`hashCode` for you automatically. ## Direction and cost - `indexOf` scans front-to-back, returns the **first** match. - `lastIndexOf` scans back-to-front, returns the **last** match. - All are **O(n)** linear scans (no index/hash on a plain `List`). For frequent membership tests on large data, prefer a `HashSet`. ## Combined with subList Because `subList` returns a real `List`, you can search just a range: `list.subList(from, to).indexOf(x)` returns the index **within the sub-view** (0-based from `fromIndex`), or -1. The same equals-based matching applies.

  • Does indexOf on an ArrayList use hashCode?
    No. A List does a linear equals-based scan; hashCode is irrelevant to List lookups. hashCode matters for hash-based collections like HashSet/HashMap.
  • What is the easiest correct way to get value-based equals/hashCode for a simple data class?
    Use a record: record Point(int x, int y) {} auto-generates equals, hashCode, and toString based on its components.

saying these in an interview costs you the question

  • Saying indexOf/contains use == (reference equality)
  • Overriding equals but not hashCode
  • Thinking List membership uses hashing like a HashSet
  • Forgetting indexOf returns -1 (not throws) when absent
  • Assuming null cannot be searched for

context