Learn Java

Lesson 37Phase 4: Collections and algorithms

Comparable, Comparator and sorting

Giving objects a natural order, sorting by any rule with comparators, and keeping data sorted with TreeSet and TreeMap.

By Updated 50 min

  • Comparable
  • compareTo
  • Comparator
  • Comparator.comparing
  • thenComparing
  • reversed

A music app lists your playlist by title, and one tap later by artist, or by length with the longest song first. Java can sort strings and numbers straight away, because they come with an order built in. A Song class you write has no order until you give it one, and Java refuses to sort it. This lesson shows how to give a class its own order, how to sort by any rule you choose, and how to keep data sorted while you add to it.

By the end of this lesson
  • Explain why Collections.sort will not compile for a list of your own class, and why a TreeSet of them fails while the program runs.
  • Give a class a natural order by implementing Comparable, with a compareTo that keeps its contract.
  • Compare numbers with Integer.compare, and show with real numbers why subtracting them can give the wrong answer.
  • Write comparators as a class, as a lambda, and with Comparator.comparing, thenComparing and reversed.
  • Sort with List.sort, Collections.sort and Arrays.sort, and say what a stable sort promises.
  • Keep data sorted with TreeSet and TreeMap, and find neighbors with floor, ceiling, headSet and tailSet.
From before
In strings you used compareTo to put two words in dictionary order. Generics showed you the Comparable interface behind it, and a Song class that a method called max refused; this lesson finishes that story. Lambdas and method references such as Math::max come from enums and lambdas, and accessors such as song.title() from records. equals, hashCode and toString showed how a HashSet decides that two objects are the same, and HashMap and HashSet gave you a first look at TreeMap.

A list Java cannot sort#

Start with what already works. Here is a list of song titles and a list of song lengths in seconds, each sorted with one call to Collections.sort.

Titles.java
1import java.util.ArrayList;
2import java.util.Collections;
3import java.util.List;
4
5public class Main {
6 public static void main(String[] args) {
7 List<String> titles = new ArrayList<>(List.of("Paper Boats", "Hello", "Gravity", "Low Tide"));
8 Collections.sort(titles);
9 System.out.println(titles);
10
11 List<Integer> lengths = new ArrayList<>(List.of(214, 245, 198, 231));
12 Collections.sort(lengths);
13 System.out.println(lengths);
14 }
15}
Prints
›[Gravity, Hello, Low Tide, Paper Boats]
›[198, 214, 231, 245]

It prints [Gravity, Hello, Low Tide, Paper Boats] and [198, 214, 231, 245]: the titles in alphabetical order and the lengths from shortest to longest. Collections.sort rearranges the list you hand it. It does not make a new list.

Now put whole songs in the list, each with a title, an artist and a length, and sort that.

