Layers of Logic

10.8

Comparable, Comparator, and Sorting

Three sections have deferred the same question. Java can sort Integers and Strings on its own, and it has no idea which of two Students comes first until you tell it.

Core20 min read5 exercises
01

Previously on

The same question has now been deferred three times.

Section 9.2 showed you compareTo returning a number instead of a boolean, and said the shape mattered. Section 10.6 needed TreeMap to know which key comes first. Section 10.7 needed PriorityQueue to know which element is most urgent.

This section answers it, and it is the last of Phase X.

02

The problem

Java sorts these without being told anything:

Collections.sort(List.of(3, 1, 2));                  // fine
Collections.sort(List.of("pear", "apple"));          // fine

Now try your own class:

class Student {
    int marks;
    String name;
}

List<Student> students = new ArrayList<>(...);
Collections.sort(students);      // ClassCastException at run time

The class compiles. The sort throws.

And Java is right to. Given two students, which comes first? The one with higher marks? The one whose name comes earlier alphabetically? Both, with marks as the tiebreaker?

Only you know. Java is not being unhelpful, it is refusing to guess.

The same refusal blocks a lot of things at once:

What you cannot doBecause it needs to compare
Collections.sort(list)put them in orderwhich comes first
new TreeSet<>(items)a sorted setwhere each one goes in the tree
new TreeMap<>() with your keya sorted mapsame
new PriorityQueue<>(items)a heapwhich one is smallest
03

The idea

Comparable: the class defines its own order

class Student implements Comparable<Student> {
    int marks;
    String name;

    @Override
    public int compareTo(Student other) {
        return Integer.compare(this.marks, other.marks);
    }
}

One method. It returns a number, not a boolean, and the number means:

Return valueMeaning
negativethis comes before otherthis is "smaller"
zerothey tieequal for ordering purposes
positivethis comes after otherthis is "larger"

Only the sign matters. Returning -1 and returning -5000 mean exactly the same thing.

Now everything works:

Collections.sort(students);          // [Rohit(85), Cipher(90), Aditya(95)]
new TreeSet<>(students);             // fine
new PriorityQueue<>(students);       // fine

This is the natural order of the class: the one order that is part of what the thing is.

Comparator: an order passed in from outside

One natural order is often not enough. Sometimes you want students by name, sometimes by marks, sometimes by marks and then by name.

A Comparator is a separate object that knows how to compare two things:

students.sort(Comparator.comparing(s -> s.name));                 // by name
students.sort(Comparator.comparingInt((Student s) -> s.marks));   // by marks
ComparableComparator
Where it livesinside the classoutside, passed in
How manyone, the natural orderas many as you like
The methodcompareTo(other)compare(a, b)
Argumentsone. this is the other side.two
Use it whenthere is one obvious orderseveral orders, or you do not own the class
04

Under the hood

Going deeper

Building comparators without writing one

Since Java 8, you almost never write a Comparator by hand. You build one:

Comparator.comparing(Student::getName)                     // by name
Comparator.comparingInt(Student::getMarks)                 // by marks, no boxing
Comparator.comparingInt(Student::getMarks).reversed()      // marks, highest first
Comparator.comparingInt(Student::getMarks)
          .thenComparing(Student::getName)                 // marks, then name as tiebreaker
Comparator.nullsFirst(Comparator.naturalOrder())           // nulls at the front

reversed, thenComparing and nullsFirst are default methods on Comparator, from Section 8.5. They could be added in Java 8 without breaking any existing comparator anywhere.

Comparator is also a functional interface: one abstract method, compare. So it can be written as a lambda, which is why s -> s.name works, and why Phase XI will feel familiar.

The subtraction trap

You will see this everywhere, and it is a bug:

public int compareTo(Student other) {
    return this.marks - other.marks;      // looks fine. Is not.
}

It gives the right sign most of the time, which is what makes it dangerous. Then the numbers get large:

int big   =  2_000_000_000;
int small = -2_000_000_000;

big - small;                  // -294967296      NEGATIVE
Integer.compare(big, small);  //  1              correct

The real answer is 4 billion, which does not fit in an int. It wraps around, exactly as in Section 2.2, and comes back negative.

Negative means “this one is smaller”. So your comparison now says the larger value is the smaller one, and your sort is silently wrong for a range of inputs your tests never covered.

