Introduction

The Comparator interface is a comparison function that imposes a total ordering on a collection of objects. We can use a Comparator to sort a list of objects. We can use it on data structures like SortedSet or a SortedMap to determine the order in which objects will be stored in such structures. From Java 8+, the Comparator has several static utility methods we can use. We will explore them and see how we can use them.

Comparator

A Comparator is a FunctionalInterface that has a single abstract method compare. It’s signature is

int compare(T o1, T o2)

It takes two arguments and determines the ordering between them.

  • If o1​ is lesser than o2, then a negative number must be returned.
  • If o1 is greater than o2, then a positive number must be returned.
  • Else, return 0.

Example Data

Let us say we have the following data model representing an Inventory.

import java.util.HashMap;
import java.util.Map;

public class Inventory {
    private long inventoryId;
    private Location location;
    private Map<String, Integer> items;

    public Inventory(Long inventoryId, Location location, Map<String, Integer> items) {
        this.inventoryId = inventoryId;
        this.location = location;
        this.items = items;
    }

    public Long getInventoryId() {
        return inventoryId;
    }

    public Location getLocation() {
        return location;
    }

    public Map<String, Integer> getItems() {
        return new HashMap<>(items);
    }

    @Override
    public String toString() {
        return "InventoryId: " + inventoryId + " " +
                "Location: " + location + " " +
                "Items: " + items;
    }
}
public class Location {
    private String state;

    public Location(String state) {
        this.state = state;
    }

    public String getState() {
        return state;
    }

    @Override
    public String toString() {
        return state;
    }
}

An inventory has an unique id, a location and the items in the inventory represented by a map. The key of the map is the item name, and the value is the quantity of that item in the inventory. The location is an object that just has the name of the state (place).

Let us build a list of inventories.

List<Inventory> inventories = new ArrayList<>();
Map<String, Integer> items = new HashMap<>();
items.put("Coke", 20);
items.put("Toothpaste", 10);
inventories.add(new Inventory(20L, new Location("Oregon"), items));

items = new HashMap<>();
items.put("Mattress", 125);
inventories.add(new Inventory(10L, new Location("Ohio"), items));

items = new HashMap<>();
items.put("Cheese", 24);
items.put("Milk", 21);
inventories.add(new Inventory(15L, new Location("Alaska"), items));

Sorting using Comparator

If we have to sort the above Inventory list using a Comparator, the usual(old) way of doing it would be to create a Comparator anonymous class for comparing two Inventory instances. Let us first, sort the list by the inventory id.

inventories.sort(new Comparator<Inventory>() {
  @Override
   public int compare(Inventory o1, Inventory o2) {
       return Long.compare(o1.getInventoryId(), o2.getInventoryId());
   }
 });
inventories.forEach(System.out::println);

Outputs:

InventoryId: 10 Location: Ohio Items: {Mattress=125}
InventoryId: 15 Location: Alaska Items: {Cheese=24, Milk=21}
InventoryId: 20 Location: Oregon Items: {Toothpaste=10, Coke=20}

A slight improvement is to use a Lambda expression to represent the Comparator as in

 inventories.sort((i1, i2) -> Long.compare(i1.getInventoryId(), i2.getInventoryId()));

Comparator comparing

We can do the same using Comparator.comparing.

inventories.sort(Comparator.comparing(Inventory::getInventoryId));

The comparing method takes a function that maps the object to a Comparable. This function is known as the key extractor. It then returns a Comparator, that compares objects using the passed key extractor.

So, for the above example, during sorting, to compare two Inventory objects, the returned Comparator from the comparing method first maps the Inventory object to a Long. And since Long is a Comparable, it compares the two Long values to determine the ordering of the Inventory instances.

Summary: This might look difficult to grasp initially, but think of it this way. When we write a Comparator, we usually write a lambda with two arguments (the type we want to compare). Then we determine which comes first (or which is greater). Using these static utility methods, we just focus on extracting or mapping the instance (Inventory here) to a new type for which ordering is already defined i.e., it is Comparable.

Comparator comparingLong

