Challenge

The equals that was ignored

20 minjunior18 yrs

Edge cases. You have to reason, and two valid fixes differ.

What this teaches

  • equals(MyType) is an OVERLOAD; only equals(Object) is an override
  • Collections call equals(Object) through the interface, so the overload never runs
  • @Override turns this from a silent production bug into a compile error
  • hashCode has to agree with whatever equals you actually wrote

Starter

Starter.javaOpen in playground
import java.util.*;

/**
 * CHALLENGE — 20 minutes.
 *
 * Dedup is not deduplicating. A support ticket says the same customer gets
 * the same notification several times.
 *
 * The class has an equals. It looks correct. A direct call to it returns
 * true. The HashSet still holds duplicates.
 *
 * TASKS
 *   1. Run it. Note that assertion (a) passes and (b) fails — the equals
 *      works when YOU call it and not when the collection does.
 *   2. Add @Override to equals. Read the compile error. That is the bug.
 *   3. Fix it, and then fix what the HashSet needs beyond equals.
 *   4. In a comment: the original compiled cleanly and no tool warned. Why
 *      not, and what would have caught it?
 */
public class Starter {

    static final class Recipient {
        private final String email;

        Recipient(String email) {
            this.email = email;
        }

        /** Looks right. Is not an override. */
        public boolean equals(Recipient other) {
            return other != null && email.equalsIgnoreCase(other.email);
        }

        @Override
        public String toString() {
            return email;
        }
    }

    public static void main(String[] args) {
        Recipient a = new Recipient("ravi@example.com");
        Recipient b = new Recipient("RAVI@example.com");

        System.out.println("(a) a.equals(b) directly     : " + a.equals(b));

        Set<Recipient> unique = new HashSet<>();
        unique.add(a);
        unique.add(b);
        System.out.println("(b) HashSet size            : " + unique.size() + "   (expected 1)");
        System.out.println("(c) HashSet contains a copy : "
                + unique.contains(new Recipient("ravi@example.com")));

        List<Recipient> list = new ArrayList<>(List.of(a));
        System.out.println("(d) List contains a copy    : "
                + list.contains(new Recipient("ravi@example.com")));

        System.out.println();
        System.out.println("notifications that would be sent: " + unique.size());

        // Question to answer in a comment before you move on:
        // (b) and (d) both fail, but for different reasons. What does a
        // HashSet need that an ArrayList does not?
    }
}

Run it locally:

cd exercises/java/oop/overloading-vs-overriding/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out Starter

Hints

  1. Hint 1

    Look at the parameter type of equals. What does HashSet call?

  2. Hint 2

    Add @Override to it and compile. The error is the whole diagnosis.

  3. Hint 3

    Once equals(Object) is right, the set still misbehaves. What else does a hash-based collection need?

  4. Hint 4

    Try it in an ArrayList too — contains() uses equals but not hashCode, so the two collections fail for different reasons and at different times.

Done when

  • equals takes Object and carries @Override
  • hashCode is consistent with equals
  • The HashSet holds one element and contains() finds it
  • A comment explains why the original compiled and why nothing warned

Stretch

Write the version that would have caught this in CI without anyone remembering to add @Override: a test that puts two equal objects in a HashSet and asserts size 1. Then say why a test that only calls a.equals(b) directly would have passed against the broken code — which is exactly how this bug survives review.

← Back to What is the difference between overloading and overriding?