Always use Integer.compare(a, b). One call, never overflows, and it says what it means.

The contract

Like equals and hashCode in Section 8.3, compareTo has rules, and breaking them fails quietly.

RuleWhat it means
Antisymmetricsign of a.compareTo(b)must be the opposite of b.compareTo(a)
Transitivea before b, b before cthen a must come before c
Consistentif a and b tiethey must compare the same way against every c
RecommendedcompareTo == 0should agree with equals

Break the first three and Collections.sort may throw:

java.lang.IllegalArgumentException: Comparison method violates its general contract!

That message is genuinely helpful, and it is not guaranteed. Java’s sort only notices on certain inputs, so a broken comparator can sort a small list without complaint and blow up in production on a bigger one.

What actually sorts

Collections.sort and List.sort use TimSort: a merge sort that spots runs of already sorted data and takes advantage of them.

It is stable, which matters more than it sounds. Stable means two elements that tie keep their original relative order. That is what makes this work:

students.sort(Comparator.comparing(Student::getName));    // by name first
students.sort(Comparator.comparingInt(Student::getMarks)); // then by marks
// students with equal marks are still in name order

Two simple sorts instead of one compound comparator, and it only works because the second sort does not disturb ties from the first.

The Collections utility class

From Section 10.3: Collection is the interface, Collections is the box of static helpers.

Collections.sort(list);
Collections.reverse(list);
Collections.shuffle(list);
Collections.max(list);                  // needs Comparable
Collections.min(list, comparator);      // or give it one
Collections.frequency(list, "Atlas");
Collections.unmodifiableList(list);
Collections.emptyList();

Collections.binarySearch is the one that checks for RandomAccess, the marker interface from Section 8.5. On an ArrayList it jumps by index. On a LinkedList it walks with an iterator, because repeated indexing would be the 806 millisecond disaster from Section 10.2.

05

What it costs

Comparable gives a class exactly one order, permanently, and it has to be the right one. Choose badly and every TreeSet and PriorityQueue using it inherits your choice.

It also couples the class to an ordering decision that may belong to the caller instead. A Student sorted by marks in one program and by name in another should probably not have a natural order at all.

The contract is not enforced. Break it and you may get a helpful exception, or you may get a wrong order, and which one depends on the data.

compareTo disagreeing with equals is legal and produces sets of different sizes for the same data, with nothing to point at.

And sorting is never free. n log n comparisons, each one a call into your code. A slow compareTo on a large list is a real cost.

What you get is that every ordered thing in Java works with your class. One method, and sort, TreeSet, TreeMap, PriorityQueue, max and binarySearch all understand your type.

06

Check yourself

Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.

  1. What is the difference between `Comparable` and `Comparator`, and how do you choose?

    Show the answer

    Comparable is implemented by the class. It defines one natural order, and the class carries it everywhere. compareTo(other) takes one argument, because this is the other side.

    Comparator is a separate object passed to a sort. You can have as many as you like, and the class does not have to know about them. compare(a, b) takes two arguments.

    Choose Comparable when there is one obvious order that is part of what the thing is: a date sorts by time, a version sorts by number.

    Choose Comparator for everything else, especially when there are several orders, or when you do not own the class.

  2. Why is `return this.marks - other.marks;` a bug, even though it usually works?

    Show the answer

    Because subtraction can overflow, and an overflowed int comes back with the wrong sign.

    Take 2,000,000,000 minus -2,000,000,000. The real answer is 4 billion, which does not fit in an int. It wraps around, exactly as in Section 2.2, and comes out as -294967296.

    Negative means "this one is smaller", so your comparison says the larger value is the smaller one. Your sort is now silently wrong, for a small range of inputs, and every test you wrote with small numbers passed.

    Use Integer.compare(a, b). It is one call, it never overflows, and it says what it means.

  3. You put objects in a `TreeSet` and get a `ClassCastException`. The class compiles fine. Why?

    Show the answer

    Because TreeSet keeps its elements sorted, so it must compare them, and your class never said how.

    The compiler cannot catch this one. TreeSet<E> accepts any type at all, because it also has to accept types you hand a Comparator for. So it only finds out at run time. The exception arrives on the very first add: an empty tree compares the key with itself, purely to check the type before it trusts it.

    TreeMap, PriorityQueue and list.sort(null) all fail the same way, at run time. Collections.sort and Collections.max do not. Those two declare <T extends Comparable<? super T>>, so passing a class with no order is a compile error. Whether you find out early or late depends on whether the method was able to demand an order in its signature.

    Two fixes: implement Comparable, or hand the collection a Comparator when you build it.

