Complete Guide to Comparator in Java 8 with examples

1. Introduction

The Comparator<T> interface has existed in Java from early days since version 1.2. The aim of this interface is to impose the ordering of objects. You can use it to impose an ordering in a data structure like TreeMap<K, V> or PriorityQueue<E>, and also to impose ordering while sorting a data structure. For all practical purposes of this article, we will focus on the second aspect, i.e. usage of Comparator<T> while sorting the data structure. 

I would highly recommend that you experiment with all the methods and see how it works.

Let us dive in.

Using reversed() method imposes the reverse ordering on this Comparator.

2. Content

Prior to Java 8 Comparator<T> interface had two methods 

boolean equals(Object obj)

int compare(T o1, T o2)

As of Java 8, Comparator interface now has 16 additional methods. That is a major overhaul. All new methods are default or public static methods. In this article, we will look at all methods with examples. Below is a list of methods added in Java 8.

We can characterize these new methods in 3 different families.

  1. thenComparing
  2. comparing
  3. miscellaneous

3. int compare(T o1, T o2)

compare method is the place where we write an implementation of the comparison. The concrete implementation of this method defines the ordering in which we will sort the elements.

The return type int signifies the less than, equal to or greater than comparison. Two objects are to be compared, namely t1 and t2:

  1. If t1 < t2, a negative number represents less than
  2. If t1 == t2, 0 represents equal objects
  3. If t1 > t2, 1 represents greater than

Let us take an example. Give a List<Transaction> sort it using the transaction date. The transaction class has a date object whose type is java.time.LocalDate.

Comparator<Transaction> TIME_SORT = new Comparator<Transaction>() {
	@Override
	public int compare(Transaction t1, Transaction t2) {
		return t1.date().compareTo(t2.date());
	}
};

The above comparator will impose the ordering based on the date. How will the compare method work?

  1. If the year, month and the day of t1 is smaller than t2 then return -1 as t1 < t2.
  2. If the year, month and the day of t1 is equal to t2 then return 0 as t1 == t2
  3. If the year, month and the day of t1 is greater than t2 then return 1 as t1> t2.

The compare method is the heart and soul of the Comparator interface. Now you can sort the List<Transaction> using Collections class or using the sort method(default method added in Java 8) in List interface.

Sort using Collections class.

Collections.sort(transactions, TIME_SORT);

Sort using sort method in List interface.

transactions.sort(TIME_SORT);

We can refactor the anonymous class syntax to use lambda expression. Yes, the Comparator interface is a Functional Interface, as it has only one abstract method. The equals method is overridden from Object class.

Let us refactor it.

Comparator<Transaction> TIME_SORT = 
(Transaction t1, Transaction t2) -> t1.date().compareTo(t2.date());

Looks good, right? Clean, concise, elegant and no anonymous class syntax. Can we do better? Yes, we can. Right now, our lambda expression has declared a type of parameters, i.e. 

(Transaction t1, Transaction t2) -> t1.date().compareTo(t2.date());

We can refactor this to inferred type 

Comparator<Transaction> TIME_SORT 
        = (t1, t2) -> t1.date().compareTo(t2.date());

From now on in this article, we will use lambda expression or method references. No more anonymous class.

4. default Comparator<T> reversed()

Family: Miscellaneous

reversed method returns a Comparator that imposes reverse ordering of this Comparator. reversed is a default method that works on already created Comparator instances. 

Example: Give the List<Transaction> reverse sort the List based on the transaction date.

List<Transaction> transactions = Transactions.getDataSet();

Comparator<Transaction> timeComp 
        = (t1, t2) -> t1.date().compareTo(t2.date());

Comparator<Transaction> reverseTimeComp = timeComp.reversed();

transactions.sort(reverseTimeComp);
Output
Before sorting
2020-06-18, US
2020-06-14, US
2020-06-18, CA
2020-06-23, AU

After sorting
2020-06-23, AU
2020-06-18, US
2020-06-18, CA
2020-06-14, US


5. public static <T extends Comparable<? super T>>
Comparator<T> reverseOrder()

Family: Miscellaneous

