Concepts / Comparing Objects with compareTo

Comparing Objects with compareTo

The == operator tests identity: whether two references refer to the same object in memory.

  • Programming
Interactive lab

Try it: Sorting with key= and reverse=

How sorted() and list.sort() compute key(item) once per item, order the items by those keys with Python's comparison rules, and keep equal keys in their original order — including py4e's sort-a-dict-by-value pattern.

How it works

  1. Decorate: call the key function once on every item (no key means the item itself is compared).
  2. Compare keys with Python's rules: ints by value, strs by code point ('Z' < 'a'), tuples item by item.
  3. Order the items by key; reverse=True puts the largest key first.
  4. Stable: items with equal keys keep their original relative order, also with reverse=True.
  5. sorted() returns a new list; list.sort() rearranges the list in place and returns None; [:N] keeps the first N.

Default run (12 steps): 5 items in their original order. Python will call lambda t: t[1] once per item. … sorted() returned a new list: result = [('fig', 5), ('kiwi', 5), ('pear', 3), ('apple', 3), ('date', 1)].

Simplified: Up to 10 items of one kind (ints, short ASCII words, (word, count) tuples or a small dict) and a fixed menu of key functions. The sorting algorithm itself (Timsort) is not shown — only what it guarantees: the order by key and stability.

Educational simulation

Loading the simulation…

Why a True Result Can Mislead

Suppose you store playing cards in a collection and then ask the collection whether a particular card is present. Methods such as contains and remove perform this search for you: they look through the collection for an item that matches the one you supplied. The result depends entirely on what the word matches means. If that meaning is wrong, the collection will appear to lose cards, fail to find cards that are plainly there, or behave in ways that seem random.

A very common first instinct is to compare two objects with the == operator and assume that a true result means the objects hold the same data. That instinct is a misconception. The == operator asks a narrower question: do the two expressions refer to the same object in memory? Two separate objects can hold identical data and still fail the == test, because == does not look inside them. This article follows the meaning of equality for objects, because that meaning is what a collection relies on when it searches, and it then extends the same idea to ordering with compareTo.

Same Object or Same Value

It helps to separate two questions that look alike. The first question is identity: are these two references pointing at one and the same object? The second question is equality: do these two objects represent the same value? Identity is a fact about where the objects live. Equality is a fact about what they mean. A pair of references can answer yes to both questions, no to identity but yes to equality, or no to both. Only the first kind of answer comes automatically from the == operator.

xCard: suit 2, value 7one objectyCard: suit 2, value 7a separate objectpCard: suit 2, value 7another separate objectqrefers torefers torefers torefers to
When two variables hold references, how do I tell whether they point to one shared object in memory or to two separate objects with equal contents?

Read the diagram as two situations. In the upper pair, x and y are two names for one object, so == returns true and the values agree trivially. In the lower pair, p and q lead to two different objects that both hold a suit of 2 and a value of 7. Their contents match, which is an equality question, but the == operator still reports false, because it only checks whether both references lead to the same object. The == operator answers the identity question automatically; answering the equality question requires code that you write.

Inside a Custom equals() Method

Consider a Card class that stores a suit as a number from 0 to 3 and a value as a number from 1 to 13. Two cards should count as equal when both fields match. The overriding method first converts the incoming object to a Card so that its fields can be read. If that conversion succeeds, it compares the suit and the value. If it fails, it returns false, because the object cannot possibly be an equal card.

java
Notes on the code
  • The cast converts the incoming Object to a Card so its fields can be read
  • Both fields must match for the method to return true
  • The catch block returns false when the object is null or of some other type
equals(other)calledRead other as a CardConversion succeeds?Compare suit andvalueBoth fields match?return truereturn falseyesno: null or other typeyesno
Which checks run in what order inside equals(), and at which branch does the method return true or false for two Card objects?

Trace a matching pair. The conversion succeeds, both the suit and the value agree, and the method returns true. Now trace a mismatched pair with the same suit but different values. The conversion succeeds, the suit comparison passes, the value comparison fails, and the method returns false. For an object that is null, the conversion path fails and the method returns false without crashing. The source names two situations: a null reference leads to a NullPointerException and an object of some other type leads to a ClassCastException. In both situations the method catches the exception and returns false, because a null reference or a foreign object cannot be equal to a card.

Worked example

