Complete Guide to Comparator in Apache Collections with examples.

1. Introduction

Welcome to the 4th article in the Comparators series. Below are three articles that we have already discussed :

  1. Complete Guide to Comparator in Java 8 with examples
  2. Complete Guide to Comparators in Spring Framework
  3. Complete Guide to Comparator in Google Guava

In this article we will discuss 10 different methods of Comparators class with examples. If you are using maven project then use below dependency in your project. I recommend you to write code as we go along.

<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-collections4</artifactId>
	<version>4.4</version>
</dependency>

2. Content

Below are 10 different methods that we will discuss.

public static <E extends Comparable<? super E>> 
			Comparator<E> naturalComparator()

public static <E> Comparator<E> reversedComparator(
			final Comparator<E> comparator)

public static Comparator<Boolean> booleanComparator(
			final boolean trueFirst)

public static <E> Comparator<E> nullLowComparator(
			Comparator<E> comparator)

public static <E> Comparator<E> nullHighComparator(
			Comparator<E> comparator)

public static <I, O> Comparator<I> transformedComparator(
			Comparator<O> comparator, 
			final Transformer<? super I, ? extends O> transformer)

public static <E> E min(
			final E o1, final E o2, Comparator<E> comparator)

public static <E> E max(
			final E o1, final E o2, Comparator<E> comparator)

public static <E> Comparator<E> chainedComparator(
			final Comparator<E>... comparators)

public static <E> Comparator<E> chainedComparator(
			final Collection<Comparator<E>> comparators)

3. public static <E extends Comparable<? super E>>
Comparator<E> naturalComparator()

This method works as Comparator.naturalOrder() method. The comparison of natural order is specified using Comparable interface. The object in the Collection that needs to be sorted must have implemented the Comparable interface otherwise you get compile time error.

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);

If we try to sort the List<Transaction> where the Transaction class doesn’t implement the Comparable interface then you get below compile time error.

The method sort(Comparator<? super Transaction>) in the type List<Transaction> is not applicable for the arguments (Comparator<Comparable<? super Comparable<? super E>>>)

Below is how you can refactor the code to Java 8 Comparator.naturalOrder().

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

// using Java 8
strings.sort(Comparator.naturalOrder());

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

Assert.assertEquals(output, strings);

4. public static <E> Comparator<E>
reversedComparator(final Comparator<E> comparator)

If a comparator is passed as null then reversedComparator will reverse the natural order. Remember natural order requires us to implement a Comparable interface for the class that needs to be passed in sort method. I would consider this as a design flaw in the library. Why? Because now if the null parameter is passed to reversedComparator and the object to be sorted doesn’t implement Comparable interface then we will get a run time error of ClassCastException. No idea why it was decided to use natural order comparison instead of throwing NullPointerException if the parameter was null.

Let us taken an example : 

The below code will fail with ClassCastException because the Transaction class doesn’t implement Comparable interface.

try {
	List<Transaction> txns = Transactions.getDataSet();

	txns.sort(ComparatorUtils.reversedComparator(null));

	Assert.fail();
	} catch (ClassCastException e) {

		Assert.assertTrue(true);

	}

Strings are Comparable because String class implements Comparable interface.

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

strings.sort(ComparatorUtils.reversedComparator(null));

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

Assert.assertEquals(output, strings);

String comparison based on String length.

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

strings.sort(ComparatorUtils.reversedComparator(
			(s1, s2) -> Integer.compare(s1.length(), s2.length())));

List<String> output = Arrays.asList("aaaa", "azz", "zzz", 
									"aa", "zz", "z");

Assert.assertEquals(output, strings);

Below is how you can refactor the code to Java 8 Comparator.reversed().

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

Comparator<String> lengthComp = 
		(s1, s2) -> Integer.compare(s1.length(), s2.length());

// using Java 8 
strings.sort(lengthComp.reversed());

List<String> output = Arrays.asList("aaaa", "azz", "zzz", 
									"aa", "zz", "z");

Assert.assertEquals(output, strings);

5. public static Comparator<Boolean>
booleanComparator(final boolean trueFirst)

booleanComparator() method returns a Comparator<Boolean> based on how to treat boolean values. The parameter to the method defined how we should the boolean values in collection.

