Implement the Comparator interface
The Comparator interface can be seen everywhere in the JDK library, from searching to sorting to reverse operations, and so on. In Java 8, it becomes a functional interface. The advantage of this is that we can use streaming syntax to implement the comparator.
Let's implement Comparator in several different ways to see the value of the new syntax. Your fingers will thank you. Not having to implement anonymous inner classes saves you a lot of keystrokes.
Sorting using Comparator
The following example will use different comparison methods to sort a group of people. Let's first create a Person JavaBean.
Copy the code code as follows:
public class Person {
private final String name;
private final int age;
public Person(final String theName, final int theAge) {
name = theName;
age = theAge;
}
public String getName() { return name; }
public int getAge() { return age; }
public int ageDifference(final Person other) {
return age - other.age;
}
public String toString() {
return String.format("%s - %d", name, age);
}
}
We can implement the Comparator interface through the Person class, but in this way we can only use one comparison method. We want to be able to compare different attributes - such as name, age, or a combination of these. In order to perform comparison flexibly, we can use Comparator to generate relevant code when we need to compare.
Let's first create a list of Persons, each with a different name and age.
Copy the code code as follows:
final List<Person> people = Arrays.asList(
new Person("John", 20),
new Person("Sara", 21),
new Person("Jane", 21),
new Person("Greg", 35));
We can sort people in ascending or descending order by their name or age. The general method is to use anonymous inner classes to implement the Comparator interface. If written this way, only the more relevant code is meaningful, and the rest is just a formality. Using lambda expressions can focus on the essence of comparison.
Let’s first sort them by age from youngest to oldest.
Now that we have a List object, we can use its sort() method to sort. However, this method also has its problems. This is a void method, which means that when we call this method, the list will change. To retain the original list, we must first make a copy and then call the sort() method. It was just too much effort. At this time we have to turn to the Stream class for help.
We can get a Stream object from the List and then call its sorted() method. It returns a sorted collection rather than modifying the original collection. Using this method, you can easily configure the parameters of Comparator.
Copy the code code as follows:
List<Person> ascendingAge =
people.stream()
.sorted((person1, person2) -> person1.ageDifference(person2))
.collect(toList());
printPeople("Sorted in ascending order by age: ", ascendingAge);
We first convert the list into a Stream object through the stream() method. Then call its sorted() method. This method accepts a Comparator parameter. Since Comparator is a functional interface, we can pass in a lambda expression. Finally we call the collect method and have it store the results in a list. The collect method is a reducer that can output the objects during the iteration process into a specific format or type. The toList() method is a static method of the Collectors class.
The abstract method compareTo() of Comparator receives two parameters, which are the objects to be compared, and returns a result of type int. To be compatible with this, our lambda expression also receives two parameters, two Person objects, whose types are automatically deduced by the compiler. We return an int type indicating whether the compared objects are equal.
Because we want to sort by age, we will compare the ages of the two objects and then return the result of the comparison. If they are the same size, return 0. Otherwise, a negative number is returned if the first person is younger, and a positive number is returned if the first person is older.
The sorted() method will traverse each element of the target collection and call the specified Comparator to determine the sort order of the elements. The execution method of the sorted() method is somewhat similar to the reduce() method mentioned earlier. The reduce() method gradually reduces the list to a result. The sorted() method sorts by the results of comparison.
Once we've sorted we want to print the results, so we call a printPeople() method; let's implement this method.
Copy the code code as follows:
public static void printPeople(
final String message, final List<Person> people) {
System.out.println(message);
people.forEach(System.out::println);
}
In this method, we first print a message, then traverse the list and print out each element in it.
Let's call the sorted() method to see how it will sort the people in the list from youngest to oldest.
Copy the code code as follows:
Sorted in ascending order by age:
John - 20
Sara - 21
Jane - 21
Greg - 35
Let's take another look at the sorted() method to make an improvement.
Copy the code code as follows:
.sorted((person1, person2) -> person1.ageDifference(person2))
In the lambda expression passed in, we simply route these two parameters - the first parameter is used as the call target of the ageDifference() method, and the second parameter is used as its parameter. But we can not write it like this, but use an office-space mode-that is, use method references and let the Java compiler do the routing.
The parameter routing used here is a little different from what we saw before. We saw earlier that arguments are either passed as call targets or as call parameters. Now, we have two parameters, and we want to split them into two parts, one as the target of the method call, and the second as the parameter. Don't worry, the Java compiler will tell you, "I'll take care of this."
We can replace the lambda expression in the previous sorted() method with a short and concise ageDifference method.
Copy the code code as follows:
people.stream()
.sorted(Person::ageDifference)
This code is very concise, thanks to the method references provided by the Java compiler. The compiler receives two person instance parameters and uses the first as the target of the ageDifference() method and the second as the method parameter. We let the compiler do this work instead of writing the code directly. When using this method, we must make sure that the first parameter is the calling target of the referenced method, and the remaining one is the input parameter of the method.
Reuse Comparator
It is easy to sort the people in the list from youngest to oldest, and it is also easy to sort from oldest to youngest. Let's give it a try.
Copy the code code as follows:
printPeople("Sorted in descending order by age: ",
people.stream()
.sorted((person1, person2) -> person2.ageDifference(person1))
.collect(toList()));
We call the sorted() method and pass in a lambda expression, which fits into the Comparator interface, just like in the previous example. The only difference is the implementation of this lambda expression - we have changed the order of the people to be compared. The results should be arranged from oldest to youngest in terms of their age. Let's take a look.
Copy the code code as follows:
Sorted in descending order by age:
Greg - 35
Sara - 21
Jane - 21
John - 20
It doesn't take much effort to just change the logic of comparison. But we cannot reconstruct this version into a method reference because the order of the parameters does not comply with the rules of parameter routing for method references; the first parameter is not used as the calling target of the method, but as a method parameter. There is a way to solve this problem that also reduces duplication of effort. Let’s see how to do it.
We have created two lambda expressions before: one is to sort by age from small to large, and the other is to sort from large to small. Doing so will lead to code redundancy and duplication, and violate the DRY principle. If we just want to adjust the sort order, JDK provides a reverse method, which has a special method modifier, default. We will discuss it in the default method on page 77. Here we first use the reversed() method to remove redundancy.
Copy the code code as follows:
Comparator<Person> compareAscending =
(person1, person2) -> person1.ageDifference(person2);
Comparator<Person> compareDescending = compareAscending.reversed();
We first created a Comparator, compareAscending, to sort people by age from youngest to oldest. In order to reverse the comparison order, instead of writing this code again, we only need to call the reversed() method of the first Comparator to obtain the second Comparator object. Under the hood of the reversed() method, it creates a comparator to reverse the order of the compared parameters. This shows that reversed is also a higher-order method - it creates and returns a function without side effects. We use these two comparators in the code.
Copy the code code as follows:
printPeople("Sorted in ascending order by age: ",
people.stream()
.sorted(compareAscending)
.collect(toList())
);
printPeople("Sorted in descending order by age: ",
people.stream()
.sorted(compareDescending)
.collect(toList())
);
It can be clearly seen from the code that these new features of Java8 have greatly reduced the redundancy and complexity of the code, but the benefits are far more than these. There are endless possibilities waiting for you to explore in the JDK.
We can already sort by age, and it's also easy to sort by name. Let's sort them lexicographically by name. Similarly, we only need to change the logic in the lambda expression.
Copy the code code as follows:
printPeople("Sorted in ascending order by name: ",
people.stream()
.sorted((person1, person2) ->
person1.getName().compareTo(person2.getName()))
.collect(toList()));
The output results will be sorted in lexicographic order by name.
Copy the code code as follows:
Sorted in ascending order by name:
Greg - 35
Jane - 21
John - 20
Sara - 21
So far, we've sorted either by age or by name. We can make the logic of lambda expressions smarter. For example, we can sort by age and name at the same time.
Let's pick the youngest person on the list. We can first sort by age from smallest to largest and then select the first one in the results. But that doesn't actually work. Stream has a min() method to achieve this. This method also accepts a Comparator, but returns the smallest object in the collection. Let's use it.
Copy the code code as follows:
people.stream()
.min(Person::ageDifference)
.ifPresent(youngest -> System.out.println("Youngest: " + youngest));
When calling the min() method, we used the ageDifference method reference. The min() method returns an Optinal object because the list may be empty and there may be more than one youngest person in it. Then we get the youngest person through Optinal's ifPrsend() method and print out his detailed information. Let’s take a look at the output.
Copy the code code as follows:
Youngest: John - 20
Exporting the oldest one is also very simple. Just pass this method reference to a max() method.
Copy the code code as follows:
people.stream()
.max(Person::ageDifference)
.ifPresent(eldest -> System.out.println("Eldest: " + eldest));
Let’s take a look at the name and age of the eldest.
Copy the code code as follows:
Eldest: Greg - 35
With lambda expressions and method references, the implementation of comparators becomes simpler and more convenient. JDK also introduces many convenient methods to the Compararor class, allowing us to compare more smoothly, as we will see below.
Multiple comparisons and streaming comparisons
Let's take a look at the convenient new methods provided by the Comparator interface and use them to compare multiple properties.
Let's continue using the example from the previous section. Sorting by name, this is what we wrote above:
Copy the code code as follows:
people.stream()
.sorted((person1, person2) ->
person1.getName().compareTo(person2.getName()));
Compared with the inner class writing method of the last century, this writing method is simply too simple. However, it can be made simpler if we use some functions in the Comparator class. Using these functions can allow us to express our purpose more smoothly. For example, if we want to sort by name, we can write:
Copy the code code as follows:
final Function<Person, String> byName = person -> person.getName();
people.stream()
.sorted(comparing(byName));
In this code we have imported the static method comparing() of the Comparator class. The comparing() method uses the passed lambda expression to generate a Comparator object. In other words, it is also a higher-order function that accepts a function as input parameter and returns another function. In addition to making the syntax more concise, such code can also better express the actual problem we want to solve.
With it, multiple comparisons can become smoother. For example, the following code comparing by name and age says it all:
Copy the code code as follows:
final Function<Person, Integer> byAge = person -> person.getAge();
final Function<Person, String> byTheirName = person -> person.getName();
printPeople("Sorted in ascending order by age and name: ",
people.stream()
.sorted(comparing(byAge).thenComparing(byTheirName))
.collect(toList()));
We first created two lambda expressions, one returns the age of the specified person, and the other returns his name. When calling the sorted() method, we combine these two expressions so that multiple attributes can be compared. The comparing() method creates and returns a Comparator based on age. We then call the thenComparing() method on the returned Comparator to create a combined comparator that compares age and name. The output below is the result of sorting first by age and then by name.
Copy the code code as follows:
Sorted in ascending order by age and name:
John - 20
Jane - 21
Sara - 21
Greg - 35
As you can see, the implementation of Comparator can be easily combined using lambda expressions and the new tool classes provided by the JDK. Let’s introduce Collectors below.