07

Exercises

Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.

5 exercises115 pointsabout 125 minutes

The Layers of Logic VS Code extension runs the checks for exercises marked checked. For a manual exercise, run the program and compare its behaviour with the stated requirements and sample output.
A

Four Things That Refuse to Work

Warm up·20 min·15 points

checkedex-10-8-a

Four operations, four run time failures, one missing method.

Try each one and note that the compiler was happy with all of them. That is worth pausing on: these are type errors in spirit, and Java cannot catch them until the code runs.

Then add one method and watch all four start working at once.

Finish with the timing question. TreeSet fails on the second element and not the first, and the reason tells you when a comparison actually happens.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class NoOrder {
    static List<Student> three()
    static String tryTreeSet()
    static String tryPriorityQueue()
    static String tryListSort()
    static boolean collectionsSortCompiles()
    static boolean collectionsMaxCompiles()
    static int treeSetSizeAfterFirstAdd()
    static List<String> sortedOnceComparable()
}

Keep the broken Student as it is and add a SortableStudent beside it. The three try methods return "ok" or "ClassCastException". The two compiles methods are you recording what you found: write the line, try to compile it, then answer true or false and comment the line out.

What your program must do

  • Try all five operations and record which fail to compile and which crash at run time
  • Look at the signatures of Collections.sort and TreeSet and explain why only one of them could demand an order
  • Show that the TreeSet fails on the very first add, not the second
  • Add a Comparable version and show sorting works
NoOrder.java
import java.util.*;

public class NoOrder {

    // No natural order. Leave it that way.
    static class Student {
        final int marks;
        final String name;
        Student(int m, String n) { marks = m; name = n; }
        @Override public String toString() { return name + "(" + marks + ")"; }
    }

    // TODO: a second class, SortableStudent, the same but Comparable by marks

    static List<Student> three() {
        return new ArrayList<>(List.of(
                new Student(95, "Aditya"), new Student(85, "Rohit"), new Student(90, "Cipher")));
    }

    // These three compile. Predict what they do at run time, then find out.
    static String tryTreeSet()       { return "ok"; }  // TODO: new TreeSet<>(three())
    static String tryPriorityQueue() { return "ok"; }  // TODO: new PriorityQueue<>(three())
    static String tryListSort()      { return "ok"; }  // TODO: three().sort(null)

    // These two do NOT compile. Write each line, read the error, comment it out,
    // and record your answer here.
    static boolean collectionsSortCompiles() { return true; }  // TODO
    static boolean collectionsMaxCompiles()  { return true; }  // TODO

    // Add ONE student to an empty TreeSet, catching the failure. How many got in?
    static int treeSetSizeAfterFirstAdd() {
        return 1; // TODO
    }

    // Sort your SortableStudent version. Return the names in order.
    static List<String> sortedOnceComparable() {
        return List.of(); // TODO
    }

    public static void main(String[] args) {
        // TODO: print everything and read the two groups side by side
    }
}
Hint 1
Collections.sort is declared <T extends Comparable<? super T>> void sort(List<T>). That bound is a demand, and the compiler enforces it.
Hint 2
TreeSet<E> cannot make the same demand. It also has to accept types you hand a Comparator for, and those types are not Comparable. So its type parameter is unbounded and the check has to wait until the program runs.
Hint 3almost the answer
The first add is not a free pass. An empty tree runs compare(key, key) against the key itself, purely to check the type before it trusts it. So nothing gets in and the size stays 0.
What this is really testing

Whether you can tell a failure the compiler caught from one it could not. Two of these four are compile errors and two are run time crashes, and the difference is written in the method signatures.

B

The Subtraction That Overflows

Real work·20 min·20 points

checkedex-10-8-b

A comparison that is right almost always, which is worse than one that is wrong every time.

Run all five pairs. The first three agree perfectly, and that is exactly why this bug passes review and passes tests.

Then look at the last two. The signs are opposite, which means the comparison is claiming the larger number is smaller.