If trueFirst = true, then the element set to true is considered greater than element set to false.
If trueFirst = false, then the element set to true is considered smaller than element set to false.

trueFirst = true

List<Boolean> booleanValues = Arrays.asList(false, false, true, 
											false, true, true);

boolean trueValuesFirst = true;

List<Boolean> collect = 
	booleanValues
		.stream()
		.sorted(ComparatorUtils.booleanComparator(trueValuesFirst))
		.collect(Collectors.toList());

List<Boolean> output = Arrays.asList(true, true, true, 
									false, false, false);

Assert.assertEquals(output, collect);

boolean trueValuesFirst = false;

List<Boolean> booleanValues = Arrays.asList(false, false, true, 
											false, true, true);

boolean trueValuesFirst = false;

List<Boolean> collect = 
	booleanValues
		.stream()
		.sorted(ComparatorUtils.booleanComparator(trueValuesFirst))
		.collect(Collectors.toList());

List<Boolean> output = Arrays.asList(false, false, false, 
									true, true, true);

Assert.assertEquals(output, collect);

6. public static <E> Comparator<E>
nullLowComparator(Comparator<E> comparator)

nullsLowComparator() method treats the null element in the collection lower or smaller as compared to non null elements. As a result of this, all the null elements will be pushed to the beginning of the collection.

We will use List<String> as input and provide a string length comparator. Below is the example :

List<String> strings = Arrays.asList("azz", null, "aa", "zzz", 
									null, null, "aaaa", "zz", "z");

strings.sort(ComparatorUtils.nullLowComparator(
		(s1, s2) -> Integer.compare(s1.length(), s2.length())));

List<String> output = Arrays.asList(null, null, null, "z", "aa", 
									"zz", "azz", "zzz", "aaaa");

Assert.assertEquals(output, strings);

Below is how you can refactor the code to Java 8 Comparator.nullsFirst().

List<String> strings = Arrays.asList("azz", null, "aa", "zzz", 
									null, null, "aaaa", "zz", "z");

strings.sort(Comparator.nullsFirst(
		(s1, s2) -> Integer.compare(s1.length(), s2.length())));

List<String> output = Arrays.asList(null, null, null, "z", "aa", 
									"zz", "azz", "zzz", "aaaa");

Assert.assertEquals(output, strings);

7. public static <E> Comparator<E>
nullHighComparator(Comparator<E> comparator)

nullHighComparator() method treats the null element in the collection higher or greater as compared to non null elements. As a result of this all the null elements will be pushed to the end of the collection.

We will use List<String> as input and provide a string length comparator. Below is the example :

We will use List<String> as input and provide a string length comparator. Below is the example :

List<String> strings = Arrays.asList("azz", null, "aa", "zzz", null, 
									null, "aaaa", "zz", "z");

strings.sort(ComparatorUtils.nullHighComparator(
		(s1, s2) -> Integer.compare(s1.length(), s2.length())));

List<String> output = Arrays.asList("z", "aa", "zz", "azz", "zzz", 
									"aaaa", null, null, null);

Assert.assertEquals(output, strings);

Below is how you can refactor the code to Java 8 Comparator.nullsLast().

List<String> strings = Arrays.asList("azz", null, "aa", "zzz", null, 
									null, "aaaa", "zz", "z");
strings.sort(Comparator.nullsLast(
		(s1, s2) -> Integer.compare(s1.length(), s2.length())));

List<String> output = Arrays.asList("z", "aa", "zz", "azz", "zzz", 
									"aaaa", null, null, null);

Assert.assertEquals(output, strings);

8. public static <I, O> Comparator<I>
transformedComparator(
            Comparator<O> comparator,
            final Transformer<? super I, ? extends O> transformer)

transformedComparator() is an interesting method. It accepts the Transformers object. The Transaction is similar to the Function interface in java.util.function package. The basic idea is the same for both the interfaces i.e. they accept an input of some type T and produce a result of type R. Both Transformer and Function interface are Functional Interface.

In our example we have List<Transaction> which needs to be sorted by date. So we need to provide a Comparator for date and also provide a logic to get a date from Transaction.

Comparator<LocalDate> comparator = LocalDate::compareTo;

Transformer<Transaction, LocalDate> transformer = Transaction::date;
List<Transaction> transactions = Transactions.getDataSet();

Comparator<Transaction> dateComp = 
	ComparatorUtils.transformedComparator(
		LocalDate::compareTo, 
		Transaction::date);

transactions.sort(dateComp);

Before sort
2020-06-23
2020-06-19
2020-06-23
2020-06-28

After sort
2020-06-19
2020-06-23
2020-06-23
2020-06-28

Below is how you can refactor the code to Java 8 Comparator.comparing (Function, Comparator).

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

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

transactions.sort(dateComp);

9. public static <E> E
min(final E o1, final E o2, Comparator<E> comparator)

min method returns the minimum of two elements. The minimum is decided based on Comparator.

Comparator<String> strLength = 
	(s1, s2) -> Integer.compare(s1.length(), s2.length());

String min = ComparatorUtils.min("Sansa", "Jon", strLength);

Assert.assertEquals("Jon", min);

10. public static <E> E
max(final E o1, final E o2, Comparator<E> comparator)

max method returns the maximum of two elements. The maximum is decided based on Comparator.

Comparator<String> strLength = 
	(s1, s2) -> Integer.compare(s1.length(), s2.length());

String min = ComparatorUtils.max("Sansa", "Jon", strLength);

Assert.assertEquals("Sansa", min);

11. public static <E> Comparator<E>
chainedComparator(final Comparator<E>… comparators)

chainedComparator returns a Comparator that compares using an array of Comparators which is applied in sequence if objects in comparison are the same.

Let us take an example with only one Comparator. Give List<Transaction> sort the List using date of transaction.

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

Comparator<Transaction> transactionDateComp = 
	(txn1, txn2) -> txn1.date().compareTo(txn2.date());

Comparator<Transaction> comp = 
	ComparatorUtils.chainedComparator(transactionDateComp);

transactions.sort(comp);
	
Before sort
2020-06-23,US
2020-06-19,US
2020-06-23,CA
2020-06-28,AU

After sort
2020-06-19,US
2020-06-23,US
2020-06-23,CA
2020-06-28,AU

The above output has two records that are same as per date. If so, we now want to compare them by country code. Now let us add one more comparator called country comparator. The country comparator will sort country by country code. So CA will take precedence over US.

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

Comparator<Transaction> transactionDateComp = 
	(txn1, txn2) -> txn1.date().compareTo(txn2.date());

Comparator<Transaction> transactionCountryComp = 
	(txn1, txn2) -> txn1.country().compareTo(txn2.country());

Comparator<Transaction> comp = 
	ComparatorUtils.chainedComparator(
		transactionDateComp, 
		transactionCountryComp);

transactions.sort(comp);
	
Before sort
2020-06-23,US
2020-06-19,US
2020-06-23,CA
2020-06-28,AU

After sort
2020-06-19,US
2020-06-23,CA
2020-06-23,US
2020-06-28,AU

12. public static <E> Comparator<E>
chainedComparator(final Collection<Comparator<E>> comparators)

This method is similar to the previous method. In this method we will wrap all Comparators in one Collection and provide a Collection as input.

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

Comparator<Transaction> transactionDateComp = 
	(txn1, txn2) -> txn1.date().compareTo(txn2.date());

Comparator<Transaction> transactionCountryComp = 
	(txn1, txn2) -> txn1.country().compareTo(txn2.country());

List<Comparator<Transaction>> comparators = 
	Arrays.asList(
		transactionDateComp, 
		transactionCountryComp);
		
Comparator<Transaction> comp = 
	ComparatorUtils.chainedComparator(comparators);

transactions.sort(comp);

Before sort
2020-06-23,US
2020-06-19,US
2020-06-23,CA
2020-06-28,AU
After sort
2020-06-19,US
2020-06-23,CA
2020-06-23,US
2020-06-28,AU

13. Conclusion

Apache Comparators are really useful but the methods throwing ClassCastException are to be used with caution. It is prudent to replace the Apache Comparators with Java Comparator wherever possible. If you are using Chained Comparators of Apache then you can refactor your code using Comparator interface. There are several comparators that can be chained using new API. Click here to learn more about Comparators introduced in Java 8.

Code can be found on Github. Click here.

Leave a Reply

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