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.
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.
- thenComparing
- comparing
- 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:
- If t1 < t2, a negative number represents less than
- If t1 == t2, 0 represents equal objects
- 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?
- If the year, month and the day of t1 is smaller than t2 then return -1 as t1 < t2.
- If the year, month and the day of t1 is equal to t2 then return 0 as t1 == t2
- 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.