الترتيب الطبيعي باستخدام Comparable
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
Picture the finish line of a race. The judges rank runners by time: the smallest time wins. Before anyone can print a results board, there must be a rule that answers one question for any two runners: who goes first?
Java collections need the same rule. In the last modules you used List, Set, and Map, and you met TreeSet, which keeps its elements sorted. Sorting strings and numbers just works, because Java already knows how to order them. For your own classes, Java has no idea, so you must say it. This lesson shows how: you implement the Comparable interface and write one method, compareTo. That gives your class a natural ordering, its default order.
Why this matters: sorting is everywhere in backend work. An API returns "newest orders first", a report lists customers alphabetically, and SQL has ORDER BY for the same reason (you will meet it later in the course). The ideas in this lesson are the base for the next three.
This lesson builds on interfaces (BEG-024), equals and hashCode (BEG-026), and the collections and generics you have used so far (BEG-030, BEG-031).
Learning Objectives
By the end of this lesson you will be able to:
- Explain what a natural ordering is and why a class needs one.
- Implement
Comparable<T>and write a correctcompareTo. - Read the result of
compareTo: negative, zero, or positive. - Sort a list with
Collections.sort(list)and keep elements in aTreeSetorTreeMapusing the natural ordering. - Compare
intvalues safely withInteger.compareand explain whya - bis risky. - Explain what "consistent with
equals" means and why aTreeSetcares.
Written Explanation
Some types already know their order
String, Integer, Long, Double and the other wrapper classes already implement Comparable. That is why this works without any extra code:
List<String> names = new ArrayList<>(List.of("Mona", "Ali", "Zed", "Bob"));
Collections.sort(names);
System.out.println(names); // [Ali, Bob, Mona, Zed]Strings sort by dictionary (character code) order, and numbers sort by value. A class you wrote, such as Book or Player, has no such rule. If you try Collections.sort on a list of them, the compiler refuses, because the elements are not Comparable.
The Comparable interface
Comparable<T> is a one-method interface from java.lang:
public interface Comparable<T> {
int compareTo(T other);
}A class that implements it declares: "I know how to compare myself with another T". The type parameter should normally be the class itself, so Book implements Comparable<Book>. (You met type parameters in BEG-031.)
What compareTo returns
a.compareTo(b) returns an int, and only its sign matters:
| Result | Meaning | Race picture |
|---|---|---|
| Negative | a comes before b | a has the faster time |
| Zero | a and b rank equal | A tie |
| Positive | a comes after b | a is slower |
Do not rely on the exact value. It may be -1, 1, or any other number of the right sign. Oracle's documentation for Integer.compare says the same: it specifies only the sign.
Writing compareTo
For one numeric field, delegate to the wrapper's static compare method:
public class Book implements Comparable<Book> {
private final String title;
private final int year;
Book(String title, int year) { this.title = title; this.year = year; }
@Override
public int compareTo(Book other) {
return Integer.compare(year, other.year);
}
}This orders books by year, oldest first. Integer.compare(x, y) gives a negative number if x < y, zero if equal, and a positive number if x > y. For long, double, and char there are Long.compare, Double.compare, and Character.compare. For a String field, call its own compareTo.
Once the class is Comparable, three things work with no more code:
Collections.sort(list)andlist.sort(null)(passingnullas the comparator means "use the natural ordering", according to theListdocumentation).new TreeSet<>(collection)andTreeSet'sadd, which keep elements in order.TreeMapkeys, which are kept sorted.
Why not return a - b?
It is tempting to write return year - other.year;, because the sign of the difference looks right. It is wrong for large values, because int subtraction overflows: the result wraps around. With Integer.MAX_VALUE - (-5) the true answer is 2,147,483,652, which does not fit in an int, and Java wraps it to -2147483644: a negative number, so the larger value would be ranked first. Years are small and would never trigger this, but scores, ids, and amounts can. Integer.compare never has that problem, so always use it.
The rules of a good compareTo
The Java documentation asks every compareTo to follow a few rules. In plain words:
- Symmetric: if
acomes beforeb, thenbcomes aftera. - Transitive: if
ais beforebandbis beforec, thenais beforec. - Ties behave: if
aandbtie, they must compare the same way against every other element. - Null:
x.compareTo(null)should throw aNullPointerException; it must not return a number.
Break these and sorting can give strange results or an exception.
Consistent with equals
A natural ordering is consistent with equals when a.compareTo(b) == 0 exactly when a.equals(b). The documentation recommends this but does not require it. It matters because a TreeSet and a TreeMap never call equals. They treat two elements that compare as 0 as the same element. If your ordering says "tie" for two objects that equals calls different, a TreeSet quietly keeps only one of them. The JDK's own example is BigDecimal: 4.0 and 4.00 compare as 0 but are not equals.
Examples
Example 1: built-in types (simple)
List<Integer> nums = new ArrayList<>(List.of(30, 5, 12));
Collections.sort(nums);
System.out.println(nums); // [5, 12, 30]
System.out.println("Ali".compareTo("Bob") < 0); // true
System.out.println("Ali".compareTo("Ali")); // 0Output of this program (compiled and run with JDK 21):
[5, 12, 30]
true
0"Ali" is before "Bob", so the result is negative. The same text gives zero.
Example 2: sorting books by year (practical)
List<Book> books = new ArrayList<>();
books.add(new Book("Dune", 1965));
books.add(new Book("Neuromancer", 1984));
books.add(new Book("Foundation", 1951));
Collections.sort(books);
System.out.println(books);
System.out.println(new TreeSet<>(books).first());With a toString that prints title (year), the output is:
[Foundation (1951), Dune (1965), Neuromancer (1984)]
Foundation (1951)One compareTo method served both Collections.sort and TreeSet.
Example 3: a leaderboard that loses a player (realistic)
Here Player is Comparable by score only, and we put players in a TreeSet:
Set<Player> set = new TreeSet<>();
System.out.println(set.add(new Player("Mona", 90))); // true
System.out.println(set.add(new Player("Ali", 70))); // true
System.out.println(set.add(new Player("Zed", 90))); // false
System.out.println(set + " size=" + set.size());Real output:
true
true
false
[Ali:70, Mona:90] size=2Zed scored the same as Mona, so compareTo returned 0 and the set decided Zed was a duplicate and refused him. Nothing is wrong with the TreeSet. The ordering did not separate two different players. The fix, a second key, is the topic of lesson 3.
Backend Insight
Backend code sorts constantly: orders by date, products by price, log entries by time. Often the sort is done by the database with ORDER BY, but any list you build in Java, such as a response assembled from several sources, must be sorted by your own code. Defining the order in one place (compareTo) gives a class one obvious default.
Keep one thing in mind: natural ordering is one order per class. "Orders by date" and "orders by total" cannot both be the natural order. For that you will use comparators in the next lesson. Reserve Comparable for an order nobody would argue with: dates by time, versions by number, money by amount.
Also remember that sorted collections use the ordering to decide sameness. If your service keeps users in a TreeSet ordered only by signup date, two users who signed up at the same moment collapse into one. That is a data-loss bug that no exception will announce.
Common Mistakes
Mistake 1: Subtracting to compare
- Incorrect:
return a.score - b.score; - Correct:
return Integer.compare(a.score, b.score);
The incorrect version works on small test data and fails on large values, which makes it hard to catch.
Mistake 2: Forgetting the type argument
- Incorrect:
class Book implements Comparable(raw type, socompareTo(Object)and casts) - Correct:
class Book implements Comparable<Book>
With the type argument, compareTo takes a Book and the compiler checks the types for you.
Mistake 3: An ordering that ties different objects
If two different objects can compare as 0, a TreeSet or TreeMap will treat them as one. Check whether your compared fields are enough to tell objects apart.
Mistake 4: Assuming the result is exactly -1, 0, or 1
Only the sign counts. Write compare(a, b) < 0, never compare(a, b) == -1.
Mistake 5: Expecting compareTo to replace equals
compareTo answers "which comes first?". equals answers "are they the same?". Use equals for sameness in a list or a HashSet.
Exercises
Try each exercise before opening the solutions.
Level A: Foundation
A1. "Zed".compareTo("Ali") returns a number. Is it negative, zero, or positive? What does that say about the order?
أظهر الحل A1
A1. The result is positive, since Z comes after A. A positive result means the receiver ("Zed") comes after the argument.
A2. What single method does Comparable<T> require, and what are its parameter and return types?
أظهر الحل A2
A2. int compareTo(T other): it takes one argument of the type parameter and returns an int whose sign gives the order.
A3. This compareTo is meant to order tasks by priority, but it is risky. Explain why and fix it.
public int compareTo(Task other) { return priority - other.priority; }أظهر الحل A3
A3. priority - other.priority can overflow when the values are far apart (for example Integer.MAX_VALUE and a negative number), giving the wrong sign. Fix: return Integer.compare(priority, other.priority);.
Level B: Application
B1. Write a class Task with a String title and an int priority that implements Comparable<Task> (lowest priority number first). Create three tasks, sort them with Collections.sort, and print them.
أظهر الحل B1
B1.
class Task implements Comparable<Task> {
final String title; final int priority;
Task(String t, int p) { title = t; priority = p; }
@Override public int compareTo(Task o) { return Integer.compare(priority, o.priority); }
@Override public String toString() { return title + "/" + priority; }
}
// list: Write/3, Test/1, Ship/2 -> [Test/1, Ship/2, Write/3]I compiled and ran this; the printed list was [Test/1, Ship/2, Write/3].
B2. Make a class Student with last and first name strings, ordered by last name, then by first name when the last names are equal. Use only String.compareTo.
أظهر الحل B2
B2.
@Override public int compareTo(Student o) {
int c = last.compareTo(o.last);
if (c != 0) return c; // last names differ: they decide
return first.compareTo(o.first);
}Sorting Smith Zoe, Adams Tom, Smith Amy gave [Adams Tom, Smith Amy, Smith Zoe].
B3. Put two Task objects with the same priority but different titles into a TreeSet<Task>. What is the set's size? Explain why.
أظهر الحل B3
B3. The size is 1. The ordering compares only priority, so compareTo returns 0, and the TreeSet treats the second task as a duplicate of the first and does not add it (I ran this and got [A/1]).
Level C: Challenge
C1. Suppose User has equals based on id, but compareTo compares only by signupDate. Describe a case where a TreeSet<User> loses a user, and say what the ordering should include to prevent it.
أظهر الحل C1
C1. Two different users who signed up at the same moment have different ids (so equals says "different"), but compareTo returns 0, so the TreeSet keeps only one. Include a field that tells users apart, for example id, as a tie-breaker, so that compareTo is 0 only when the users are truly the same.
C2. A Version class stores major and minor ints. Write compareTo so that 2.10 comes after 2.9, comparing major first and minor second. Why would comparing the text "2.10" and "2.9" give the wrong answer?
أظهر الحل C2
C2.
@Override public int compareTo(Version o) {
int c = Integer.compare(major, o.major);
return (c != 0) ? c : Integer.compare(minor, o.minor);
}As text, "2.10" and "2.9" are compared character by character. The first difference is '1' against '9' at the third position, and '1' is smaller, so "2.10" would sort before "2.9". Comparing the numbers gives the right order.
Mini Challenge
Create a class Movie with a title and a releaseYear. Make it Comparable<Movie> so movies sort by release year, oldest first. Put five movies into a list and sort it with Collections.sort. Then add two movies from the same year into a TreeSet<Movie> and print its size.
Check your work: the sorted list is in year order, you used Integer.compare, and you can explain in one sentence why the TreeSet is smaller than the number of movies you added.
Summary & Next Lesson
Key takeaways
- A natural ordering is a class's default order, defined by implementing
Comparable<T>and its one methodcompareTo. compareToreturns negative (this first), zero (tie), or positive (this after). Only the sign matters.- Use
Integer.compare(a, b)and its siblings. Never writea - b, because it can overflow. Collections.sort,TreeSet, andTreeMapuse the natural ordering with no extra code.- Sorted collections decide sameness by the ordering, not by
equals. Keep the two consistent, or ties will quietly drop elements.
Next lesson
A class gets only one natural ordering. The next lesson, "Comparators", shows how to sort the same objects in other ways, such as by name one time and by score another, without changing the class.
Self-check
- What does a negative result from
a.compareTo(b)mean? - Which method does
Comparable<T>require? - Why is
return a - b;risky forintvalues? - Why can a
TreeSethold fewer elements than you added? - What does
list.sort(null)do?
أظهر الإجابات
acomes beforeb.int compareTo(T other).- The subtraction can overflow and wrap around, giving a result with the wrong sign.
- It uses the ordering to detect duplicates, so elements that compare as 0 count as the same element, even if
equalswould say they differ. - It sorts the list by the elements' natural ordering, so the elements must be
Comparable.
References
- Oracle, Java SE 21 API,
Comparable. Supports the meaning of the result, the symmetry, transitivity, null and "consistent with equals" rules, and theBigDecimalexample. - Oracle, Java SE 21 API,
Integer. SupportsInteger.comparereturning only a sign, and theMIN_VALUE/MAX_VALUErange. - Oracle, Java SE 21 API,
List. Supportssort(null)using natural ordering. - Oracle, Java SE 21 API,
TreeMap. Supports sorted maps using natural ordering and treating keys that compare as equal as the same key. - The overflow value and every program output in this lesson come from code compiled and run with JDK 21 in a scratch folder. The statement that
Long,Double, andCharacterhavecomparemethods is from general knowledge and was not separately fetched.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.