The reverseOrder method imposes a reverse comparison as compared to naturalOrder() comparison. The comparison happens between two comparable objects. Hence, the objects in the List must be Comparable, i.e. must have implemented the Comparable interface. The method signatures explicitly ask that the object in the collection must have implemented the Comparable interface.

Internal code would look like this.

public int compare(Comparable<Object> c1, Comparable<Object> c2) {
	return c2.compareTo(c1);
}

Example : 

List<String> strings = Arrays.asList("aaa","aa","a","zzz","zz","z");

strings.sort(Comparator.reverseOrder());

// expected output
List<String> output = Arrays.asList("zzz","zz","z","aaa","aa","a");

Assert.assertEquals(output, strings);

6. public static <T extends Comparable<? super T>>
Comparator<T> naturalOrder()

Family: Miscellaneous

The natural order comparison invokes two objects comparison using natural order. The method signature forces us to implement Comparable interface to specify the natural order of object. If the objects in the Collection to be sorted do not implement Comparable, then you get compile time warning.

Internal code would look like this.

public int compare(Comparable<Object> c1, Comparable<Object> c2) {
	return c1.compareTo(c2);
}

Example : 

List<String> strings = Arrays.asList("aaa","aa","a","zzz","zz","z");

strings.sort(Comparator.naturalOrder());

// expected output
List<String> output = Arrays.asList("a","aa","aaa","z","zz","zzz");

Assert.assertEquals(output, strings);

7. public static <T> Comparator<T>
nullsFirst(Comparator<? super T> comparator)

Family: Miscellaneous

nullsFirst method is used to move all the null elements to the beginning of the Collection. As of the non-null elements, we provide the comparator in parameter of this method.

Example using lambda expression :

List<Integer> numbers = Arrays.asList(56, 73, null, 42, 3, 7, null);

numbers.sort(
          Comparator.nullsFirst((x, y) -> Integer.compare(x, y)));

// expected output
List<Integer> output = Arrays.asList(null, null, 3, 7, 42, 56, 73);

Assert.assertEquals(output, numbers);

Example using methods reference :

List<Integer> numbers = Arrays.asList(56, 73, null, 42, 3, 7, null);

numbers.sort(Comparator.nullsFirst(Integer::compare));

// expected output
List<Integer> output = Arrays.asList(null, null, 3, 7, 42, 56, 73);

Assert.assertEquals(output, numbers);

8. public static <T> Comparator<T>
nullsLast(Comparator<? super T> comparator)

Family: Miscellaneous

nullsLast method is used to move all the null elements to the end of the Collection. As of the non-null elements, we provide the comparator in parameter of this method.

Example using lambda expression : 

List<Integer> numbers = Arrays.asList(56, 73, null, 42, 3, 7, null);

numbers.sort(Comparator.nullsLast((x, y) -> Integer.compare(x, y)));

// expected output
List<Integer> output = Arrays.asList(3, 7, 42, 56, 73, null, null);

Assert.assertEquals(output, numbers);

Example using method reference : 

List<Integer> numbers = Arrays.asList(56, 73, null, 42, 3, 7, null);

numbers.sort(Comparator.nullsLast(Integer::compare));

// expected output
List<Integer> output = Arrays.asList(3, 7, 42, 56, 73, null, null);

Assert.assertEquals(output, numbers);

9. default Comparator<T> thenComparing(
Comparator<? super T> other)

Family: thenComparing

This method composes two comparators into one. When both the objects in the compare method are equal or compare(x, y) == 0 then only comparator specified in the parameter gets executed.

Example: Give the List<Transaction> sort all transactions by country in ascending order. If the country is the same, sort it by date.

List<Transaction> transactions = Transactions.getDataSet();

Comparator<Transaction> countryComp 
    = (t1, t2) -> t1.country().compareTo(t2.country());

Comparator<Transaction> timeComp 
    = (t1, t2) -> t1.date().compareTo(t2.date());

Comparator<Transaction> thenComparing 
    = countryComp.thenComparing(timeComp);

transactions.sort(thenComparing);
Before sorting
2020-06-18, US
2020-06-14, US
2020-06-18, CA
2020-06-23, AU

After sorting
2020-06-23, AU
2020-06-18, CA
2020-06-14, US
2020-06-18, US

Two countries in the above transaction were the same, so we sorted them by date. The interesting part about this is we can chain more comparators. 

