1. The problem of comparing objects
Reminder: reference comparison vs. object comparison
You already know that in Java the == operator, when used with objects, compares their references — that is, whether they point to the same address in memory. Two objects with identical fields but created via new will be different for ==.
Person p1 = new Person("Alex", 20);
Person p2 = new Person("Alex", 20);
System.out.println(p1 == p2); // false — these are different objects in memory!
If we want to know whether they are equal by content, we use equals(), hashCode() … you already know this. But what if we need to determine who is “older”, “younger”, or “earlier alphabetically”? For example, to sort a list of users by age or by name.
The need to sort and search objects
Suppose we have a list of users and we want to sort them by age:
List<Person> people = new ArrayList<>();
people.add(new Person("John", 25));
people.add(new Person("Peter", 20));
people.add(new Person("Kate", 30));
// How to sort?
Collections.sort(people); // Oops! Java doesn't know how to compare Person!
The compiler will immediately complain: the Person class does not implement the Comparable interface. Java can’t read minds and doesn’t know what “greater” or “less” means for Person. To teach it that, we must explicitly describe the comparison rules.
2. The Comparable interface
Interface declaration
The Comparable interface is the standard way to tell Java: “My class is comparable, and here’s how to do it.”
public interface Comparable<T> {
int compareTo(T o);
}
Our old friend a.compareTo(b) returns:
- a negative number — a is “less than” b.
- If 0 — the objects are considered equal.
- a positive number — a is “greater than” b.
Example: implementing compareTo for the Person class
Let’s create a Person class that can be compared by age:
public class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Getters (for the examples below)
public String getName() { return name; }
public int getAge() { return age; }
// Implementation of compareTo method
@Override
public int compareTo(Person other) {
// Sort by age (ascending)
return Integer.compare(this.age, other.age);
// Alternative: return this.age - other.age;
}
}
Important: if you want to sort in descending order, just swap the arguments: Integer.compare(other.age, this.age).
Analogy. compareTo is like a judge at a competition: it must clearly decide who is ahead, who is behind, and who is at the same level. If all judges (the compareTo methods) judge differently — chaos ensues!
3. Using Comparable
Sorting collections with Comparable
Now that our class implements Comparable, sorting works out of the box:
List<Person> people = new ArrayList<>();
people.add(new Person("John", 25));
people.add(new Person("Peter", 20));
people.add(new Person("Kate", 30));
Collections.sort(people); // Uses compareTo!
for (Person p : people) {
System.out.println(p.getName() + " (" + p.getAge() + ")");
}
// Peter (20)
// John (25)
// Kate (30)
The sort method on a list works similarly:
people.sort(null); // If you pass null, compareTo is used
Sorting by name
If we want to sort by name — change the implementation:
@Override
public int compareTo(Person other) {
return this.name.compareTo(other.name);
}
Sorting by multiple fields
Sometimes you need to compare first by one field, and if equal — by another:
@Override
public int compareTo(Person other) {
int cmp = Integer.compare(this.age, other.age);
if (cmp != 0) return cmp;
return this.name.compareTo(other.name);
}
4. Best practices when implementing Comparable
Honor the Comparable contract
- If a.compareTo(b) == 0, then b.compareTo(a) must also be 0.
- If a.compareTo(b) < 0, then b.compareTo(a) must be > 0 (and vice versa).
- If a.compareTo(b) == 0, it is desirable that a.equals(b) be true (but this is not strictly required).
Why does this matter?
Collections (for example, TreeSet, TreeMap) and sorting methods can behave unpredictably if the contract is violated. For example, “duplicates” may appear in a collection where they shouldn’t exist.
Don't forget about equals and hashCode
If you implement compareTo, consider whether equals and hashCode are implemented correctly — especially if your class will be used in collections like HashSet or Map.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person other = (Person) o;
return age == other.age && Objects.equals(name, other.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
Do not use fields that can be null in compareTo without checking!
If a field can be null, use safe comparisons:
@Override
public int compareTo(Person other) {
return Objects.compare(this.name, other.name, Comparator.nullsFirst(String::compareTo));
}
Don’t mutate fields participating in compareTo if the object is already in a sorted collection
This can cause the object to get “lost” inside the collection — for example, in TreeSet or TreeMap.
5. Evolving the training app: sorting users
Step 1: Define the class
public class Person implements Comparable<Person> {
private String name;
private int age;
// ... constructor, getters, compareTo, equals, hashCode ...
}
Step 2: Add users
List<Person> people = new ArrayList<>();
people.add(new Person("Mary", 23));
people.add(new Person("Greg", 19));
people.add(new Person("Anne", 25));
Step 3: Sort and print
Collections.sort(people);
for (Person p : people) {
System.out.println(p.getName() + " (" + p.getAge() + ")");
}
Result:
Greg (19)
Mary (23)
Anne (25)
6. How Comparable works
┌────────────────────────────┐
│ Your class (Person) │
├────────────────────────────┤
│ implements Comparable │
│ ↓ │
│ public int compareTo(T o) │
│ ↓ │
│ (this < o) → -1 │
│ (this == o) → 0 │
│ (this > o) → 1 │
└────────────────────────────┘
│
▼
Collections.sort(list)
│
▼
Sorting works!
7. Useful details
How Collections.sort works
- If a list contains objects that implement Comparable, sorting will use their compareTo method.
- If it doesn’t implement it — you’ll get a compilation error.
- For standard types (Integer, String, etc.) Comparable is already implemented.
Can we have multiple comparison strategies?
- Within a single class — only one “natural order” via Comparable.
- For alternative orders, use Comparator (next lecture).
Example: compareTo for strings
String a = "apple";
String b = "banana";
System.out.println(a.compareTo(b)); // a negative number because "apple" < "banana"
Table: what compareTo returns
| Comparison | Return value |
|---|---|
|
|
|
|
|
|
8. Common mistakes when implementing Comparable
Mistake #1: Violating the compareTo contract.
If a.compareTo(b) returns 0, but b.compareTo(a) does not return 0, collections will behave strangely. For example, a TreeSet may treat the objects as different and add both.
Mistake #2: Using uninitialized (null) fields.
If the field you compare by can be null and you don’t check it — you’ll get a NullPointerException.
Mistake #3: Inconsistency between compareTo and equals.
If compareTo says that objects are equal (0) but equals says they are different (false), this will lead to bugs when working with collections.
Mistake #4: Mutating fields involved in compareTo after adding to a sorted collection.
It’s like changing your last name in your passport while you’re already standing in an alphabetic queue. The collection may “lose” your object.
Mistake #5: Returning only -1, 0, or 1.
The compareTo method can return any negative or positive number; it doesn’t have to be strictly -1 or 1. But for simplicity, -1/0/1 are often used.
GO TO FULL VERSION