192. Introducing stream comparators
Let’s assume that we have the following three lists (a list of numbers, a list of strings, and a list of Car):
List<Integer> nrs = new ArrayList<>();
List<String> strs = new ArrayList<>();
List<Car> cars = List.of(…);
public class Car {
private final String brand;
private final String fuel;
private final int horsepower;
…
}
Next, we want to sort these lists in a stream pipeline.
Sorting via natural order
Sorting via natural order is very simple. All we have to do is to call the built-in intermediate operation, sorted():
nrs.stream()
.sorted()
.forEach(System.out::println);
strs.stream()
.sorted()
.forEach(System.out::println);
If nrs contains 1, 6, 3, 8, 2, 3, and 0 then sorted() will produce 0, 1, 2, 3, 3, 6, and 8. So, for numbers, the natural order is the ascending order by value.If strs contains “book”, “old”, “new”, “quiz”, “around”, and “tick” then sorted() will produce “around”, “book”, “new”, “old”, “quiz”, and “tick”. So, for strings, the natural order is the alphabetical order.The same result can be obtained if we explicitly call the Integer#compareTo() and String#compareTo() via sorted(Comparator<? super T> comparator):
nrs.stream()
.sorted((n1, n2) -> n1.compareTo(n2))
.forEach(System.out::println);
strs.stream()
.sorted((s1, s2) -> s1.compareTo(s2))
.forEach(System.out::println);
Or, we can use the java.util.Comparator functional interface, as follows:
nrs.stream()
.sorted(Comparator.naturalOrder())
.forEach(System.out::println);
strs.stream()
.sorted(Comparator.naturalOrder())
.forEach(System.out::println);
All these three approaches return the same result.
Reversing the natural order
Reversing the natural order can be done via Comparator.reverseOrder() as follows:
nrs.stream()
.sorted(Comparator.reverseOrder())
.forEach(System.out::println);
strs.stream()
.sorted(Comparator.reverseOrder())
.forEach(System.out::println);
If nrs contains 1, 6, 3, 8, 2, 3, and 0 then sorted() will produce 8, 6, 3, 3, 2, 1, and 0. Reversing the natural order of numbers results in descending order by value.If strs contains “book”, “old”, “new”, “quiz”, “around”, and “tick” then sorted() will produce “tick”, “quiz”, “old”, “new”, “book”, and “around”. So, for strings, reversing the natural order results in reversing the alphabetical order.
Sorting and nulls
If nrs/strs contains null values as well then all the previous examples will throw a NullPointerException. But, java.util.Comparator exposes two methods that allow us to sort null values first (nullsFirst(Comparator<? super T> comparator)) or last (nullsLast(Comparator<? super T> comparator)). They can be used as in the following examples:
nrs.stream()
.sorted(Comparator.nullsFirst(Comparator.naturalOrder()))
.forEach(System.out::println);
nrs.stream()
.sorted(Comparator.nullsLast(Comparator.naturalOrder()))
.forEach(System.out::println);
nrs.stream()
.sorted(Comparator.nullsFirst(Comparator.reverseOrder()))
.forEach(System.out::println);
The third example sorts the null values first followed by the numbers in reverse order.
Writing custom comparators
Sometimes we need a custom comparator. For instance, if we want to sort strs ascending by the last character then we can write a custom comparator as follows:
strs.stream()
.sorted((s1, s2) ->
Character.compare(s1.charAt(s1.length() – 1),
s2.charAt(s2.length() – 1)))
.forEach(System.out::println);
If strs contains “book”, “old”, “new”, “quiz”, “around”, and “tick” then sorted() will produce “old”, “around”, “book”, “tick”, “new”, and “quiz”.But, custom comparators are typically used to sort our models. For instance, if we need to sort the cars list then we need to define a comparator. We cannot just say:
cars.stream()
.sorted()
.forEach(System.out::println);
This will not compile because there is no comparator for Car objects. An approach consists of implementing the Comparable interface and overriding the compareTo(Car c) method. For instance, if we want to sort cars ascending by horsepower then we start by implementing Comparable as follows:
public class Car implements Comparable<Car> {
…
@Override
public int compareTo(Car c) {
return this.getHorsepower() > c.getHorsepower()
? 1 : this.getHorsepower() < c.getHorsepower() ? -1 : 0;
}
}
Now, we can successfully write this:
cars.stream()
.sorted()
.forEach(System.out::println);
Alternatively, if we cannot alter the Car code, we can try to use one of the existing Comparator methods that allow us to push a function that contains the sort key and returns a Comparator that automatically compares by that key. Since horsepower is an integer, we can use comparingInt(ToIntFunction<? super T> keyExtractor) as follows:
cars.stream()
.sorted(Comparator.comparingInt(Car::getHorsepower))
.forEach(System.out::println);
Or, in reverse order:
cars.stream()
.sorted(Comparator.comparingInt(
Car::getHorsepower).reversed())
.forEach(System.out::println);
You may also be interested in comparingLong(ToLongFunction) and comparingDouble(ToDoubleFunction). The ToIntFunction, ToLongFunction, and ToDoubleFunction are specializations of the Function. In this context, we can say that comparingInt(),comparingLong(), and comparingDouble() are specializations of comparing() which comes in two flavors: comparing(Function<? super T,? extends U> keyExtractor), and comparing(Function<? super T,? extends U> keyExtractor, Comparator<? super U> keyComparator).Here is an example of using the second flavor of comparing() for sorting cars ascending by fuel type (natural order) with null values placed at the end:
cars.stream()
.sorted(Comparator.comparing(Car::getFuel,
Comparator.nullsLast(Comparator.naturalOrder())))
.forEach(System.out::println);
And, here is another example for sorting cars ascending by the last character of fuel type with null values placed at the end:
cars.stream()
.sorted(Comparator.comparing(Car::getFuel,
Comparator.nullsLast((s1, s2) ->
Character.compare(s1.charAt(s1.length() – 1),
s2.charAt(s2.length() – 1)))))
.forEach(System.out::println);
Done! In the next problem, we will sort a map.