Example: Give the List<Transaction> sort all transactions by country in ascending order. If the country is the same, reverse sort it by date.

List<Transaction> transactions = Transactions.getDataSet();

Comparator<Transaction> countryComp 
    = (t1, t2) -> t1.country().compareTo(t2.country());

Comparator<Transaction> timeComp 
    = (t1, t2) -> t1.date().compareTo(t2.date());

Comparator<Transaction> timeReverse = timeComp.reversed();

Comparator<Transaction> thenComparing 
    = countryComp.thenComparing(timeReverse);

transactions.sort(thenComparing);
Before sorting
2020-06-18, US
2020-06-14, US
2020-06-18, CA
2020-06-23, AU

After reverse date sorting
2020-06-23, AU
2020-06-18, CA
2020-06-18, US
2020-06-14, US

10. default <U> Comparator<T> thenComparing(
Function<? super T, ? extends U> keyExtractor,
Comparator<? super U> keyComparator)

Family: thenComparing

This method takes the previously discussed method, thenComparing(Comparator<? super T> other) to another level. 

You can apply comparison to different objects in class by providing that object in Function and its Comparator. 

Example: Give the List<Transaction> sort all transactions by country. If the countries are the same, then sort based on transaction amount. 

List<Transaction> transactions = Transactions.getDataSet();

Comparator<Transaction> countryComp 
    = (t1, t2) -> t1.country().compareTo(t2.country());

Comparator<BigDecimal> amountComp = BigDecimal::compareTo;

Comparator<Transaction> thenComparing 
    = countryComp.thenComparing(Transaction::amount, amountComp);

transactions.sort(thenComparing);
Before sorting
2020-06-18, US
2020-06-14, US
2020-06-18, CA
2020-06-23, AU

After sorting by country.
2020-06-23, 16.00, AU
2020-06-18, 11.00, CA
2020-06-18, 25.00, US
2020-06-14, 20.00, US

If countries are the same, sort by amount.
2020-06-23, 16.00, AU
2020-06-18, 11.00, CA
2020-06-14, 20.00, US
2020-06-18, 25.00, US

The difference between the thenComparing(Comparator) and this method is that you can provide a Comparator of any type that goes in the keyExtractor function. This makes the intent of writing the Comparator clear.

11. default <U extends Comparable<? super U>> Comparator<T> thenComparing(
Function<? super T, ? extends U> keyExtractor)

Family: thenComparing

This method is an interesting flavor of thenComparing method family. Till now we saw two different flavors of thenComparing methods which accept Comparator in parameter. 

This method is different. It just accepts the Function which gets the key to be compared if the previous comparison were equal. The interesting part here is the key that is extracted should be Comparable. Look at method signature. U is the key that is extracted and it should extend Comparable interface.

Example: Give the List<Transaction> sort all transactions by country. If the countries are the same, then sort based on date. 

List<Transaction> transactions = Transactions.getDataSet();

Comparator<Transaction> countryComp 
    = (t1, t2) -> t1.country().compareTo(t2.country());

Comparator<Transaction> thenComparing 
    = countryComp.thenComparing(Transaction::date);

transactions.sort(thenComparing);
Before sorting
2020-06-18, US
2020-06-14, US
2020-06-18, CA
2020-06-23, AU

After sorting by country.
2020-06-23, AU
2020-06-18, CA
2020-06-18, US
2020-06-14, US

If the countries are the same then sort by date.
2020-06-23, AU
2020-06-18, CA
2020-06-14, US
2020-06-18, US

Till now we saw methods that compare the reference types. Now we will see 3 methods that will compare based on primitive types, i.e. int, long and double.

12. default Comparator<T> thenComparingInt(
ToIntFunction<? super T> keyExtractor)

Family: thenComparing

thenComparingInt method is used to compare the 2 int values using Integer.compare method. It accepts the ToIntFunction, which gets the key to be compared if the previous comparison was equal. 

For this example, we will sort using the ISO country code’s numeric value. Refer this link to understand what numeric code means. https://en.wikipedia.org/wiki/ISO_3166-1_numeric

Example: Give the List<Transaction> sort the transactions by date. If the date of transactions are the same then sort by a numeric code of that country. 