Finish by sorting a real list both ways and showing one of the results is actually wrong. Seeing a wrong order come out of Collections.sort with no exception is the thing worth remembering.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Overflow {
    static int bySubtraction(int a, int b)
    static int byCompare(int a, int b)
    static boolean signsAgree(int a, int b)
    static List<Integer> sortWithSubtraction(List<Integer> values)
    static List<Integer> sortWithCompare(List<Integer> values)
}

signsAgree compares the SIGNS of the two answers, not the answers themselves. A comparator only ever promises a sign. Both sort methods copy first and leave the input alone.

What your program must do

  • Print both comparisons for all five pairs and mark where the signs disagree
  • Explain the overflow using what Section 2.2 said about the range of an int
  • Sort a real list with each comparator and show one result is wrong
  • State the rule you will follow
Overflow.java
import java.util.*;

public class Overflow {

    static int bySubtraction(int a, int b) { return a - b; }
    static int byCompare(int a, int b)     { return Integer.compare(a, b); }

    // Do the two answers have the same SIGN? That is all a comparator promises.
    static boolean signsAgree(int a, int b) {
        return true; // TODO
    }

    // Sort a copy with each comparator.
    static List<Integer> sortWithSubtraction(List<Integer> values) {
        return List.of(); // TODO
    }

    static List<Integer> sortWithCompare(List<Integer> values) {
        return List.of(); // TODO
    }

    public static void main(String[] args) {
        int[][] pairs = {
                {5, 3}, {3, 5}, {7, 7},
                {2_000_000_000, -2_000_000_000},
                {-2_000_000_000, 2_000_000_000},
        };
        // TODO: print both methods for each pair, and mark where the signs disagree
        // TODO: sort a list holding those extreme values with each one
    }
}
Hint 1
Integer.signum turns any number into -1, 0 or 1, which is exactly the part a comparator is allowed to care about.
Hint 2
An int holds up to about 2.1 billion. 2_000_000_000 - (-2_000_000_000) is 4 billion, which does not fit, so it wraps around to a negative number. The comparator now says the larger value is smaller.
Hint 3almost the answer
The rule: never subtract inside a comparator. Integer.compare and Double.compare exist for this, and they never do the arithmetic that overflows. The bug is invisible in testing because every small number gives the right answer.
What this is really testing

Whether you know why a - b is a bug in compareTo. It gives the right sign almost always, and the exception is exactly where it matters.

C

Build Comparators Without Writing One

Real work·25 min·25 points

checkedex-10-8-c

Five orderings, and you should not write a single compare method body.

The chaining is the skill. reversed, thenComparing and thenComparingInt are default methods on Comparator, added in Java 8 without breaking any existing comparator anywhere.

Use comparingInt for the number field and be able to say why. The difference is an object allocated on every single comparison, inside a sort that does thousands of them.

Finish by writing one of the five as an explicit anonymous class and putting the two versions side by side. That comparison is Phase XI arriving early.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Compose {
    static List<Unit> roster()
    static List<String> byName(List<Unit> units)
    static List<String> byReadinessLowestFirst(List<Unit> units)
    static List<String> byReadinessHighestFirst(List<Unit> units)
    static List<String> byBaseThenReadinessThenName(List<Unit> units)
    static List<String> byNameIgnoringCase(List<Unit> units)
}

Every method returns just the names, in order, and must leave the list it was given untouched. Write all five without a compare method body anywhere.

What your program must do

  • Produce all five orderings using Comparator factory and default methods
  • Use comparingInt rather than comparing for the readiness field, and say why
  • Show the three-level sort putting Atlas before Cipher
  • Write one of them as an explicit anonymous class and compare the code
Compose.java
import java.util.*;

public class Compose {

    record Unit(String name, int readiness, String base) { }

    static List<Unit> roster() {
        return new ArrayList<>(List.of(
                new Unit("Atlas",  88, "North"),
                new Unit("Beacon", 42, "South"),
                new Unit("Cipher", 88, "North"),
                new Unit("Drift",  61, "South")));
    }

    // All five return just the names, in order, and must not change the list given.
    // Do all five WITHOUT writing a compare method body.

    static List<String> byName(List<Unit> units) {
        return List.of(); // TODO
    }

    static List<String> byReadinessLowestFirst(List<Unit> units) {
        return List.of(); // TODO: comparingInt, not comparing
    }

    static List<String> byReadinessHighestFirst(List<Unit> units) {
        return List.of(); // TODO
    }