Tracing equals() on two Card objects

Call a.equals(b) where a has suit 2 and value 7, and b has suit 2 and value 9. Which lines run, and what is returned?

  1. 1
    Enter equals

    The argument b is an object, so the method starts the try block.

  2. 2
    Convert the argument

    The cast to Card succeeds because b is a Card, so no exception is thrown.

  3. 3
    Compare suit

    a.suit is 2 and b.suit is 2, so the first part of the && expression is true.

  4. 4
    Compare value

    a.value is 7 and b.value is 9, so the second part is false and the whole expression is false.

  5. 5
    Return

    The return statement sends back false, so a.equals(b) is false.

Result

false, because the value fields differ even though the suit fields match

Where a Card Comparison Goes Wrong

The debugging question is not whether equals() exists, but where in its logic a result departs from what you expected. A correct method can still produce a wrong answer if the comparison reads the wrong fields or stops at the wrong point. Follow the comparison step by step, note which value is read from which object, and check the answer at each branch. Then locate the exact line where the observed outcome first differs from the intended one.

java
Output
Card(suit 2, value 7).equals(Card(suit 2, value 9)) returns true
Notes on the code
  • The type check and the cast are correct
  • The final line compares only suit, so the value field is never read
  • Common mistake

    Assuming == reports whether two objects hold the same data

    What it looks like

    Two Card objects with suit 2 and value 7 are created separately, and == returns false.

    Why it fails

    The == operator checks whether both references lead to the same object, not whether the fields match.

    Do this instead

    Override equals() to define value equality, then call equals() when you need to compare contents.

  • Common mistake

    Reading only some of the fields that define the value

    What it looks like

    The equals() method above compares suit but never reads value, so different cards appear equal.

    Why it fails

    A comparison that ignores a defining field reports matches that do not exist.

    Do this instead

    Compare every field that defines the object's value, as the Card method does with both suit and value.

  • Common mistake

    Letting equals() crash on a null or foreign argument

    What it looks like

    Calling equals() with null and letting the NullPointerException escape.

    Why it fails

    An unexpected argument should produce a clean answer that no match exists, not a crash.

    Do this instead

    Handle null and unexpected types and return false, as the Card method does.

Why Sorting Needs a Rule

Numbers and strings have an obvious order, but a program cannot sort a collection of arbitrary objects until it knows how to decide that one object comes before another. Sorting means arranging items in ascending order according to some criterion. For custom objects there is no built-in notion of that criterion, so something must be defined for comparing them before a sort can proceed.

Hopper, GraceByron, LordLovelace, AdaHopper, GraceByron, LordLovelace, Ada
What does the list look like before and after sorting, and why can the sort not proceed without a rule for deciding which of two objects comes first?

The before column is the original list and the after column is the same objects in ascending order by last name. The sort cannot place Hopper before or after Lovelace without a rule that answers which of two objects comes first. That rule is the comparison. Comparable and Comparator, covered below, are two ways of supplying it.

Reading the Sign of compareTo

The Comparable interface is a parameterized interface, written Comparable<T>, that expresses the ability to be compared to an object of type T. It defines exactly one method, compareTo, which takes an object of type T as its parameter. Calling obj1.compareTo(obj2) asks obj1 to report where it stands relative to obj2. Only the sign of the result matters: a negative result means obj1 comes before obj2, a positive result means obj1 comes after obj2, and zero means the two are the same for that comparison.

What do you think happens?

Given FullName a = Ada Lovelace and FullName b = Alan Turing, with last name as the primary key, what does a.compareTo(b) return?

  • -1

  • 0

  • 1

Reveal answer
Answer:

-1

The last names differ. Lovelace comes before Turing in alphabetical order, so a comes before b and the method returns -1.

Worked example

Predicting a compareTo result when last names match

x is Grace Hopper and y is Alan Hopper. Both share the last name Hopper. What does x.compareTo(y) return?

  1. 1
    Compare last names

    Both last names are Hopper, so the last-name comparison gives zero and does not decide the order.

  2. 2
    Fall through to first names

    With the last names equal, the method returns the result of comparing the first names.

  3. 3
    Compare first names

    Grace comes after Alan alphabetically, so the first-name comparison is positive.

  4. 4
    Read the sign

    A positive result means x comes after y.

Result

1 (a positive value), so Grace Hopper is placed after Alan Hopper

