(Coursenotes for CSC 203 Project-based Object-oriented Programming and Design)

Comparators, lambdas, and method references

Overview

This lesson provides more information about Comparators in Java. While conceptually simple, Comparators are used here as a vehicle to introduce a number of new concepts and syntaxes that may be unfamiliar. Specifically, in this lesson we will:

The next lesson will dive into lambdas more deeply and functional interfaces more generally.

Recap

In the previous lesson, we learned about the Comparable and Comparator interfaces. To recap the main difference between the two:

We worked with an Album class that had the following fields. We’ll continue with the same example in this lesson.

(Assume that we have defined getter methods for all the instance variables above.)

There are many ways of defining Comparators in Java. We will explore them in this lesson, starting with the most basic (repeated from the previous lesson), and then consider some more convenient alternatives.

Defining a Comparator in a separate class

Recall that the Comparator interface defines one abstract method: public int compare(Album a1, Album a2).

The simplest way to create a Comparator for Album objects is to create a separate class that implements the Comparator interface.

Let’s consider a comparator that compares albums by their title.

// The Comparator interface only has one abstract method
public class AlbumTitleComparator implements Comparator<Album> {

  public int compare(Album a1, Album a2) {
    return a1.getTitle().compareTo(a2.getTitle());
  }
}

The Comparator above can be used in functions like Collections.sort like below. We create a new instance of the comparator object, and pass that object as a second parameter to the sort function.

List<Album> albums = ...; // Assume this list is populated

Comparator<Album> titleComp = new AlbumTitleComparator();
Collections.sort(albums, titleComp);

This version of the sort function will now use the specified comparator for the pairwise comparisons during the sorting process. As a result, the list of albums will be sorted by the title name in ascending order.

In essence, we have parameterised the comparison procedure in the sort function, thereby making the sort function more generally usable. Because we tell it what comparator to use, the same sort function can be used to sort objects using any ordering we define. We only need to define comparators and pass them to the function as parameters.

Lambdas

In the example above, we created a whole new class (AlbumTitleComparator) just to define a single function (the compare function). What’s more, we created a new instance of the comparator only to pass it to the sort function. We never called the compare function ourselves.

All this to say: all we really care about in that comparator class is the compare function. Can we define it separately, instead of wrapping it in a class and creating a new object to hold the function?

This is where the lambda syntax comes in. Lambdas have the following basic “anatomy”:

(<params>) -> <lambda body>;

A lambda is an anonymous function.

Since the lambda is a function, it has inputs (<params>) and it has a body (<lambda body>). But, since it’s anonymous, it doesn’t have a name. And this is okay! The writer of a lambda is usually not the one who calls the lambda, so we often don’t need to name the function.

However, we can store the lambda in a variable that has a name, or we can pass lambdas to other functions as parameters.

Writing a comparator using a lambda

With that in mind, let’s take a look at what a comparator looks like expressed as a lambda. We’ll start simple, by defining a comparator that only compares Albums by their title.

When we create Comparators, what we’re really trying to create is a compare function. Lambdas allow us to do that without going through the steps of creating a new class that implements the Comparator interface. The key thing is that our compiler still thinks of the resulting comparator as a Comparator object.

The code below defines a Comparator using the lambda syntax. This code is considerably more concise than the previous example — in particular, we don’t need to create a new class. This titleComp comparator can be used in the same way as the previous example.

Comparator<Album> titleComp = (a1, a2) -> a1.getTitle().compareTo(a2.getTitle());

Let’s break down the code above.

In most cases, lambdas are single-line functions that return values. If a lambda has only one line, and that line is an expression, there is no need to use the return keyword. The value of that expression is returned implicitly.

We can write same lambda in “long form”:

Comparator<Album> titleComp = (Album a1, Album a2) -> {
  return a1.getTitle().compareTo(a2.getTitle());
}

By using curly braces in the lambda, we can write functions that include multiple lines. This is sometimes necessary, e.g., if you need to write code that uses loops. At that point, the lambda starts to look like a “plain old function”, and you need to explicitly use the return keyword to return a value.

The lambdas above can be used in the same way we’ve already seen. For example, if we want to sort a list of albums by title:

List<Album> albums = ...; // Assume this list is populated

// Create the comparator using the lambda syntax
Comparator<Album> titleComp = (a1, a2) -> a1.getTitle().compareTo(a2.getTitle());

// Pass the comparator to the sort function
Collections.sort(albums, titleComp);

In the example above, the comparator is a value that is passed to the sort function as a parameter. You can write the function without first storing it in the titleComp variable.

Collections.sort(albums, (Album a1, Album a2) -> a1.getTitle().compareTo(a2.getTitle()));

This time we do need to specify the types of a1 and a2, because this time the compiler doesn’t have clues from which to infer the the types of those parameters.

