We’ll build an example to sort a list of people using a few different points of comparisons. Let’s first create the Person JavaBean.
compare/fpij/Person.java 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 could implement the Comparable interface on the Person class, but that’d limit us to one particular comparison. We would want to compare on different things—on name, age, or a combination of fields, for example. To get this flexibility, we’ll create the code for different comparisons just when we need them, with the help of the Comparator interface.
Let’s create a list of people to work with, folks with different names and ages.
compare/fpij/Compare.java
final List<Person> people = Arrays.asList(
new Person("John", 20), new Person("Sara", 21), new Person("Jane", 21), new Person("Greg", 35));
Implementing the Comparator Interface
•
45We could sort the people by their names or ages and in ascending or descending order. In the habitual way to achieve this we would implement the Comparator interface using anonymous inner classes. But the essence here is the code for the comparison logic, and anything else we write would be pure ceremony. We can boil this down to its essence using lambda expressions.
Let’s first sort the people in the list in ascending order by age.
Since we have a List, the obvious choice is the sort() method on the List. There are downsides to using this method, however. That’s a void method, which means the list will be mutated when we call it. To preserve the original list, we’d have to make a copy and then invoke the sort() method on the copy; that’s quite labor intensive. Instead we’ll seek the help of the Stream.
We can get a Stream from the List and conveniently call the sorted() method on it. Rather than messing with the given collection, it will return a sorted collec- tion. We can nicely configure the Comparator parameter when calling this method.
compare/fpij/Compare.java List<Person> ascendingAge =
people.stream()
.sorted((person1, person2) -> person1.ageDifference(person2)) .collect(toList());
printPeople("Sorted in ascending order by age: ", ascendingAge);
We first transformed the given List of people to a Stream using the stream() method.
We then invoked the sorted() method on it. This method takes a Comparator as its parameter. Since Comparator is a functional interface, we conveniently passed in a lambda expression. Finally we invoked the collect() method and asked it to put the result into a List. Recall that the collect() method is a reducer that will help to target the members of the transformed iteration into a desirable type or format. The toList() is a static method on the Collectors convenience class.
Comparator’s compareTo() abstract method takes two parameters, the objects to be compared, and returns an int result. To comply with this, our lambda expression takes two parameters, two instances of Person, with their types inferred by the Java compiler. We return an int indicating whether the objects are equal.
Since we want to sort by the age property, we compare the two given people’s ages and return the difference. If they’re the same age, our lambda expression will return a 0 to indicate they’re equal. Otherwise, it will indicate the first person is younger by returning a negative number or older by returning a positive number for the age difference.
The sorted() method will iterate over each element in the target collection (people in this example) and apply the given Comparator (a lambda expression in this case) to decide the logical ordering of the elements. The execution mechanism of sorted() is much like the reduce() method we saw earlier. The reduce() method trickles the list down to one value. The sorted() method, on the other hand, uses the result of the comparison to perform the ordering.
Once we sort the instances we want to print the values, so we invoke a con- venience method printPeople(); let’s write that method next.
compare/fpij/Compare.java
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 a message and iterate over the given collection, printing each of the instances.
Let’s call the sorted() method, and the people in the list will be printed in ascending order by age.
Sorted in ascending order by age:
John - 20 Sara - 21 Jane - 21 Greg - 35
Let’s revisit the call to the sorted() method and make one more improvement to it.
.sorted((person1, person2) -> person1.ageDifference(person2))
In the lambda expression we’re passing to the sorted() method, we’re simply routing the two parameters—the first parameter as the target to the ageDiffer- ence() method and the second as its argument. Rather than writing this code, we can use the office-space pattern—i.e., ask the Java compiler to do the routing again, using a method reference.
The parameter routing we want here is a bit different from the ones we saw earlier. So far we’ve seen a parameter being used as a target in one case and as an argument in another case. In the current situation, however, we have two parameters and we want those to be split, the first to be used as a target to the method and the second as an argument. No worries. The Java compiler gives us a friendly nod: “I can take care of that for you.”
Implementing the Comparator Interface
•
47Let’s replace the lambda expression in the previous call to the sorted() method with a short and sweet reference to the ageDifference() method.
people.stream()
.sorted(Person::ageDifference)
The code is fantastically concise, thanks to the method-reference convenience the Java compiler offers. The compiler took the parameters, the two person instances being compared, and made the first the ageDifference() method’s target and the second the parameter. Rather than explicitly connecting these, we let the compiler work a little extra for us. When using this conciseness, we must be careful to ensure that the first parameter is really the intended target of the method referenced and the remaining parameters are its arguments.