Comparing Objects with compareTo
The == operator tests identity: whether two references refer to the same object in memory.
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
- Decorate: call the key function once on every item (no key means the item itself is compared).
- Compare keys with Python's rules: ints by value, strs by code point ('Z' < 'a'), tuples item by item.
- Order the items by key; reverse=True puts the largest key first.
- Stable: items with equal keys keep their original relative order, also with reverse=True.
- 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.
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.
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.
- 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
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.
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?
- 1Enter equals
The argument b is an object, so the method starts the try block.
- 2Convert the argument
The cast to Card succeeds because b is a Card, so no exception is thrown.
- 3Compare suit
a.suit is 2 and b.suit is 2, so the first part of the && expression is true.
- 4Compare value
a.valueis 7 andb.valueis 9, so the second part is false and the whole expression is false. - 5Return
The return statement sends back false, so
a.equals(b)is false.
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.
Card(suit 2, value 7).equals(Card(suit 2, value 9)) returns true- 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 likeTwo Card objects with suit 2 and value 7 are created separately, and == returns false.
Why it failsThe == operator checks whether both references lead to the same object, not whether the fields match.
Do this insteadOverride 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 likeThe equals() method above compares suit but never reads value, so different cards appear equal.
Why it failsA comparison that ignores a defining field reports matches that do not exist.
Do this insteadCompare 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 likeCalling equals() with null and letting the NullPointerException escape.
Why it failsAn unexpected argument should produce a clean answer that no match exists, not a crash.
Do this insteadHandle 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.
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?
Reveal answer
-1
The last names differ. Lovelace comes before Turing in alphabetical order, so a comes before b and the method returns -1.
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?
- 1Compare last names
Both last names are Hopper, so the last-name comparison gives zero and does not decide the order.
- 2Fall through to first names
With the last names equal, the method returns the result of comparing the first names.
- 3Compare first names
Grace comes after Alan alphabetically, so the first-name comparison is positive.
- 4Read the sign
A positive result means x comes after y.
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.
- 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.
- 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
| Question | Comparable<T> | Comparator<T> |
|---|---|---|
| Where does the rule live? | Inside the class, in compareTo | In a separate object, in compare |
| Can it be used on a class you cannot modify? | No, the class must implement the interface | Yes, the comparator sits outside the class |
| Can one collection have several orderings? | Only one natural order is built in | Yes, each comparator defines a different ordering |
| What does the return value mean? | Negative, zero, or positive, as with compareTo | Negative, 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
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
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
compareToinside 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.