    static List<String> byBaseThenReadinessThenName(List<Unit> units) {
        return List.of(); // TODO: three levels
    }

    static List<String> byNameIgnoringCase(List<Unit> units) {
        return List.of(); // TODO
    }

    public static void main(String[] args) {
        // TODO: print all five
        // TODO: write one of them as an explicit anonymous class and compare the code
    }
}
Hint 1
Comparator.comparing(Unit::name) builds the whole comparator from the field you want. .reversed() turns it round and .thenComparing(...) adds a tie-break.
Hint 2
comparingInt keeps the value as an int. Plain comparing boxes it into an Integer on every single comparison, which is a lot of objects for a big sort.
Hint 3almost the answer
For case insensitive order, Comparator.comparing(Unit::name, String.CASE_INSENSITIVE_ORDER) takes a second comparator saying how to compare the extracted values.
What this is really testing

Whether you can compose comparators instead of hand writing compare methods. Five orders, and you should not write a single compare method body.

D

When compareTo Disagrees With equals

Hard·30 min·30 points

checkedex-10-8-d

Same two students, two sets, different sizes, no error.

Produce it, then say exactly which method each set consulted. One uses equals, the other uses compareTo, and your class made them disagree.

Then find the same inconsistency in BigDecimal. It is documented, it is deliberate, and it still catches people, which tells you this is a real trap rather than a beginner mistake.

Fix your class so the two agree. The fix is small, and the rule it enforces is the fourth line of the Comparable contract, the one that is only a recommendation.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Disagree {
    static List<Student> twoOnNinety()
    static int hashSetSize()
    static int treeSetSize()
    static int fixedHashSetSize()
    static int fixedTreeSetSize()
    static boolean bigDecimalEquals()
    static int bigDecimalCompare()
}

Leave Student broken and add a second class FixedStudent beside it. The two must both exist so the before and after can be compared in one run.

What your program must do

  • Show the HashSet and TreeSet holding different numbers of the same students
  • Say which method each set used to decide
  • Show that BigDecimal has the same split in the standard library
  • Add a fixed version where both sets agree, and say what you changed
Disagree.java
import java.util.*;
import java.math.BigDecimal;

public class Disagree {

    // compareTo looks at marks only. equals looks at marks AND name. They disagree.
    static class Student implements Comparable<Student> {
        final int marks;
        final String name;
        Student(int m, String n) { marks = m; name = n; }
        @Override public int compareTo(Student o) { return Integer.compare(marks, o.marks); }
        @Override public boolean equals(Object o) {
            return o instanceof Student s && s.marks == marks && s.name.equals(name);
        }
        @Override public int hashCode() { return Objects.hash(marks, name); }
        @Override public String toString() { return name + "(" + marks + ")"; }
    }

    // TODO: a second class FixedStudent, where compareTo and equals agree

    static List<Student> twoOnNinety() {
        return List.of(new Student(90, "Aditya"), new Student(90, "Rohit"));
    }

    static int hashSetSize() { return 0; }  // TODO
    static int treeSetSize() { return 0; }  // TODO

    static int fixedHashSetSize() { return 0; }  // TODO
    static int fixedTreeSetSize() { return 0; }  // TODO

    // The same split, in the standard library.
    static boolean bigDecimalEquals() { return true; }  // TODO: 1.0 equals 1.00 ?
    static int bigDecimalCompare()    { return -1; }    // TODO: 1.0 compareTo 1.00 ?

    public static void main(String[] args) {
        // TODO: print all six and read them together
    }
}
Hint 1
A HashSet asks hashCode and then equals. A TreeSet asks compareTo and never asks equals at all.
Hint 2
new BigDecimal("1.0").equals(new BigDecimal("1.00")) is false, because equals compares the scale too. compareTo returns 0, because the values are the same.
Hint 3almost the answer
The fix is a tie-break: when the marks are equal, compare the names. Then compareTo returns 0 in exactly the cases where equals returns true, and both sets agree.
What this is really testing

Whether you can produce two sets of different sizes from the same data. It is legal, it is in the standard library, and there is no error anywhere.

E

Registry Reports, Sorted Every Way

Real work·30 min·25 points·The Registry

checkedex-10-8-e

Four reports from one roster, and one design decision underneath them.

