Java functional programming of nine: Comparator

  • 2020-04-01 03:30:48
  • OfStack

Implement the Comparator interface

The Comparator interface is found everywhere in the JDK library, from lookup to sorting to inversion. In Java 8 it became a functional interface, and the advantage of this is that we can use streaming syntax to implement the comparator.

Let's implement Comparator in a few different ways to see the value of the new syntax. Your fingers will thank you for not having to implement anonymous inner classes so much less typing.

Sort using the Comparator

The following example USES a different comparison method to sort a group of people. Let's first create a JavaBean for Person.


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 then we can only use one way of comparing. We want to be able to compare different attributes -- like name, age, or combination of these. To be flexible, we can use Comparator to generate the relevant code when we need to compare.

Let's start by creating a list of persons, each with a different name and age.


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 by name or age in ascending or descending order. The common approach is to implement the Comparator interface using anonymous inner classes. Only the relevant code makes sense, and the rest is just a walk. Using lambda expressions, however, focuses on the nature of the comparison.

Let's start by ranking them by age.

Now that we have a List object, we can use its sort() method to sort it. But this method has its problems. This is a void method, which means that when we call this method, the list changes. To keep the original list, we have to make a copy and then call the sort() method. It's too much work. At this point we have to turn to the Stream class.

We can get a Stream object from the List and call its sorted() method. Instead of making changes to the original collection, it returns a sorted collection. Using this method allows you to easily configure the parameters of the Comparator.


List<Person> ascendingAge =
people.stream()
.sorted((person1, person2) -> person1.ageDifference(person2))
.collect(toList());
printPeople("Sorted in ascending order by age: ", ascendingAge);

We first turn the list into a stream object using the stream() method. Then I call its sorted() method. This method accepts a Comparator parameter. Since the Comparator is a functional interface, we can pass in a lambda expression. Finally we call the collect method to store the results in a list. The collect method is a reducer that outputs an object in an iteration into a particular format or type. The toList() method is a static method of the Collectors class.

The abstract method of Comparator, compareTo(), takes two arguments, the object to be compared, and returns an int. To be compatible with this, our lambda expression also takes two arguments, two Person objects, whose types are automatically derived by the compiler. We return an int to indicate whether the objects being compared are equal.

Since we are sorting by age, we compare the ages of the two objects and return the results of the comparison. If they are the same size, 0 is returned. Otherwise if the first person is younger it returns a negative number, and if the first person is older it returns a positive number.

The sorted() method walks through each element of the target collection and invokes the specified Comparator to determine the sorted order of the elements. The sorted() method executes a bit like the reduce() method described earlier. The reduce() method gradually reduces the list to a result. The sorted() method sorts by the results of the comparison.

Once we've sorted it out we want to print it out, so we call a printPeople() method; Now let's implement this method.


public static void printPeople(
final String message, final List<Person> people) {
System.out.println(message);
people.forEach(System.out::println);
}

In this method, we print out a message and then walk through the list, printing out each element in it.

So let's call our sorted() method, which lists the people from youngest to oldest.


Sorted in ascending order by age:
John - 20
Sara - 21
Jane - 21
Greg - 35

Let's look at our sorted() method again to make an improvement.


.sorted((person1, person2) -> person1.ageDifference(person2))

In this incoming lambda expression, we simply route the two arguments -- the first as the target of the ageDifference() method, and the second as its argument. But instead of writing this, we can use an office-space pattern -- 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 earlier. As we've seen before, either the argument is the target of the call, or the argument is the call parameter. Now, we have two arguments, and we want to split them into two parts, one as the target of the method call, and the second as the argument. 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 shorter, more concise ageDifference method.


people.stream()
.sorted(Person::ageDifference)

This code is very clean, thanks to the method references provided by the Java compiler. The compiler receives arguments for two person instances, using the first as the target for the ageDifference() method and the second as the method argument. Instead of writing the code ourselves, we let the compiler do the work. When using this approach, we must make sure that the first parameter is the target of the referenced method, and that the remaining parameter is the method's input.

Reuse the Comparator

It's easy to sort the list by age, but it's also easy to sort it by age. So let's try that.


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 that just fits into the Comparator interface, as in the previous example. The only difference is the implementation of this lambda expression -- we've changed the order of comparison. The results should be listed in order of age. So let's see.