List<Transaction> transactions = Transactions.getDataSet();

Comparator<Transaction> dateComp 
    = (t1, t2) -> t1.date().compareTo(t2.date());

Comparator<Transaction> thenComparingInt 
    = dateComp.thenComparingInt(txn -> txn.country().getNumeric());

transactions.sort(thenComparingInt);
Before sorting
2020-06-23, AU, 36
2020-06-14, US, 840
2020-06-18, US, 840
2020-06-18, CA, 124

After sorting by date
2020-06-14, US, 840
2020-06-18, US, 840
2020-06-18, CA, 124
2020-06-23, AU, 36

After sorting by country’s numeric code.
2020-06-14, US, 840
2020-06-18, CA, 124
2020-06-18, US, 840
2020-06-23, AU, 36

13. default Comparator<T> thenComparingLong(
ToLongFunction<? super T> keyExtractor)

Family: thenComparing

thenComparingLong method is used to compare the 2 int values using the Long.compare method. It accepts the ToLongFunction, which gets the key to be compared if the previous comparison was equal. 

Example: Give the List<Transaction> sort the transactions by date. If the date is the same, then sort by transaction id.  

List<Transaction> transactions = Transactions.getDataSet();

Comparator<Transaction> dateComp = 
    (t1, t2) -> t1.date().compareTo(t2.date());

Comparator<Transaction> thenComparingLong 
    = dateComp.thenComparingLong(Transaction::transactionId);

transactions.sort(thenComparingLong);
Before sorting
2020-06-18, US, 4696781218545847910
2020-06-14, US, 5466122532059410617
2020-06-18, CA, 3970992376907265446
2020-06-23, AU, 5824449955055703953

After sorting by date
2020-06-14, US, 5466122532059410617
2020-06-18, US, 4696781218545847910
2020-06-18, CA, 3970992376907265446
2020-06-23, AU, 5824449955055703953

After sorting by transaction id
2020-06-14, US, 5466122532059410617
2020-06-18, CA, 3970992376907265446
2020-06-18, US, 4696781218545847910
2020-06-23, AU, 5824449955055703953

14. default Comparator<T> thenComparingDouble(
ToDoubleFunction<? super T> keyExtractor)

Family: thenComparing

thenComparingDouble method is used to compare the 2 int values using Double.compare method. It accepts the ToDoubleFunction, which gets the key to be compared if the previous comparison was equal. 

Example: Give the List<Transaction> sort the transactions by date. If the date is the same then sort by raw money amount.  

List<Transaction> transactions = Transactions.getDataSet();

Comparator<Transaction> dateComp 
  = (t1, t2) -> t1.date().compareTo(t2.date());

Comparator<Transaction> thenComparingDouble 
  = dateComp.thenComparingDouble(txn -> txn.amount().doubleValue());

transactions.sort(thenComparingDouble);
Before sorting
2020-06-18, 25.00, US
2020-06-14, 20.00, US
2020-06-18, 11.00, CA
2020-06-23, 16.00, AU

After sorting by date
2020-06-14, 20.00, US
2020-06-18, 25.00, US
2020-06-18, 11.00, CA
2020-06-23, 16.00, AU

If the date is same then sort by money value
2020-06-14, 20.00, US
2020-06-18, 11.00, CA
2020-06-18, 25.00, US
2020-06-23, 16.00, AU

15. public static <T, U> Comparator<T> comparing(
             Function<? super T, ? extends U> keyExtractor,
             Comparator<? super U> keyComparator)

Family: comparing

The comparing method accepts two parameters. First parameter is a Function which extracts the sort key out of the input object. And the second parameter accepts the Comparator of the sort key. 

Let us take an example. Sort the List<Transaction> in ascending order by date.

First parameter is Function, so we get the value out of the Transaction object and then provide a Comparator for the date object whose type is LocalDate.

Example: Give the List<Transaction> sort the transaction by its date.

List<Transaction> transactions = Transactions.getDataSet();

Comparator<Transaction> comparing 
    = Comparator.comparing(Transaction::date, LocalDate::compareTo);

transactions.sort(comparing);
Before sort
2020-06-18, US
2020-06-14, US
2020-06-18, CA
2020-06-23, AU