Main.java (does not compile)
read along
1import java.util.ArrayList;
2import java.util.Collections;
3import java.util.List;
4
5public class Main {
6 public static void main(String[] args) {
7 List<Song> playlist = new ArrayList<>();
8 playlist.add(new Song("Paper Boats", "June Okafor", 214));
9 playlist.add(new Song("Hello", "Mira Sol", 245));
10
11 Collections.sort(playlist);
12 System.out.println(playlist);
13 }
14}
15
16record Song(String title, String artist, int seconds) { }
Terminal
read along
1$ javac Main.java
2Main.java:11: error: no suitable method found for sort(List<Song>)
3 Collections.sort(playlist);
4 ^
5 method Collections.<T#1>sort(List<T#1>) is not applicable
6 (inference variable T#1 has incompatible bounds
7 equality constraints: Song
8 upper bounds: Comparable<? super T#1>)
9 method Collections.<T#2>sort(List<T#2>,Comparator<? super T#2>) is not applicable
10 (cannot infer type-variable(s) T#2
11 (actual and formal argument lists differ in length))
12 where T#1,T#2 are type-variables:
13 T#1 extends Comparable<? super T#1> declared in method <T#1>sort(List<T#1>)
14 T#2 extends Object declared in method <T#2>sort(List<T#2>,Comparator<? super T#2>)
151 error

The message is long, but you have seen its shape before. Collections has two sort methods. The one you called takes only a list, and its type parameter has a bound: the elements must be Comparable<? super T>, a type that can compare itself with others of its kind. The list holds Song, which is not comparable, so the two demands clash, just as they did when max refused a list of songs in generics. The other sort needs a second argument, which you will meet later on this page.

Behind the message is a plain problem. Java cannot tell whether one song comes before another. It could go by title, by artist or by length, and nothing in Song says which.

A TreeSet finds out while running#

A TreeSet keeps its elements sorted all the time, so it needs the same rule. This program compiles. Run it.

SortedPlaylist.java
1import java.util.TreeSet;
2
3public class Main {
4 public static void main(String[] args) {
5 TreeSet<Song> playlist = new TreeSet<>();
6 System.out.println("Adding the first song");
7 playlist.add(new Song("Paper Boats", "June Okafor", 214));
8 System.out.println("Added it");
9 }
10}
11
12record Song(String title, String artist, int seconds) { }
Prints
›Adding the first song
!Exception in thread "main" java.lang.ClassCastException: class Song cannot be cast to class java.lang.Comparable (Song is in unnamed module of loader 'app'; java.lang.Comparable is in module java.base of loader 'bootstrap')
! at Main.main(Main.java:7)
Stops on line 7

ClassCastException: class Song cannot be cast to class java.lang.Comparable (Song is in unnamed module of loader 'app'; java.lang.Comparable is in module java.base of loader 'bootstrap')

The object is not an instance of the class it was cast to.

It prints Adding the first song and then stops with a ClassCastException, whose message reads:

class Song cannot be cast to class java.lang.Comparable (Song is in unnamed module of loader 'app'; java.lang.Comparable is in module java.base of loader 'bootstrap')

The part in brackets only says where each class was loaded from, and can read a little differently on your computer. The start is what matters: to find the song's place, the set tried to treat it as a Comparable, and a Song is not one. It fails on the very first song, before there is anything to compare it with. Every TreeSet keeps its elements in a TreeMap, and a TreeMap compares its first key with itself, just to check that the key can be compared at all.

The two mistakes surface at different times because of how each one is declared. Collections.sort asks for a comparable type in its declaration, so the compiler can check the call. A TreeSet cannot ask for that in its type, because you may also give it a separate object that does the comparing (that comes later in this lesson), so it checks each element as it arrives.

Key idea

To sort anything, Java needs a rule that says which of two objects comes first. Strings and numbers bring their own. For a class of yours, you supply the rule.

Comparable: a natural order for your class#

The first way to supply the rule is to build it into the class. An order that a class carries with it is called its natural order: alphabetical for strings, smallest first for numbers. A class gets one by implementing Comparable<T>, the interface from java.lang that you saw in generics. Its single method, int compareTo(T other), compares the object it is called on with other.

Here Song gets an order by title. Its compareTo hands the work to the compareTo that String already has. Step through it and keep an eye on the call stack.

ByTitle.java
1import java.util.ArrayList;
2import java.util.Collections;
3import java.util.List;
4
5public class Main {
6 public static void main(String[] args) {
7 List<Song> playlist = new ArrayList<>();
8 playlist.add(new Song("Paper Boats", "June Okafor", 214));
9 playlist.add(new Song("Hello", "Mira Sol", 245));
10 playlist.add(new Song("Gravity", "The Lanterns", 198));
11
12 Collections.sort(playlist);
13 for (Song song : playlist) {
14 System.out.println(song.title() + " by " + song.artist());
15 }
16 }
17}
18
19record Song(String title, String artist, int seconds) implements Comparable<Song> {
20 @Override
21 public int compareTo(Song other) {
22 return title.compareTo(other.title);
23 }
24}
Prints
›Gravity by The Lanterns
›Hello by Mira Sol
›Paper Boats by June Okafor

Now Collections.sort compiles, and the program prints Gravity by The Lanterns, Hello by Mira Sol and Paper Boats by June Okafor. In the trace, the sort calls your compareTo again and again, each time with two songs, and moves the songs according to the answers. compareToonly ever compares two songs. Deciding where each song ends up is the sort's job.

implements Comparable<Song>goes after the record's components, the way it would go after a class name. The <Song> says what a song can be compared with: other songs, so compareTo takes a Song and needs no cast.

Records do not write compareTo for you
A record writes equals, hashCode and toString from its components, but never compareTo. Java cannot guess which component should decide the order, so a record that needs a natural order implements Comparable itself, as Song does here.

What compareTo returns#

a.compareTo(b) returns an int, and only its sign carries meaning:

  • A negative number means a comes before b.
  • Zero means they tie: neither comes first.
  • A positive number means a comes after b.
Signs.java
1public class Main {
2 public static void main(String[] args) {
3 Song gravity = new Song("Gravity", "The Lanterns", 198);
4 Song boats = new Song("Paper Boats", "June Okafor", 214);
5
6 System.out.println(gravity.compareTo(boats));
7 System.out.println(boats.compareTo(gravity));
8 System.out.println(boats.compareTo(boats));
9 }
10}
11
12record Song(String title, String artist, int seconds) implements Comparable<Song> {
13 @Override
14 public int compareTo(Song other) {
15 return title.compareTo(other.title);
16 }
17}
Prints
›-9
›9
›0

It prints -9, 9 and 0. gravity.compareTo(boats) is negative, so Gravity comes first. The 9 comes from String: the first letters differ, and 'G' (71) minus 'P' (80) is -9. Swap the two songs and the sign flips. A song compared with itself gives 0. Another class may answer -1 where String answers -9, so code that reads the result should only test the sign, with < 0, == 0 or > 0.

Sorting methods and TreeSet trust your compareTo to behave like a real order. The documentation of Comparable spells out what that means. For any objects a, b and c:

RuleIn plain words
Swapping flips the signIf a comes before b, then b comes after a, and a tie is a tie both ways. So a.compareTo(a) must be 0.
TransitiveIf a comes before b and b comes before c, then a comes before c.
Ties agreeIf a and b tie, they compare the same way with every other object.
Agree with equals (recommended)compareTo returns 0 exactly when equals returns true. Java strongly recommends this without requiring it, and a later section shows what goes wrong without it.
The rules from the documentation of Comparable, in plain words. It also asks compareTo to throw a NullPointerException when given null, where equals returns false.
Quick check
A Ticket has an int number field, from 1 to 500. Which compareTo breaks the contract?

Comparing numbers: subtract, or Integer.compare?#

To order songs by length, compareTo must compare two int fields. A shortcut turns up in a lot of code: return the difference, seconds - other.seconds. When this song is the shorter one the difference is negative, when it is longer the difference is positive, and equal lengths give 0. Song lengths are small, so for them it works. For large numbers it can give the wrong sign.

A bank keeps each balance in cents as an int, and its Account record sorts with the subtraction shortcut. Zara has 2,000,000,000 cents, which is twenty million dollars. Omar owes money, so his balance is -500,000,000.

Accounts.java
1import java.util.ArrayList;
2import java.util.Collections;
3import java.util.List;
4
5public class Main {
6 public static void main(String[] args) {
7 Account zara = new Account("Zara", 2_000_000_000);
8 Account omar = new Account("Omar", -500_000_000);
9
10 System.out.println(zara.balance() - omar.balance());
11 System.out.println(zara.compareTo(omar));
12
13 List<Account> accounts = new ArrayList<>();
14 accounts.add(zara);
15 accounts.add(omar);
16 Collections.sort(accounts);
17 for (Account a : accounts) {
18 System.out.println(a.owner() + " " + a.balance());
19 }
20 }
21}
22
23record Account(String owner, int balance) implements Comparable<Account> {
24 @Override
25 public int compareTo(Account other) {
26 return balance - other.balance;
27 }
28}
Prints
›-1794967296
›-1794967296
›Zara 2000000000
›Omar -500000000

The first two lines both print -1794967296. The true difference is 2,500,000,000, but an int only goes up to 2,147,483,647. The subtraction runs past the top and wraps around, the way the hash codes did in HashMap and HashSet: Java subtracts 232 (4,294,967,296) and lands on -1,794,967,296. compareToreturns that negative number, which claims that Zara comes before Omar. So the “smallest first” sort prints Zara 2000000000 above Omar -500000000, the richest account first, with no error anywhere.

Integer.compare(x, y) compares without subtracting. It returns a negative number when x is smaller, 0 when the two are equal and a positive number when x is larger, and nothing can overflow. Change that one line:

AccountsFixed.java
1import java.util.ArrayList;
2import java.util.Collections;
3import java.util.List;
4
5public class Main {
6 public static void main(String[] args) {
7 Account zara = new Account("Zara", 2_000_000_000);
8 Account omar = new Account("Omar", -500_000_000);
9
10 System.out.println(zara.compareTo(omar));
11
12 List<Account> accounts = new ArrayList<>();
13 accounts.add(zara);
14 accounts.add(omar);
15 Collections.sort(accounts);
16 for (Account a : accounts) {
17 System.out.println(a.owner() + " " + a.balance());
18 }
19 }
20}
21
22record Account(String owner, int balance) implements Comparable<Account> {
23 @Override
24 public int compareTo(Account other) {
25 return Integer.compare(balance, other.balance);
26 }
27}
Prints
›1
›Omar -500000000
›Zara 2000000000

Now zara.compareTo(omar) prints 1, and the sort puts Omar first and Zara second. (The documentation only promises the sign. The JDK's code happens to return exactly -1, 0 or 1.)

Use the compare method of the wrapper class
Each wrapper class has a static compare: Integer.compare, Long.compare, Double.compare, Character.compare and Boolean.compare. Use them for every field of a primitive type, even small numbers, and you never have to ask whether a difference might overflow. Double.compare also handles the odd values NaN and -0.0, as the lesson on equals and hashCode showed.
Key idea

compareTo answers with a sign: negative when this object comes first, zero for a tie, positive when it comes after. Compare int fields with Integer.compare, never by subtracting.

When compareTo and equals disagree#

Two different songs can share a title. Mira Sol and Static Bloom have each released a song called Hello. With Song ordered by title alone, decide what each line prints, then run it.

Predict the outputTwoHellos.java
1import java.util.HashSet;
2import java.util.TreeSet;
3
4public class Main {
5 public static void main(String[] args) {
6 Song first = new Song("Hello", "Mira Sol", 245);
7 Song second = new Song("Hello", "Static Bloom", 214);
8
9 System.out.println(first.equals(second));
10 System.out.println(first.compareTo(second));
11
12 TreeSet<Song> sorted = new TreeSet<>();
13 sorted.add(first);
14 sorted.add(second);
15 System.out.println(sorted.size());
16
17 HashSet<Song> hashed = new HashSet<>();
18 hashed.add(first);
19 hashed.add(second);
20 System.out.println(hashed.size());
21 }
22}
23
24record Song(String title, String artist, int seconds) implements Comparable<Song> {
25 @Override
26 public int compareTo(Song other) {
27 return title.compareTo(other.title);
28 }
29}

What do the four lines print?

It prints false, 0, 1 and 2. The two records are not equal, because their artists differ. compareTo still returns 0, because it only looks at titles. The HashSet asks hashCode and equals, sees two different songs and keeps both. The TreeSet never calls equals. To place the second Hello it compares it with what it already holds, gets 0 from the first Hello, and concludes the song is already there. Its add returns false, and Static Bloom's song is gone without any warning.

The documentation calls an order consistent with equals when compareTo returns 0 for exactly the pairs that equals calls equal. Song's order is not. A TreeSet or TreeMap built on such an order still works, but it no longer behaves like the sets and maps you know: it can drop elements, or report that it contains one it never received.

Comparing more than one field#

The fix is to look at a second field when the first one ties. Compare the titles and keep that answer unless it is 0. Only a tie moves on to the artists.

TitleThenArtist.java
1import java.util.TreeSet;
2
3public class Main {
4 public static void main(String[] args) {
5 TreeSet<Song> library = new TreeSet<>();
6 library.add(new Song("Hello", "Mira Sol", 245));
7 library.add(new Song("Hello", "Static Bloom", 214));
8 library.add(new Song("Gravity", "The Lanterns", 198));
9
10 for (Song song : library) {
11 System.out.println(song.title() + " by " + song.artist());
12 }
13 }
14}
15
16record Song(String title, String artist, int seconds) implements Comparable<Song> {
17 @Override
18 public int compareTo(Song other) {
19 int byTitle = title.compareTo(other.title);
20 if (byTitle != 0) {
21 return byTitle;
22 }
23 return artist.compareTo(other.artist);
24 }
25}
Prints
›Gravity by The Lanterns
›Hello by Mira Sol
›Hello by Static Bloom

The set keeps all three songs and walks them in order: Gravity by The Lanterns, Hello by Mira Sol, Hello by Static Bloom. Step through the second add: the titles tie, so byTitle is 0, and the artists decide. If your app could hold two recordings of one song by one artist, compare the length as a third field, so that compareTo ties only when equals would say the records are equal.

Key idea

A TreeSet or TreeMap decides whether two elements are the same with compareTo, never with equals. Make compareTo return 0 only for objects that equals calls equal.

Put the lines in orderArtistThenTitle.java
Put the lines back in order: a Song whose natural order is by artist and then by title, kept in a TreeSet. The right program prints Mira Sol: Hello, Mira Sol: Low Tide and The Lanterns: Gravity. The three add lines can go in any order, because the set sorts the songs itself.

Tap a line, then tap where it should go. Or use the arrows.

Now write one for an ordinary class. Version numbers are a good test, because sorting them as text goes wrong.

ExerciseVersions.java
As strings, "1.10" sorts before "1.9": after the matching 1., the character 1 comes before 9. Finish compareTo so that versions sort by their major number and, when those tie, by their minor number. The last line should print As versions: [1.2, 1.9, 1.10, 2.0].

Comparator: an order from outside the class#

A class has one natural order, but a music app needs several: by title, by artist, by length, longest first. And some classes cannot take a compareTo from you at all. String already has its alphabetical order, and you cannot open String and change it to sort words by length. For every order other than the natural one, Java uses a separate object called a comparator.

A comparator implements java.util.Comparator<T>, whose one abstract method is int compare(T a, T b). It answers with the same signs as compareTo, but both objects come in as arguments: negative when a comes first, zero for a tie, positive when a comes after b. With exactly one abstract method, Comparator is a functional interface, as enums and lambdas mentioned.

A comparator class#

The long way is a class that implements the interface. ByLength orders songs from shortest to longest:

ByLength.java
1import java.util.ArrayList;
2import java.util.Comparator;
3import java.util.List;
4
5public class Main {
6 public static void main(String[] args) {
7 List<Song> playlist = new ArrayList<>();
8 playlist.add(new Song("Paper Boats", "June Okafor", 214));
9 playlist.add(new Song("Hello", "Mira Sol", 245));
10 playlist.add(new Song("Gravity", "The Lanterns", 198));
11
12 playlist.sort(new ByLength());
13 for (Song song : playlist) {
14 System.out.println(song.seconds() + " s " + song.title());
15 }
16
17 playlist.sort(new ByLength().reversed());
18 System.out.println("Longest: " + playlist.get(0).title());
19 }
20}
21
22class ByLength implements Comparator<Song> {
23 @Override
24 public int compare(Song a, Song b) {
25 return Integer.compare(a.seconds(), b.seconds());
26 }
27}
28
29record Song(String title, String artist, int seconds) { }
Prints
›198 s Gravity
›214 s Paper Boats
›245 s Hello
›Longest: Hello

playlist.sort(new ByLength()) sorts the list with that comparator, so the songs print from 198 seconds up to 245. This Song does not implement Comparable at all; the comparator supplies the whole rule. The last sort uses new ByLength().reversed(). reversed() is a default method of Comparator that returns a new comparator with the opposite order, so the longest song comes first and the program ends with Longest: Hello.

A lambda#

A whole class for one line of logic is a lot of ceremony. Since Comparator is a functional interface, a lambda can be its compare method:

The same sort, with a lambda
read along
1playlist.sort((a, b) -> Integer.compare(a.seconds(), b.seconds()));

The lambda takes two songs, a and b, and returns exactly what ByLength.compare returned.

Comparator.comparing and method references#

Most comparators look like that lambda: take the same value out of both objects, then compare the two values. That value is called the sort key. Comparator.comparing writes the rest for you. You give it a function that takes one song and returns its key, and it builds the comparator. These two lines both sort by artist:

Two ways to say the same thing
read along
1playlist.sort(Comparator.comparing(song -> song.artist()));
2playlist.sort(Comparator.comparing(Song::artist));

The second line uses a method reference. In enums and lambdas you met Math::max, which names a static method. Song::artist names an instance method of Song and means “call artist() on whichever song you are given”. It is short for the lambda song -> song.artist(), and it reads almost like the sentence “compare songs by artist”. For an ordinary class with getters, name the getter the same way, as in Comparator.comparing(Student::getName).

For an int key, use Comparator.comparingInt(Song::seconds). Comparator.comparing(Song::seconds) works too, but it first boxes every length into an Integer, while comparingInt compares the plain int values. comparingLong and comparingDouble do the same for long and double keys. All of these arrived in Java 8, together with lambdas.

SortKeys.java
1import java.util.ArrayList;
2import java.util.Comparator;
3import java.util.List;
4
5public class Main {
6 public static void main(String[] args) {
7 List<Song> playlist = new ArrayList<>();
8 playlist.add(new Song("Paper Boats", "June Okafor", 214));
9 playlist.add(new Song("Hello", "Mira Sol", 245));
10 playlist.add(new Song("Gravity", "The Lanterns", 198));
11
12 playlist.sort(Comparator.comparing(Song::artist));
13 System.out.println("By artist: " + titles(playlist));
14
15 playlist.sort(Comparator.comparingInt(Song::seconds));
16 System.out.println("By length: " + titles(playlist));
17
18 List<String> words = new ArrayList<>(List.of("banana", "kiwi", "fig", "apple"));
19 words.sort(Comparator.comparing(String::length));
20 System.out.println("Words by length: " + words);
21 }
22
23 static List<String> titles(List<Song> songs) {
24 List<String> names = new ArrayList<>();
25 for (Song song : songs) {
26 names.add(song.title());
27 }
28 return names;
29 }
30}
31
32record Song(String title, String artist, int seconds) { }
Prints
›By artist: [Paper Boats, Hello, Gravity]
›By length: [Gravity, Paper Boats, Hello]
›Words by length: [fig, kiwi, apple, banana]

By artist, the order is [Paper Boats, Hello, Gravity], because June Okafor comes before Mira Sol and The Lanterns. By length it is [Gravity, Paper Boats, Hello]. The last line sorts strings by String::length, an order String itself does not have: [fig, kiwi, apple, banana].

A lambda can lose its type when you add reversed()
Comparator.comparing(song -> song.title()).reversed() does not compile. javac stops with cannot find symbol for title(), in variable song of type Object. Java works out a lambda's parameter type from where the result is used, but here the result of comparing is used only to call reversed() on, which says nothing about songs, so song falls back to Object. Write the method reference Comparator.comparing(Song::title).reversed(), which names the type, or give the parameter a type: (Song song) -> song.title().
Comparable
Comparator
Lives inside the class: implements Comparable<Song>.
A separate object, often a lambda or a Comparator.comparing call.
One method: a.compareTo(b).
One method: c.compare(a, b).
Gives the class one natural order.
As many orders as you like: by artist, by length, longest first.
Used when no comparator is given: Collections.sort(list), new TreeSet<>().
Handed over: list.sort(c), new TreeSet<>(c), Arrays.sort(array, c).
Needs you to change the class.
Works for classes you cannot change, such as String.

Chaining comparators#

Real orders often need more than one key. Comparator has methods that build a bigger comparator out of smaller ones. Each returns a new comparator and leaves the one you called it on unchanged.

thenComparing breaks ties#

Sorting by artist leaves the songs of each artist tied. Comparator.comparing(Song::artist).thenComparing(Song::title) compares the artists first and uses that answer unless it is 0. Only for a tie does it compare the titles. That is the pattern you wrote by hand in compareTo: the first key decides unless it ties.

ArtistThenTitle.java
1import java.util.ArrayList;
2import java.util.Comparator;
3import java.util.List;
4
5public class Main {
6 public static void main(String[] args) {
7 List<Song> playlist = new ArrayList<>();
8 playlist.add(new Song("Paper Boats", "June Okafor", 214));
9 playlist.add(new Song("Low Tide", "Mira Sol", 231));
10 playlist.add(new Song("Gravity", "The Lanterns", 198));
11 playlist.add(new Song("Northern Lights", "June Okafor", 263));
12 playlist.add(new Song("Hello", "Mira Sol", 245));
13
14 playlist.sort(Comparator.comparing(Song::artist).thenComparing(Song::title));
15 for (Song song : playlist) {
16 System.out.println(song.artist() + ": " + song.title());
17 }
18 }
19}
20
21record Song(String title, String artist, int seconds) { }
Prints
›June Okafor: Northern Lights
›June Okafor: Paper Boats
›Mira Sol: Hello
›Mira Sol: Low Tide
›The Lanterns: Gravity

It prints June Okafor's two songs, then Mira Sol's two, then The Lanterns' one, and inside each artist the titles are alphabetical: Northern Lights before Paper Boats, Hello before Low Tide.

For an int key there is thenComparingInt, as in Comparator.comparing(Song::artist).thenComparingInt(Song::seconds), which puts each artist's shortest song first. thenComparing also accepts a whole comparator, and since a comparator is an object like any other, you can give the pieces names and reuse them: Comparator<Song> byArtist = Comparator.comparing(Song::artist); and a byTitle made the same way combine into byArtist.thenComparing(byTitle), the same order as above.

reversed flips everything before it#

reversed() returns a comparator that gives the opposite answer to the whole comparator it was called on. In a chain, that means everything written to its left. So where you put it decides which keys run backwards. Here the named byArtist and byTitle go into two chains that differ only in the place of reversed().

Predict the outputWhereReversedGoes.java
1import java.util.ArrayList;
2import java.util.Comparator;
3import java.util.List;
4
5public class Main {
6 public static void main(String[] args) {
7 List<Song> playlist = new ArrayList<>();
8 playlist.add(new Song("Paper Boats", "June Okafor"));
9 playlist.add(new Song("Low Tide", "Mira Sol"));
10 playlist.add(new Song("Gravity", "The Lanterns"));
11 playlist.add(new Song("Northern Lights", "June Okafor"));
12 playlist.add(new Song("Hello", "Mira Sol"));
13
14 Comparator<Song> byArtist = Comparator.comparing(Song::artist);
15 Comparator<Song> byTitle = Comparator.comparing(Song::title);
16
17 playlist.sort(byArtist.thenComparing(byTitle).reversed());
18 System.out.println(titles(playlist));
19
20 playlist.sort(byArtist.reversed().thenComparing(byTitle));
21 System.out.println(titles(playlist));
22 }
23
24 static List<String> titles(List<Song> songs) {
25 List<String> names = new ArrayList<>();
26 for (Song song : songs) {
27 names.add(song.title());
28 }
29 return names;
30 }
31}
32
33record Song(String title, String artist) { }

The first sort reverses after thenComparing, the second before it. Pick the two lines it prints.

Both lines start with Gravity, because both put the artists from Z to A and The Lanterns comes last in the alphabet. They differ inside each artist. In the first chain reversed() comes after thenComparing(byTitle), so it flips the titles as well: Low Tide before Hello, Paper Boats before Northern Lights. In the second, reversed() only covers byArtist, and the titles added after it stay A to Z.

To flip just one key, flip it before it joins the chain. Both of these sort artists A to Z and, inside each artist, put the longest song first:

Reversing one key only
read along
1Comparator<Song> byLength = Comparator.comparingInt(Song::seconds);
2playlist.sort(byArtist.thenComparing(byLength.reversed()));
3playlist.sort(Comparator.comparing(Song::artist).thenComparing(Song::seconds, Comparator.reverseOrder()));

The second form passes a comparator for the key itself: thenComparing(Song::seconds, ...) takes the lengths and compares them with Comparator.reverseOrder(), which you meet properly below.

Build your own chains in the lab. Each button adds one call to the comparator, and the playlist sorts with List.sort after every click. The #column is each song's place in the original playlist.

Comparator chain lab
// Add a key below to start a comparator.

No comparator yet, so the playlist is in the order the songs were added.

#TitleArtistLength
  1. #1Paper BoatsJune Okafor3:34
  2. #2Low TideMira Sol3:51
  3. #3GravityThe Lanterns3:18
  4. #4Northern LightsJune Okafor4:23
  5. #5HelloMira Sol4:05
  6. #6HelloStatic Bloom3:34

Nothing has been compared yet.

Try artist, then title, then reversed(), and compare it with artist, reversed(), then title. Then sort by length alone and look at the two songs of 3:34: whichever comparator you build, tied songs keep their playlist order. The section on stable sorting explains why.

Quick check
A class register lists students by surname, and students who share a surname by first name. Which comparator does that?

naturalOrder and reverseOrder#

Comparator.naturalOrder() is a comparator that calls compareTo, and Comparator.reverseOrder()is one that calls it the other way round. They let you use a type's natural order wherever a comparator is expected. words.sort(Comparator.reverseOrder()) sorts strings from Z to A, and both turn up as arguments to other comparators, as in the next program.

Lists with nulls in them#

Some songs are singles, released on no album, so their album is null. Sorting a list that contains null by its natural order throws a NullPointerException, because there is no object to call compareTo on. Comparator.nullsFirst(c) wraps another comparator c: every null comes first, and c orders the rest. nullsLast puts the nulls at the end.

Albums.java
1import java.util.ArrayList;
2import java.util.Collections;
3import java.util.Comparator;
4import java.util.List;
5
6public class Main {
7 public static void main(String[] args) {
8 List<String> albums = new ArrayList<>();
9 albums.add("Tides");
10 albums.add(null);
11 albums.add("Echoes");
12 albums.add("Amber");
13 albums.add(null);
14
15 albums.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
16 System.out.println(albums);
17
18 albums.sort(Comparator.nullsLast(Comparator.reverseOrder()));
19 System.out.println(albums);
20
21 Collections.sort(albums);
22 System.out.println("This line is never reached");
23 }
24}
Prints
›[null, null, Amber, Echoes, Tides]
›[Tides, Echoes, Amber, null, null]
!Exception in thread "main" java.lang.NullPointerException
! at Main.main(Main.java:21)
Stops on line 21

NullPointerException

A variable held null, so there was no object to use.

It prints [null, null, Amber, Echoes, Tides], then [Tides, Echoes, Amber, null, null], and the plain Collections.sort on the last lines stops with a NullPointerException. To sort songs by an album that may be missing, pass the null-friendly comparator for the key: Comparator.comparing(Song::album, Comparator.nullsFirst(Comparator.naturalOrder())).

ExerciseLeaderboard.java
A game shows a leaderboard. Change ranking so that the highest score comes first, and players on the same score appear in alphabetical order. Player is a record, so its method references are Player::score and Player::name.
Key idea

Build an order from small parts: comparing picks the first key, thenComparing breaks ties, and reversed() flips everything written before it.

List.sort, Collections.sort and stable sorting#

You have now sorted lists two ways. list.sort(comparator) is a method of List itself, added in Java 8. Collections.sort(list) sorts by the natural order, and Collections.sort(list, comparator) takes a comparator; both hand the work to list.sort, so the results are identical. Passing null to list.sortmeans “use the natural order”.

All of them sort the list in place and return nothing. That means the list must allow changes. A list made with List.of does not, so copy it into an ArrayList first:

InPlace.java
1import java.util.ArrayList;
2import java.util.Collections;
3import java.util.Comparator;
4import java.util.List;
5
6public class Main {
7 public static void main(String[] args) {
8 List<String> fixed = List.of("Tides", "Amber", "Echoes");
9 List<String> copy = new ArrayList<>(fixed);
10
11 copy.sort(null);
12 System.out.println(copy);
13
14 Collections.sort(copy, Comparator.reverseOrder());
15 System.out.println(copy);
16
17 System.out.println(fixed);
18 Collections.sort(fixed);
19 System.out.println("This line is never reached");
20 }
21}
Prints
›[Amber, Echoes, Tides]
›[Tides, Echoes, Amber]
›[Tides, Amber, Echoes]
!Exception in thread "main" java.lang.UnsupportedOperationException
! at Main.main(Main.java:18)
Stops on line 18

UnsupportedOperationException

This collection cannot be changed. List.of and similar methods make read-only lists.

The copy sorts to [Amber, Echoes, Tides] and then to [Tides, Echoes, Amber], while the original still prints [Tides, Amber, Echoes]. Sorting the List.of list itself throws an UnsupportedOperationException, even though nothing has been added or removed.

What stable means#

When a comparator says two elements tie, the documentation of List.sort decides which one goes first: the sort must not reorder equal elements. A sort that always keeps tied elements in the order they started in is called stable.

Here is a play queue, in the order the songs were added, sorted by artist alone. Step through it and watch the two songs of each artist.

StableQueue.java
1import java.util.ArrayList;
2import java.util.Comparator;
3import java.util.List;
4
5public class Main {
6 public static void main(String[] args) {
7 List<Song> queue = new ArrayList<>();
8 queue.add(new Song("Paper Boats", "June Okafor", 214));
9 queue.add(new Song("Low Tide", "Mira Sol", 231));
10 queue.add(new Song("Gravity", "The Lanterns", 198));
11 queue.add(new Song("Northern Lights", "June Okafor", 263));
12 queue.add(new Song("Hello", "Mira Sol", 245));
13
14 queue.sort(Comparator.comparing(Song::artist));
15 for (Song song : queue) {
16 System.out.println(song.artist() + ": " + song.title());
17 }
18 }
19}
20
21record Song(String title, String artist, int seconds) { }
Prints
›June Okafor: Paper Boats
›June Okafor: Northern Lights
›Mira Sol: Low Tide
›Mira Sol: Hello
›The Lanterns: Gravity

The artists come out A to Z, and inside each artist the songs keep their queue order: Paper Boats before Northern Lights, Low Tide before Hello. Neither pair is alphabetical; they tie on artist, so the sort left them as they were. A stable sort lets you sort in two passes. Sort by title first, then by artist, and the artists come out in order with each artist's titles still alphabetical, the same result as byArtist.thenComparing(byTitle).

Quick check
What does Collections.sort(playlist) give back?

Sorting arrays of objects#

Arrays have the same two choices. Arrays.sort(array) sorts by the natural order, and Arrays.sort(array, comparator) by any comparator. The sort for arrays of objects is stable too.

Chart.java
1import java.util.Arrays;
2import java.util.Comparator;
3
4public class Main {
5 public static void main(String[] args) {
6 String[] words = {"banana", "kiwi", "fig", "pear", "apple"};
7 Arrays.sort(words);
8 System.out.println(Arrays.toString(words));
9
10 Arrays.sort(words, Comparator.comparing(String::length));
11 System.out.println(Arrays.toString(words));
12
13 Song[] chart = {
14 new Song("Paper Boats", "June Okafor", 214),
15 new Song("Low Tide", "Mira Sol", 231),
16 new Song("Gravity", "The Lanterns", 198)
17 };
18 Arrays.sort(chart, Comparator.comparingInt(Song::seconds).reversed());
19 for (Song song : chart) {
20 System.out.println(song.seconds() + " " + song.title());
21 }
22
23 Arrays.sort(chart);
24 System.out.println("This line is never reached");
25 }
26}
27
28record Song(String title, String artist, int seconds) { }
Prints
›[apple, banana, fig, kiwi, pear]
›[fig, kiwi, pear, apple, banana]
›231 Low Tide
›214 Paper Boats
›198 Gravity
!Exception in thread "main" java.lang.ClassCastException: class Song cannot be cast to class java.lang.Comparable (Song is in unnamed module of loader 'app'; java.lang.Comparable is in module java.base of loader 'bootstrap')
! at Main.main(Main.java:23)
Stops on line 23

ClassCastException: class Song cannot be cast to class java.lang.Comparable (Song is in unnamed module of loader 'app'; java.lang.Comparable is in module java.base of loader 'bootstrap')

The object is not an instance of the class it was cast to.

The words print as [apple, banana, fig, kiwi, pear], then by length as [fig, kiwi, pear, apple, banana]. kiwi and pear both have four letters, and they stay in the alphabetical order the first sort gave them, because the sort is stable. The chart prints its songs longest first: 231 Low Tide, 214 Paper Boats, 198 Gravity.

The last call has no comparator, and this Song has no natural order, yet the line compiles. Arrays.sort takes any array of objects, so the compiler cannot check, and the program stops while running with the same ClassCastException the TreeSet gave.

No comparators for primitive arrays
Arrays.sort has no version that takes an int[] and a comparator, so Arrays.sort(scores, Comparator.reverseOrder()) does not compile for an int[]. To sort numbers largest first, keep them in an Integer[] or a List<Integer>, or sort the int[] and read it from the back.

TreeSet and TreeMap keep things sorted#

Sorting a list is a one-off job. Add a song afterwards and the list is out of order again. A TreeSet keeps its elements in order all the time: every add slots the new element into its place. A TreeMap does the same for its keys. Inside, both keep a balanced tree, a red-black tree, in which every node has the smaller elements on one side and the larger ones on the other. An add, contains or remove walks down from the top and visits only a few nodes, even in a large set. The documentation promises log(n) time for them, a phrase that Big-O in plain words explains later.

Because the elements are always in order, a TreeSet can answer questions about neighbors. Here is a bus timetable. Each time is written as a number, so 705 means 7:05 and 840 means 8:40. Step through it and watch the set in the memory view stay sorted as the times arrive out of order.

BusStop.java
1import java.util.TreeSet;
2
3public class Main {
4 public static void main(String[] args) {
5 TreeSet<Integer> departures = new TreeSet<>();
6 departures.add(750);
7 departures.add(705);
8 departures.add(840);
9 departures.add(720);
10 departures.add(815);
11 System.out.println(departures);
12
13 System.out.println("First bus: " + departures.first());
14 System.out.println("Last bus: " + departures.last());
15
16 int now = 731;
17 System.out.println("Next bus: " + departures.ceiling(now));
18 System.out.println("Just missed: " + departures.floor(now));
19 System.out.println("Before 800: " + departures.headSet(800));
20 System.out.println("From 800: " + departures.tailSet(800));
21 System.out.println(departures.ceiling(900));
22 }
23}
Prints
›[705, 720, 750, 815, 840]
›First bus: 705
›Last bus: 840
›Next bus: 750
›Just missed: 720
›Before 800: [705, 720, 750]
›From 800: [815, 840]
›null
  • The set prints [705, 720, 750, 815, 840], sorted, whatever order the times were added in.
  • first() and last() give the smallest and largest elements, 705 and 840.
  • ceiling(731) gives the smallest element that is 731 or more: the next bus, at 750. floor(731) gives the largest that is 731 or less: 720, the bus you just missed.
  • headSet(800) is the part of the set below 800, [705, 720, 750], and tailSet(800) the part from 800 up, [815, 840].
  • When no element fits, these methods return null: there is no bus at 900 or later, so the last line prints null.
CallReturnsGood to know
first(), last()The smallest or the largest elementOn an empty set they throw a NoSuchElementException.
floor(x), ceiling(x)The nearest element at or below x, or at or above itCan return x itself. null when nothing fits.
lower(x), higher(x)The nearest element strictly below or strictly above xNever return x itself. null when nothing fits.
headSet(x)Every element below xLeaves x out.
tailSet(x)Every element from x upKeeps x in.
TreeSet's neighbor methods. TreeMap has the same for its keys: firstKey, lastKey, floorKey, ceilingKey, lowerKey, higherKey, headMap and tailMap.

headSet and tailSet return views of the set rather than copies. Keep departures.tailSet(800) in a variable, add a bus at 910 to departures, and the view shows it as well.

Quick check
A shoe shop keeps its sizes in stock in a TreeSet: 36, 38, 40 and 42. A customer wants size 39, or the nearest size up if 39 is missing. Which call finds it?

A TreeSet or TreeMap with a comparator#

Hand a comparator to the constructor and the set or map uses it instead of the natural order. The comparator then decides two things at once: the order, and which elements count as the same.

TreeComparators.java
1import java.util.Comparator;
2import java.util.List;
3import java.util.TreeMap;
4import java.util.TreeSet;
5
6public class Main {
7 public static void main(String[] args) {
8 TreeSet<String> lengthOnly = new TreeSet<>(Comparator.comparing(String::length));
9 lengthOnly.addAll(List.of("kiwi", "fig", "banana", "pear", "apple"));
10 System.out.println(lengthOnly);
11
12 TreeSet<String> byLength = new TreeSet<>(Comparator.comparing(String::length)
13 .thenComparing(Comparator.naturalOrder()));
14 byLength.addAll(List.of("kiwi", "fig", "banana", "pear", "apple"));
15 System.out.println(byLength);
16
17 TreeMap<String, Integer> counts = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
18 for (String word : "The cat saw the dog and THE bird".split(" ")) {
19 counts.merge(word, 1, Integer::sum);
20 }
21 System.out.println(counts);
22 }
23}
Prints
›[fig, kiwi, apple, banana]
›[fig, kiwi, pear, apple, banana]
›{and=1, bird=1, cat=1, dog=1, saw=1, The=3}
  • The first set prints [fig, kiwi, apple, banana]. pear has four letters, like kiwi, so the comparator called them a tie and the set dropped pear: the Hello problem again, with a comparator this time.
  • The second set breaks ties with Comparator.naturalOrder() and keeps all five: [fig, kiwi, pear, apple, banana].
  • The map uses String.CASE_INSENSITIVE_ORDER, a comparator that treats capital and small letters as the same, so The, the and THE are one key. It prints {and=1, bird=1, cat=1, dog=1, saw=1, The=3}: the key keeps the spelling it was first put in with, and its count is 3.

new TreeMap<>(Comparator.reverseOrder()) works the same way and keeps its keys largest first, such as a map from each year to the songs released that year, newest year first.

The same rules, one more time
A PriorityQueuealways hands out its smallest element first, and it decides what “smallest” means with compareTo or a comparator, exactly as a TreeSet does. It comes next, in stacks, queues and deques.

Try it

Answer each one in your head or on paper first, then open the answer.

1
A Ticket class has no compareTo, and tickets is a List<Ticket>. Collections.sort(tickets) does not compile, but tickets.sort(null) does. What happens when this program runs, and why does the compiler let it through?
2
A class's compareTo returns 0 for two objects that its equals says are different. Name a collection that will keep only one of them, and one that keeps both.
3
What does this print? Watch the four-letter words.
4
Write a comparator that orders songs longest first, and songs of the same length by title from A to Z.
5
Why is return seconds - other.seconds; safe for song lengths, and still a habit to avoid?
6
A TreeSet holds 705, 720, 750, 815 and 840. What do floor(750), lower(750), headSet(750) and tailSet(750) return?

Practice

Work through these on your own. Each one builds on this page.

1
Write a Student record with a name and a mark from 0 to 100, whose natural order is by name. Print a class list in natural order with Collections.sort, then a results list with the highest mark first and equal marks by name, using a comparator. Both lists come from the same students.
2
A blog keeps its tags in a TreeSet<String> made with String.CASE_INSENSITIVE_ORDER. Add Java, java, Arrays and JAVA, and print the set. Which spelling survives, and why? Then write your own comparator class, IgnoreCase, that does the same job with compareToIgnoreCase, and check that it gives the same set.
3
A cinema keeps its free seat numbers in a TreeSet<Integer>. A customer asks for seat 14, which is taken. Use floor and ceiling to offer the closest free seat, choosing the lower one when two are equally close, and print a message when no seat is free at all.
4
Write enum Suit { CLUBS, DIAMONDS, HEARTS, SPADES } and a record Card(int rank, Suit suit) whose natural order is by rank and then by suit. Enums are Comparable already: their natural order is the order the constants are declared in. Sort a hand of five cards and print it.
5
Make a TreeSet<String> of words that ignores case when it orders them, yet still keeps apple and Apple as two separate words. Explain in one sentence why String.CASE_INSENSITIVE_ORDER alone is not enough.

Further reading#

Spotted a mistake, or a part that did not make sense? Report it by email