انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-032-L01

الترتيب الطبيعي باستخدام Comparable

  • الدرس 1 من 4
  • 10 دقيقة قراءة
  • مبتدئ
في هذا الدرس 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 correct compareTo.
  • Read the result of compareTo: negative, zero, or positive.
  • Sort a list with Collections.sort(list) and keep elements in a TreeSet or TreeMap using the natural ordering.
  • Compare int values safely with Integer.compare and explain why a - b is risky.
  • Explain what "consistent with equals" means and why a TreeSet cares.

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:

java
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:

java
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:

ResultMeaningRace picture
Negativea comes before ba has the faster time
Zeroa and b rank equalA tie
Positivea comes after ba 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:

java
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) and list.sort(null) (passing null as the comparator means "use the natural ordering", according to the List documentation).
  • new TreeSet<>(collection) and TreeSet's add, which keep elements in order.
  • TreeMap keys, 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 a comes before b, then b comes after a.
  • Transitive: if a is before b and b is before c, then a is before c.
  • Ties behave: if a and b tie, they must compare the same way against every other element.
  • Null: x.compareTo(null) should throw a NullPointerException; 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)

java
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"));       // 0

Output of this program (compiled and run with JDK 21):

text
[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)

java
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:

text
[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:

java
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:

text
true
true
false
[Ali:70, Mona:90] size=2

Zed 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, so compareTo(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.

java
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.

java
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.

java
@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.

java
@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 method compareTo.
  • compareTo returns negative (this first), zero (tie), or positive (this after). Only the sign matters.
  • Use Integer.compare(a, b) and its siblings. Never write a - b, because it can overflow.
  • Collections.sort, TreeSet, and TreeMap use 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

  1. What does a negative result from a.compareTo(b) mean?
  2. Which method does Comparable<T> require?
  3. Why is return a - b; risky for int values?
  4. Why can a TreeSet hold fewer elements than you added?
  5. What does list.sort(null) do?
أظهر الإجابات
  1. a comes before b.
  2. int compareTo(T other).
  3. The subtraction can overflow and wrap around, giving a result with the wrong sign.
  4. It uses the ordering to detect duplicates, so elements that compare as 0 count as the same element, even if equals would say they differ.
  5. 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 the BigDecimal example.
  • Oracle, Java SE 21 API, Integer. Supports Integer.compare returning only a sign, and the MIN_VALUE/MAX_VALUE range.
  • Oracle, Java SE 21 API, List. Supports sort(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, and Character have compare methods is from general knowledge and was not separately fetched.

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.