After sort
2020-06-14, US
2020-06-18, US
2020-06-18, CA
2020-06-23, AU

You can also chain the Comparator like this.

List<Transaction> transactions = Transactions.getDataSet();

Comparator<Transaction> comparing 
    = Comparator.comparing(Transaction::date, LocalDate::compareTo)
                .reversed();

transactions.sort(comparing);
Before sort
2020-06-18, US
2020-06-14, US
2020-06-18, CA
2020-06-23, AU

After sort
2020-06-23, AU
2020-06-18, US
2020-06-18, CA
2020-06-14, US

16. public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor)

Family: comparing

The comparing method just accepts the Function interface as the parameter.

This method expects that the output object of the method implements the Comparable interface. The method signature makes it clear for us that U is the key that is extracted and it should implement the Comparable interface.

Example: Give the List<Transaction> sort it by transaction amount.

List<Transaction> transactions = Transactions.getDataSet();

Comparator<Transaction> comparing 
    = Comparator.comparing(Transaction::amount);

transactions.sort(comparing);
Before sort
2020-06-18, 25.00, US
2020-06-14, 20.00, US
2020-06-18, 11.00, CA
2020-06-23, 16.00, AU

After sorting by transaction amount
2020-06-18, 11.00, CA
2020-06-23, 16.00, AU
2020-06-14, 20.00, US
2020-06-18, 25.00, US

17. public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor)

Family: comparing

comparingInt method is like thenComparingInt method except that this method is static. It accepts the ToIntFunction interface, which accepts object type T and returns an int value.

Example: Given the List<Transaction> sort it using the country’s numeric code.

List<Transaction> transactions = Transactions.getDataSet();

Comparator<Transaction> comparingInt 
    = Comparator.comparingInt(txn -> txn.country().getNumeric());

transactions.sort(comparingInt);
Before sorting
2020-06-23, AU, 36
2020-06-18, US, 840
2020-06-18, CA, 124
2020-06-14, US, 840

After sorting
2020-06-23, AU, 36
2020-06-18, CA, 124
2020-06-18, US, 840
2020-06-14, US, 840

18. public static <T> Comparator<T> comparingLong(
ToLongFunction<? super T> keyExtractor)

Family: comparing

comparingLong method is like thenComparingLong method except that this method is static. It accepts the ToLongFunction interface which accepts object type T and returns a long value.

Example: Give the List<Transaction> sort it using the transaction id.

List<Transaction> transactions = Transactions.getDataSet();

Comparator<Transaction> comparingLong 
    = Comparator.comparingLong(Transaction::transactionId);

transactions.sort(comparingLong);
Before sorting
2020-06-18, US, 8041621644904455210
2020-06-14, US, 2895071731436452279
2020-06-18, CA, 3120238735274721276
2020-06-23, AU, 7585908016907279152

After sorting
2020-06-14, US, 2895071731436452279
2020-06-18, CA, 3120238735274721276
2020-06-23, AU, 7585908016907279152
2020-06-18, US, 8041621644904455210

19. public static<T> Comparator<T> comparingDouble(
ToDoubleFunction<? super T> keyExtractor)

Family : comparing

comparingDouble method is like thenComparingDouble method except that this method is static. It accepts the ToDoubleFunction interface, which accepts object type T and returns a double value.

Example: Given the List<Transaction> sort it using the transaction’s raw amount. Amount data type is BigDecimal, so we convert it to double using the doubleValue() method.

List<Transaction> transactions = Transactions.getDataSet();

Comparator<Transaction> comparingDouble 
    = Comparator.comparingDouble(txn -> txn.amount().doubleValue());

transactions.sort(comparingDouble);
Before sorting
2020-06-18, 25.00, US
2020-06-14, 20.00, US
2020-06-18, 11.00, CA
2020-06-23, 16.00, AU

After sorting
2020-06-18, 11.00, CA
2020-06-23, 16.00, AU
2020-06-14, 20.00, US
2020-06-18, 25.00, US

20. Conclusion

With all these utility methods in the Comparator interface, it is significantly easier to use Comparator. We can also chain the methods to implement different Comparators into a single one. 

Github link Here.

Leave a Reply

Your email address will not be published. Required fields are marked *