Java Arena
Step 4 of 4

Natural order or comparator?

An order can live in two places: inside the class, as its compareTo, or outside it, as a comparator handed to a sort. This step is about choosing between them, and it ends the module with a challenge that uses both.

An order that belongs to the class

An app store keeps the releases of an app. Each release has a version number of two parts, such as 2.9 or 2.10, and a download count. Kept as text, the versions sort wrongly:

ArrayList<String> versions = new ArrayList<>();
versions.add("2.9");
versions.add("10.0");
versions.add("2.10");
Collections.sort(versions);
System.out.println(versions);
Output
[10.0, 2.10, 2.9]

Text is compared character by character. The character 1 comes before 2, so "10.0" goes first, and "2.10" goes before "2.9" because 1 comes before 9. But 2.10 is the release after 2.9, and 10.0 came after both. Versions have one right order, the same everywhere and for everyone. That's what a natural order is for:

Main.java
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Release> releases = new ArrayList<>();
        releases.add(new Release(2, 9, 5400));
        releases.add(new Release(10, 0, 820));
        releases.add(new Release(2, 10, 7300));
        releases.add(new Release(1, 4, 12000));

        Collections.sort(releases);
        System.out.println("History: " + releases);

        releases.sort(Comparator.comparing(Release::getDownloads).reversed());
        System.out.println("Most downloaded: " + releases);
    }
}
Release.java
public class Release implements Comparable<Release> {
    private int major;
    private int minor;
    private int downloads;

    public Release(int major, int minor, int downloads) {
        this.major = major;
        this.minor = minor;
        this.downloads = downloads;
    }

    public int getDownloads() {
        return this.downloads;
    }

    @Override
    public int compareTo(Release other) {
        int byMajor = Integer.compare(this.major, other.major);
        if (byMajor != 0) {
            return byMajor;
        }
        return Integer.compare(this.minor, other.minor);
    }

    @Override
    public String toString() {
        return this.major + "." + this.minor;
    }
}
Output
History: [1.4, 2.9, 2.10, 10.0]
Most downloaded: [1.4, 2.10, 2.9, 10.0]
  • The version numbers are ints, and compareTo compares them as step 3 did: the first part, and for a tie, the second.
  • The download ranking is the order of one page of the store. It changes every day, and it isn't what anyone means by "the order of the versions". So it's a comparator, written where that page sorts its list.

How to choose

  • In the class, with Comparable, when the objects have one order that everyone expects, the same all over the program: versions, times of day, dates, sizes. Then Collections.sort(list) and sorted() work with nothing more, and so does anything else in Java that needs an order. A class has only one compareTo, so give it the obvious order, or none.
  • Where you sort, with a Comparator, for every other order. There can be any number of them, and the class doesn't change: the flights by gate, the releases by downloads, the teas by price and then name. A rule that belongs to one page or one report goes next to the code of that page.
  • No natural order at all is fine. Is a hackathon entry "smaller" than another when its score is lower, or when its team's name comes first? Neither is obvious, and the contest's ranking rules might change. Such a class is better off without compareTo, and every sort says which order it wants. Step 1's hackathon challenge gave Entry a compareTo only to show the two values swapped. Step 3's Entry has none, and that's the better design.
  • A class that isn't yours keeps its natural order. String's compareTo is alphabetical, and you can't change the class String. A comparator can sort Strings in any other order, such as by length.
  • The natural order backwards is Comparator.reverseOrder(). releases.sort(Comparator.reverseOrder()) would put the newest version first.

The last two, on names:

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        names.add("Priya");
        names.add("Bo");
        names.add("Alexander");
        names.add("Ines");
        names.sort(Comparator.comparing(String::length));
        System.out.println(names);
        names.sort(Comparator.reverseOrder());
        System.out.println(names);
    }
}
Output
[Bo, Ines, Priya, Alexander]
[Priya, Ines, Bo, Alexander]

String::length is a method of the value itself, like module 37's String::toUpperCase, so Comparator.comparing(String::length) compares two names by their lengths.

You want Use Examples from this module
The one order everybody expects of the class implements Comparable<...> and compareTo talks by start time, versions by number
Another order, for one list or one page a lambda, or Comparator.comparing(...) flights by gate, releases by downloads
A second criterion for ties thenComparing, or a second comparison in compareTo talks by hour, then room
The largest first reversed(), or the two values swapped in Integer.compare the hackathon's scoreboard
A sorted view that leaves the list as it is sorted() or sorted(comparator) in a stream, or a sorted copy the podium, the largest videos

What this module covered

A class gets a natural order by implementing Comparable and its compareTo, which answers negative, zero or positive, and Integer.compare and String's own compareTo do most of the work. Collections.sort(list) and a stream's sorted() use that order. Any other order is a Comparator: a lambda of two objects, or one built with Comparator.comparing from a getter, turned around with reversed() and given more criteria with thenComparing, for list.sort, Collections.sort and sorted. These are the topics of the MOOC's section 10.2. Module 39 finishes part 10 with StringBuilder, regular expressions, enums and iterators.

Your task

Challenge 1 of 3

A student's planner lists exams by date. Exam.java has a course, a month and a day. Make Exam implement Comparable<Exam>, with a compareTo that puts exams in date order: by month, and exams in the same month by day. Exams on the same day are a tie, so compareTo gives 0 and the sort keeps them in the list's order. Put @Override above compareTo. The check runs:

ArrayList<Exam> exams = new ArrayList<>();
exams.add(new Exam("Statistics", 6, 12));
exams.add(new Exam("Algebra", 5, 28));
exams.add(new Exam("Biology", 6, 3));
exams.add(new Exam("History", 5, 30));
Collections.sort(exams);
System.out.println(exams);

and expects:

[Algebra 5/28, History 5/30, Biology 6/3, Statistics 6/12]

Challenge 2

A tree nursery sorts the names on its labels in several orders. A String's natural order is alphabetical. What does this program print?

Challenge 3

To end the module, the departures board again, now with a natural order. A board lists flights by time: that's the order anyone expects of it, so it becomes Flight's natural order. The gate staff want another order, so that one is a comparator. Write:

  • In Flight.java: make Flight implement Comparable<Flight>. Flights go by departure time, and flights that leave at the same time by destination, alphabetically.
  • In Board.java, the class Board, which keeps its flights in a list:
    • the constructor, and add(Flight flight), which adds a flight to the board
    • departures(), which returns a new list of all the flights in their natural order
    • atGate(int gate), which returns a new list of the flights from that gate, in their natural order
    • byGate(), which returns a new list of all the flights sorted by gate number, and the flights of the same gate by departure time

The lists that the methods return are new lists, not the board's own: flights added to the board later don't show up in a list it returned before. Main.java is a test drive, and it prints:

[07:45 Oslo (gate 12), 09:05 Riga (gate 12), 09:30 Dublin (gate 4), 13:10 Athens (gate 2), 13:10 Lisbon (gate 4)]
[07:45 Oslo (gate 12), 09:05 Riga (gate 12)]
[13:10 Athens (gate 2), 09:30 Dublin (gate 4), 13:10 Lisbon (gate 4), 07:45 Oslo (gate 12), 09:05 Riga (gate 12)]