The key extractor we used above maps an Inventory instance to a Long (wrapper over primitive long). So, it involves some boxing overhead. To avoid this we can use specialized method comparingLong.

inventories.sort(Comparator.comparingLong(Inventory::getInventoryId));

The comparingLong function takes a ToLongFunction, that maps an object to a long. The returned comparator then uses Long.compare to compare the long values. So, it has no boxing involved.

There are similar methods for working with int and double namely comparingInt and comparingDouble.

Comparator comparing - Passing a Comparator

What if the type mapped by the key extractor is not Comparable. Simple, we write a Comparable for that and pass it. Let us see this in action.

Let us now, sort the inventories by the Location i.e., we will sort the inventories by the name of the state (in chronological order).

inventories.sort(Comparator.comparing(Inventory::getLocation));

If you try this, you would have noticed that it does not compile!! But why?

The function we have passed, maps an Inventory to a Location object. But a Location object is not Comparable. So, we need to pass Comparator to compare two Location instances as a second argument to comparing. (A problem within a problem ;))

inventories.sort(Comparator.comparing(Inventory::getLocation,
    (l1, l2) -> l1.getState().compareTo(l2.getState())));


Or, using a Lambda 

inventories.sort(Comparator.comparing(Inventory::getLocation, 
    Comparator.comparing(Location::getState)));

This returns a Comparator that compares two Inventory objects using its Location’s state. Since Strings are Comparable, it knows how to order them (natural ordering of String is chronological)

Running this produces (note they are sorted by the state name)

InventoryId: 15 Location: Alaska Items: {Cheese=24, Milk=21}
InventoryId: 10 Location: Ohio Items: {Mattress=125}
InventoryId: 20 Location: Oregon Items: {Toothpaste=10, Coke=20}

Note: To make the first failed code snippet to work we can make Location to implement Comparable. Its compareTo method would look like

public int compareTo(Location o) {
    return this.state.compareTo(o.state);
}

Comparator comparing reversed

To reverse the above produced ordering (sort by state name in reverse chronological order), we have the reversed method to do just that.

inventories.sort(Comparator.comparing(Inventory::getLocation,
        Comparator.comparing(Location::getState)
            .reversed()));

It produces the following output,

InventoryId: 20 Location: Oregon Items: {Toothpaste=10, Coke=20}
InventoryId: 10 Location: Ohio Items: {Mattress=125}
InventoryId: 15 Location: Alaska Items: {Cheese=24, Milk=21}

Comparator thenComparing

We can chain Comparators easily. The Comparators next in the chain will be used to break ties if the first comparator determines the two objects are equal.

Let us now sort the list by the number of items in an inventory. If two inventories have the same number of items, we sort by the inventory id (the tie breaker)

inventories.sort(Comparator.comparingInt((Inventory i) -> i.getItems().size())
        .thenComparing(Inventory::getInventoryId)); 

Produces,

InventoryId: 10 Location: Ohio Items: {Mattress=125}
InventoryId: 15 Location: Alaska Items: {Cheese=24, Milk=21}
InventoryId: 20 Location: Oregon Items: {Toothpaste=10, Coke=20}

If we have to use the Location name as the tie breaker, then we can plug in the Comparator we wrote earlier for the thenComparing.

inventories.sort(Comparator.comparingInt((Inventory i) -> i.getItems().size())
        .thenComparing(Inventory::getLocation, Comparator.comparing(Location::getState)));

Comparator thenComparingLong

Similar to comparingInt, comparingLong and comparingDouble that we use when working with primitives, there are thenComparing versions of them too (thenComparingLong, thenComparingInt and thenComparingDouble).

The first example in the above section can be written using thenComparingLong as

inventores.sort(Comparator.comparingInt((Inventory i) -> i.getItems().size())
        .thenComparingLong(Inventory::getInventoryId)); 

Summary

The static comparing methods in the Comparator interface helps us to write custom Comparator logic in order to compare or order two instances. They help us write it in a declarative way which makes our code more readable. It also reduces the probability of a introducing bug if we are to write it in pre-Java 8 style (imperative).