Building a Natural Order with Comparable

The source develops a class called FullName, which holds a first name and a last name, both required to be non-null. To make FullName objects sortable, the class declares that it implements Comparable<FullName> and defines compareTo so that the last name is the primary key. Only when two last names match does the first name decide the order. The method returns -1 or 1 when the last names differ, and it returns the first-name comparison result when they match. Both forms satisfy the convention because only the sign matters.

java
Notes on the code
  • implements Comparable<FullName> declares the natural order for this class
  • The last name is checked first because it is the primary key
  • The first name decides the order only when the last names match

Comparators Live Outside the Class

Comparable puts the comparison inside the class. A Comparator takes a different approach: it is a separate object whose job is to compare two objects of some type T. The object must implement Comparator<T>, and the interface defines one method, compare(T obj1, T obj2). Its return value follows the same sign convention as compareTo. Comparators are useful in two situations: comparing objects whose class does not implement Comparable, such as a class you cannot modify, and defining several different orderings over the same collection of objects. Because Comparator is a functional interface, comparators are often written as lambda expressions.

Comparable<T>compareTo(T)Inside the classComparator<T>compare(T, T)Outside the classdefineslives indefineslives in
Is the comparison rule built into the class itself (Comparable) or supplied from outside the class (Comparator), and how does that change which sort order is used?
java
Notes on the code
  • The comparison rule is a lambda kept outside the Address class
  • Only the zip code is compared, so the ordering ignores the other fields

The source notes that a Comparator does not have to agree with equals. A comparator that looks only at the zip code reports zero for two different addresses that share a zip code, even though they are not equal in every field. That is a deliberate choice for the ordering, not a mistake. It also means that two objects can be sorted as tied while still being unequal, which is worth remembering when an ordering seems to merge items you expected to keep apart.

Choosing Between Comparable and Comparator

QuestionComparable<T>Comparator<T>
Where does the rule live?Inside the class, in compareToIn a separate object, in compare
Can it be used on a class you cannot modify?No, the class must implement the interfaceYes, the comparator sits outside the class
Can one collection have several orderings?Only one natural order is built inYes, each comparator defines a different ordering
What does the return value mean?Negative, zero, or positive, as with compareToNegative, zero, or positive, as with compare

Choosing between Comparable and Comparator

Use Comparable when the class has one obvious natural order that belongs to the class itself. Use Comparator when the class cannot be changed, or when the same objects need more than one ordering. When an order looks wrong after sorting, trace the comparison rule that the sort actually uses: check whether it reads the primary key first, whether it returns zero where you expected a difference, and whether it is the rule you intended to supply.

Practice: Predicting Sort Order

MEDIUM

Using the FullName class with last name as the primary key, predict the sign of the result for each call: (1) Ada Lovelace compared to Alan Turing, (2) Grace Hopper compared to Alan Hopper, (3) Alan Hopper compared to Alan Hopper. For each call, state which comparison decides the answer and whether the result is negative, zero, or positive. Then explain which line of compareTo a sort would have to pass through to place Grace Hopper before Alan Hopper.

Hints
  • Check the last names first, because they are the primary key
  • Only move to the first names when the last names are equal
  • Zero means the two objects count as the same for this comparison
HARD

A teammate writes a Comparator that sorts Card objects by value only and then reports that the sorted deck looks wrong for cards with equal values. Using the idea that a comparator may return zero for objects that are not equal in every field, explain whether this is a bug or a deliberate ordering, and name the step where the unexpected order comes from.

Hints
  • Ask whether the comparator reads every field that defines equality
  • Look at what the comparator returns for two cards with the same value but different suits

Key Takeaways

  • The == operator tests identity, meaning whether two references lead to the same object; equality asks whether two objects represent the same value, and that requires an overridden equals() method.
  • A custom equals() should compare the fields that define the object's value and return false for null or unexpected types, and a wrong answer usually traces back to a specific field or branch.
  • Sorting requires a defined comparison, because arbitrary objects have no natural ascending order until a rule decides which of two objects comes first.
  • Comparable<T> puts compareTo inside the class, and only the sign of its result matters: negative means before, positive means after, and zero means the same for that comparison.
  • Comparator<T> supplies the comparison from outside the class, which suits classes you cannot modify and situations that need several orderings over the same objects.