Decide the natural order first and write your reason as a comment. readiness changes every week, so ordering by it would mean a unit moves around inside a TreeSet whenever it is updated, which is a genuinely bad idea. id does not change.

Then produce the reports, and prove your natural order is the one a TreeSet picks up.

Finish with the stability demonstration. Sort by name, then by readiness, and confirm that ties are still in name order. That only works because Java’s sort never reorders equal elements, and it is a technique worth having.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class RegistryReports {
    static List<Unit> roster()
    static List<String> defaultOrder(List<Unit> roster)
    static List<String> byReadiness(List<Unit> roster)
    static List<String> byBaseThenReadiness(List<Unit> roster)
    static List<String> topThree(List<Unit> roster)
    static List<String> throughTreeSet(List<Unit> roster)
    static List<String> sortedTwiceForCompoundOrder(List<Unit> roster)
}

The natural order of a Unit must be by id, so the tests can check it. roster() holds Drift#104 61 South, Atlas#101 88 North, Cipher#103 88 North, Beacon#102 42 South, in that order, so the natural order is not the order it was built in. Every report returns names only and leaves the roster alone.

What your program must do

  • Choose id as the natural order and defend the choice in a comment
  • Produce five different reports from the same roster
  • Show a TreeSet using your natural order without being told anything
  • Demonstrate stability by sorting twice and getting the same compound order
RegistryReports.java
import java.util.*;

public class RegistryReports {

    static class Unit implements Comparable<Unit> {
        final String name;
        final int id;
        final double readiness;
        final String base;

        Unit(String n, int i, double r, String b) { name = n; id = i; readiness = r; base = b; }

        // What is the NATURAL order of a Unit? Decide, then defend it in a comment.
        // Which field is unique, never changes, and means the same to every reader?
        @Override public int compareTo(Unit o) { return 0; }  // TODO

        @Override public String toString() { return name + "#" + id; }
    }

    // Deliberately not in natural order, so you can see the sort do something.
    static List<Unit> roster() {
        return new ArrayList<>(List.of(
                new Unit("Drift",  104, 61.0, "South"),
                new Unit("Atlas",  101, 88.0, "North"),
                new Unit("Cipher", 103, 88.0, "North"),
                new Unit("Beacon", 102, 42.0, "South")));
    }

    static List<String> defaultOrder(List<Unit> roster)          { return List.of(); }  // TODO
    static List<String> byReadiness(List<Unit> roster)           { return List.of(); }  // TODO: highest first
    static List<String> byBaseThenReadiness(List<Unit> roster)   { return List.of(); }  // TODO
    static List<String> topThree(List<Unit> roster)              { return List.of(); }  // TODO
    static List<String> throughTreeSet(List<Unit> roster)        { return List.of(); }  // TODO

    // Sort by the WEAKER key first, then the stronger one. A stable sort keeps both.
    static List<String> sortedTwiceForCompoundOrder(List<Unit> roster) {
        return List.of(); // TODO
    }

    public static void main(String[] args) {
        // TODO: print all six reports from the same roster
    }
}
Hint 1
The natural order should be the field that is unique, never changes, and means the same to everyone. Readiness moves every day and two units can share a name. The id does neither.
Hint 2
For a compound order in two passes, sort by the weaker key first and the stronger one second. Sorting is stable, so the second sort leaves equal elements in the order the first one left them.
Hint 3almost the answer
That is why the two-pass version matches the single three-level comparator exactly. If sorting were not stable, the first pass would be thrown away and the two answers would differ.
What this is really testing

Whether you can decide what belongs as a natural order and what belongs as a comparator. It is a design choice, and putting the wrong one in the class affects every TreeSet that ever holds it.

08

After the credits

Phase X is finished, and one line in it was a signpost.

students.sort(Comparator.comparing(s -> s.name));

That s -> s.name is a lambda. Comparator has exactly one abstract method, which makes it a functional interface, which is the doorway you built by hand in Section 7.4 and named in Section 8.5.

You have now used lambdas three times in this phase without them being explained: in removeIf, in merge, and here.

Phase XI is where they stop being incidental. filter, map, reduce, collect. And the honest thing to say is that Phase XI teaches you almost no new concepts. It gives you syntax for something you already understand, and a library built entirely out of one-method interfaces.

Collection.stream() is one more default method, added the same way as removeIf and forEach, for the same reason.

Threads you opened in this section

Comparable will return in Phase XI. Functional Java