← Skill tree CS Skill Tree 0 CSCD211

compareTo vs equals: Choosing the Right Match Test

Textbook: Liang

Try This First

A linked list node holds a String. Inside removeAllOccurrences you write:

if (cur.data.compareTo(data) == 0)

A classmate writes:

if (cur.data.equals(data))

Both compile. Both produce identical results for normal strings.

Before reading: what is the one structural difference between the two lines that matters most to the class declaration?

Reveal

compareTo requires the type T to implement Comparable. The class must declare <T extends Comparable<? super T>> for compareTo to compile. equals works on any Object, so no bound is needed. When Stu’s S20 Lab 5 uses compareTo, it is deliberately exercising the bound that the class already declares.

What You Need To Walk In With

How It Works

The linked list class declaration in S20 Lab 5 is:

public class LinkedList<T extends Comparable<? super T>>

The bound T extends Comparable<? super T> says: every type argument T must implement Comparable of itself (or a supertype). This allows any method in the class to call compareTo on a stored value.

Inside removeAllOccurrences, the traversal checks each node:

public void removeAllOccurrences(final T data) {
    // empty list: nothing to do
    if (head == null) {
        return;
    }

    // remove matching nodes at the front
    while (head != null && head.data.compareTo(data) == 0) {
        head = head.next;
        size--;
    }

    // traverse the rest, removing matches from interior / tail
    if (head != null) {
        Node<T> cur = head;
        while (cur.next != null) {
            if (cur.next.data.compareTo(data) == 0) {
                cur.next = cur.next.next;
                size--;
            } else {
                cur = cur.next;
            }
        }
    }
}

The match test is cur.next.data.compareTo(data) == 0. Replacing this with cur.next.data.equals(data) changes nothing for types like String and Integer, where compareTo == 0 and equals agree. The structural difference is only in the class declaration: equals works without the bound, while compareTo requires it.

A pointer sketch of what happens when the front node matches:

Before:  head -> [A] -> [B] -> [C]    (removing "A")
         head.data.compareTo("A") == 0  =>  true
After:   head -> [B] -> [C]
         head now points past the old front node

Worked Example: Predict, Then Check

A LinkedList<String> contains: "cat" -> "dog" -> "cat" -> null. Size is 3. You call removeAllOccurrences("cat").

Trace through the method above. After the call, what does the list contain and what is size?

Reveal

Step 1: head.data.compareTo("cat") == 0 is true. Set head = head.next (now points to "dog"), size becomes 2.

Step 2: head.data.compareTo("cat") == 0 is false (“dog” does not equal “cat”). Exit the front-removal loop.

Step 3: Enter interior traversal. cur = head (the “dog” node). Check cur.next.data.compareTo("cat") == 0: the next node is “cat”, so true. Set cur.next = cur.next.next (null), size becomes 1.

Step 4: cur.next is now null. Loop ends.

Result: "dog", size 1.

A Common Mistake

Using compareTo when the class does not declare the bound.

If someone copies the compareTo match test into a class declared as LinkedList<T> (no bound), the compiler rejects it: T has no compareTo method. The fix is to either add the bound (<T extends Comparable<? super T>>) or switch the test to .equals. The choice is not arbitrary: if the class uses ordering anywhere else (sorting, finding min/max), the bound already belongs there and compareTo is the natural match. If ordering is never needed, .equals keeps the class more general. (Source: Liang 10e Ch 24)

Go Deeper (optional)

The Comparable contract recommends that compareTo == 0 and equals agree, but it does not require it. A pathological example is a custom type that compares case-insensitively in compareTo but uses exact-case equality in equals. For such a type, removeAllOccurrences("Cat") would remove "cat" nodes under the compareTo test but leave them under equals. Stu’s labs use standard library types where the two always agree, so this distinction is theoretical here. The cost of removeAllOccurrences is O(n): every node is visited exactly once.

Check Yourself

Close the notes and answer each one from memory, then reveal it. Pulling an idea back from memory is one of the strongest ways to make it stick.

Check your understanding

S20 Lab 5 declares the list as LinkedList<T extends Comparable<? super T>>. What does this bound enable inside the class?

Tier 1 · Liang 10e Ch 24

Which match test requires the Comparable bound on T to compile?

Tier 1 · Liang 10e Ch 24

A LinkedList<String> holds: “x” -> “y” -> “x”. You call removeAllOccurrences(“x”) using the compareTo test. What is the list contents and size afterward?

Tier 2 · Liang 10e Ch 24

The lab spec rejects null at insertion. Why does this matter for the compareTo match test inside removeAllOccurrences?

Tier 2 · Liang 10e Ch 24

Why does Stu’s lab use compareTo instead of equals for the match test, even though both would produce the same result for String?

Tier 3 · Liang 10e Ch 24