More lambda examples

Compare albums by year (an int)

Here is an example comparator that would compare two Albums by year.

Comparator<Album> yearComp = (a1, a2) -> a1.getYear() - a2.getYear();

Why does the code above work as a year comparator?

Hint

The compare function needs to return a positive integer if a1’s year is greater than a2’s year; a negative number a1’s year is less than a2’s year; and 0 if they are equal. We don’t care what the actual returned values are, as long as their signs are correct. Simply subtracting the two years successfully computes such an integer.

Compare albums by price (a double)

If we need to compare Albums by price, which is declared as a double, we can’t simply compute the difference between the two prices. Why do you think this is?

Hint

The result of this difference will be a double, which does not match the required signature for the compare function.

So we could write the function in “long form”:

Comparator<Album> priceComp = (a1, a2) -> {
  if (a1.getPrice() > a2.getPrice()) {
    return 1; // or any positive integer
  } else if (a1.getPrice() < a2.getPrice()) {
    return -1; // or any negative integer
  } else {
    return 0;
  }
}

Alternatively, the boxed primitive types provide a handy static function meant to do just this. The above comparator can be written as:

Comparator<Album> priceComp = (a1, a2) -> Double.compare(a1.getPrice(), a2.getPrice());

The Double.compare function returns a positive number, negative number, or 0, as appropriate for its two given parameters. Similarly, other primitive types provide similar static functions, e.g., Long.compare, Float.compare, etc.

Method references / key extractors

Finally, we can use the method reference or key extractor syntax.

You already know what methods are in Java. You can call or invoke or apply methods on objects. For example, obj.someMethod() will call someMethod() on the obj object.

However, it is also possible to simply “refer to” instance methods in Java, without calling them. We do this using the method reference or “key extractor” syntax.

For example,

These are useful because sometimes you end up creating lambdas that do nothing but call an existing method on an object. In these cases, it’s easier and simpler to simply “point to” the method you want to call instead of creating a lambda that takes a parameter and calls an existing named method on that parameter.

Creating a comparator using a method reference

The Comparator interface provides a Comparator.comparing static method. The Comparator.comparing method takes a lambda OR a method reference as a parameter.

If you give it a lambda, you define the lambda to follow the compare function signature. You are more-or-less doing what we’ve already done above.

If you give it a method reference, you “point to” the method in the class that you want the comparator to use in its comparison, and it uses that method to create a compare function.

So here’s the third and final way in which we can create comparators:

Comparator<Album> titleComp = Comparator.comparing(Album::getTitle);

A few things to notice about the code above:

Chaining comparators

Remember default methods? The Comparator defines a bunch of really useful default methods. These methods are instance methods that exist on all Comparator objects (just like you’ve learned about default methods).

We know that the Comparator interface only defines one abstract method: compare.

thenComparing

The thenComparing function lets us combine multiple comparators to create “composed” comparators. For example, to deal with tie-breakers.

Suppose we want to compare Albums by artist name, and then for Albums with the same artist name, we want to compare them based on their title. We can use the thenComparing function to chain an artist comparator and a title comparator. The result will be a third comparator that is the combination of the previous two.

Like Comparator.comparing, thenComparing can take either a lambda or a method reference as a parameter, and returns a Comparator instance.

// Written step-by-step
Comparator<Album> artistComp = Comparator.comparing(Album::getArtist);
Comparator<Album> titleComp = Comparator.comparing(Album::getTitle);
Comparator<Album> artistTitleComp = artistComp.thenComparing(titleComp);

// Written in one statement
Comparator<Album> artistTitleComp = Comparator.comparing(Album::getArtist).thenComparing(Album::getTitle);

// Sort albums by artist name, and sort by title for albums by the same artist
Collections.sort(albums, artistTitleComp);

This is a concise and convenient way to quickly chain together multiple comparisons to create complex comparison criteria. Because thenComparing returns a Comparator, you can chain together several thenComparing calls.

reversed

Another handy default method on Comparators is the reversed method. It simply reverses the Comparator object on which it is called, and returns a new Comparator.

// Written step-by-step
Comparator<Album> titleComp = Comparator.comparing(Album::getTitle);
Comparator<Album> reversed = titleComp.reversed();

// Written in one statement
Comparator<Album> reversed = Comparator.comparing(Album::getTitle).reversed();

// Sort albums in DESCENDING ORDER of title
Collections.sort(albums, reversed);

Like thenComparing, reversed also returns a Comparator object. This means that calls to reversed and thenComparing can be chained together to create various comparison combinations.

How would you use method references, thenComparing, and reversed to sort Albums by year in DESCENDING order, sorting Albums within the same year by artist in ASCENDING order, followed by title in ASCENDING order?


  1. Recall that an expression is anything that evaluates to a value.