Sorted in descending order by age:
Greg - 35
Sara - 21
Jane - 21
John - 20

Just changing the logic of the comparison doesn't take much effort. But we can't reconstitute this version of the method reference because the order of the parameters does not match the routing rules for the parameters referenced by the method; The first parameter is not used as a method invocation target, but as a method parameter. There is a way to solve this problem, and it can also reduce repetitive work. So let's see how to do that.

We've already created two lambda expressions: one sorted by age from small to large, and one sorted by age from large to small. Doing so results in code redundancy and duplication and breaks the DRY principle. If we just want to adjust the sort order, the JDK provides a reverse method with a special method modifier, default. We will be in the 77 - page default method to discuss it, here we use first this reversed () method to remove redundancy.


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 younger to older. In reverse order, rather than to write this code at a time, we only need to call the first Comparator reversed () method can obtain a second Comparator object. In the reversed () method of the ground floor, it creates a comparator, in exchange for the comparison of the order of the parameters. This shows reversed is also a high order method - it creates and returns a function without side effects. Let's use these two comparators in our code.

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's clear from the code that Java8's new features greatly reduce the redundancy and complexity of the code, but there's more to it than that, and there's plenty more to explore in the JDK.

Now that we can sort by age, it's easy to sort by name. Let's order them in lexicographical order by name, again, just change the logic in the lambda expression.


printPeople("Sorted in ascending order by name: ",
people.stream()
.sorted((person1, person2) ->
person1.getName().compareTo(person2.getName()))
.collect(toList()));

The output is arranged in lexicographical order.

Sorted in ascending order by name:
Greg - 35
Jane - 21
John - 20
Sara - 21

So far, we've either sorted by age or by name. We can make the logic of lambda expressions smarter. For example, we can sort by age and by name at the same time.

Let's pick the youngest person on the list. We can sort it by age and then select the first of the results. But you don't have to do that. Stream has a min() method to do that. This method also accepts a Comparator, but returns the smallest object in the collection. Let's use it.


people.stream()
.min(Person::ageDifference)
.ifPresent(youngest -> System.out.println("Youngest: " + youngest));

We used the ageDifference method reference when we called the min() method. 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 using Optinal's ifPrsend() method and print out his details. So let's look at the output.

Youngest: John - 20

Exporting the oldest is also easy. Just pass the method reference to a Max () method.


people.stream()
.max(Person::ageDifference)
.ifPresent(eldest -> System.out.println("Eldest: " + eldest));

Let's look at the name and age of the oldest one.

Eldest: Greg - 35

With lambda expressions and method references, the implementation of the comparator becomes simpler and more convenient. The JDK also introduced a number of handy methods to the Compararor class, making it easier to compare, as we'll see below.

Multiple comparison and streaming comparison

Let's look at what convenient new methods the Comparator interface provides and use them to compare multiple properties.

Let's continue with the example from the previous section. In order of names, here's what it says:


people.stream()
.sorted((person1, person2) ->
person1.getName().compareTo(person2.getName()));

Compared to the way inner classes were written in the last century, this is simply too neat. However, using some of the functions in the Comparator class makes it easier and allows us to express our purpose more smoothly. For example, to sort by name, we could say:


final Function<Person, String> byName = person -> person.getName();
people.stream()
.sorted(comparing(byName));

In this code we import 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's also a higher-order function that takes one function as an argument and returns another. In addition to making the syntax cleaner, this code also reads better to describe the actual problem we're trying to solve.

It also makes it easier to make multiple comparisons. For example, the following code, by name and age, tells the story:


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 that returned the age of the specified person and one that returned his name. When we call the sorted() method, we combine the two expressions so that we can compare multiple attributes. The compare () method creates and returns a Comparator by age, and we call the thencompare () method on the returned Comparator to create a combined Comparator that compares the age and name. The output below is sorted by age and then by name.


Sorted in ascending order by age and name:
John - 20
Jane - 21
Sara - 21
Greg - 35

As you can see, using lambda expressions and the new utility classes provided by the JDK, it is easy to combine the implementation of the Comparator. Let's talk about Collectors.


Related articles: