Apache Commons ComparatorUtils

Apache Commons ComparatorUtils

The org.apache.commons.collections4.comparators package in the Apache Commons Collections provides various helpful comparators. The Apache Commons ComparatorUtils class provides static utility methods for creating and using those comparators. This post covers all the utility methods in the ComparatorUtils class and also covers the FixedOrderComparator class.

ComparatorUtils min and max

The ComparatorUtils#min method takes two values and a Comparator and returns the smaller of the two passed values (ordering determined by the comparator). It will return the second object if the passed values are equal. The method signature is shown below.

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

Similarly, the ComparatorUtils#max method takes two values along with a Comparator and returns the larger of the two passed values.

In the below example code, we pass two integer values to the ComparatorUtils.min method with a comparator. In the first call, we pass the Comparator.naturalOrder so the elements will be ordered as per the integer’s natural ordering (ascending order). Hence, the result (min) will be 4.

But in the second call, we pass Comparator.reverseOrder and hence the ordering will be in descending order (and hence the result is 5).

System.out.println(ComparatorUtils.min(5, 4, Comparator.naturalOrder())); //4
System.out.println(ComparatorUtils.min(5, 4, Comparator.reverseOrder())); //5

We get a reverse result if we call ComparatorUtils.max for the same set of arguments. In the first call, considering the natural ordering, 5 will be the max element. In the second call, with Comparator.reverseOrder, the ordering will be 5 followed by 4. Hence, in this ordering 4 is the larger element.

System.out.println(ComparatorUtils.max(5, 4, Comparator.naturalOrder())); //5
System.out.println(ComparatorUtils.max(5, 4, Comparator.reverseOrder())); //4

Let us look at an example with a record class. We have a record which implements Comparable as shown below. It enforces ordering among Process instances based on the id field alone. Hence, it considers two Process objects equal if they have the same process id.

private record Process(int id, String name) implements Comparable<Process> {

        @Override
        public String toString() {
            return "Process{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }

        @Override
        public int compareTo(Process o) {
            return Integer.compare(id, o.id);
        }
}

We have two Process instances and we pass them to the min() and the max() methods with a comparator ordering by the natural order.

Process p1 = new Process(1, "Intellij IDE");
Process p2 = new Process(2, "App Store");

System.out.println(ComparatorUtils.min(p1, p2, Comparator.naturalOrder())); //Process{id=1, name='Intellij IDE'}
System.out.println(ComparatorUtils.max(p1, p2, Comparator.naturalOrder())); // Process{id=2, name='App Store'}

Let us now implement a custom comparator to order based on the process name. With this, the ordering will be based on the name of the process and hence the min and max are reversed in this example.

Comparator<Process> processComparator = Comparator.comparing(Process::name);
System.out.println(ComparatorUtils.min(p1, p2, processComparator)); //Process{id=2, name='App Store'}
System.out.println(ComparatorUtils.max(p1, p2, processComparator)); //Process{id=1, name='Intellij IDE'}

ComparatorUtils – booleanComparator

The ComparatorUtils#booleanComparator method takes a boolean as an argument and returns a Comparator<Boolean>. The boolean parameter is called trueFirst, and it specifies whether true or false is sorted first. In other words, if we pass true, then the returned comparator will sort true booleans before false booleans.

Shown below is the example of creating a True-first and False-first boolean comparators and sorting a List<Boolean>.

Comparator<Boolean> trueFirstComparator = ComparatorUtils.booleanComparator(true);
List<Boolean> booleans = new ArrayList<>(List.of(true, false, true, false));
booleans.sort(trueFirstComparator);
System.out.println(booleans); //[true, true, false, false]

Comparator<Boolean> falseFirstComparator = ComparatorUtils.booleanComparator(false);
booleans.sort(falseFirstComparator);
System.out.println(booleans); //[false, false, true, true]

ComparatorUtils – nullLow and nullHigh comparator

The nullLowComparator and nullHighComparator take a comparator and return a comparator which can handle null values when comparing, i.e., it adapts the passed comparator to handle null values.

The comparator returned by nullLowComparator will consider null to be less than any non-null value. Similarly, the comparator returned by nullHighComparator will consider null to be greater than any non-null value. It uses the passed comparator to compare non-null values.

The list shown below has null values in it. We want to sort the list in natural order using Comparator.naturalOrder. Since it cannot handle null values, we use ComparatorUtils.nullHighComparator and it will treat nulls greater than any other value.

List<Integer> list = new ArrayList<>(Arrays.asList(1, 4, null, 5, 2, 3));
Comparator<Integer> nullHighComparator = ComparatorUtils.nullHighComparator(
        Comparator.<Integer>naturalOrder()
);
list.sort(nullHighComparator);
System.out.println(list); //[1, 2, 3, 4, 5, null]

An example using Comparator.reverseOrder as the underlying comparator is shown below.

Comparator<Integer> nullHighComparator = ComparatorUtils.nullHighComparator(
        Comparator.<Integer>reverseOrder()
);
list.sort(nullHighComparator);
System.out.println(list); //[5, 4, 3, 2, 1, null]

Shown below are the examples of using ComparatorUtils.nullLowComparator using Comparator#naturalOrder and Comparator#reverseOrder.

Comparator<Integer> nullLowComparator = ComparatorUtils.nullLowComparator(
        Comparator.<Integer>naturalOrder()
);
list.sort(nullLowComparator);
System.out.println(list); //[null, 1, 2, 3, 4, 5]

nullLowComparator = ComparatorUtils.nullLowComparator(
        Comparator.<Integer>reverseOrder()
);
list.sort(nullLowComparator);
System.out.println(list); //[null, 5, 4, 3, 2, 1]

ComparableUtils – transformedComparator

The transformedComparator takes a comparator and a Transformer instance and returns a comparator which uses the passed transformer to transform objects before passing them to the given comparator.

A Transformer is a functional interface with the below shown signature.

@FunctionalInterface
public interface Transformer<I, O> {

    O transform(I input);

}

Let us say we have the Process record and a List<Process> as shown below. We also have a Comparator<String> which compares strings using their natural order.

record Process(int id, String name) {

 }

List<Process> processes = new ArrayList<>(List.of(
        new Process(1, "Intellij IDE"),
        new Process(2, "App Store"),
        new Process(3, "TextEdit")
));
Comparator<String> stringComparator = Comparator.naturalOrder();

If we want to use this comparator to sort the process list, it is not possible. We can use ComparatorUtils.transformedComparator by passing a Transformer to convert (or map) each process instance to its name and then use the stringComparator. The returned comparator will order the Processes in the list by using the stringComparator on the process name.

Transformer<Process, String> processTransformer = p -> p.name;
Comparator<Process> comparator = ComparatorUtils.transformedComparator(
        stringComparator,  processTransformer);
processes.sort(comparator);
System.out.println(processes);

This prints,

[Process[id=2, name=App Store], Process[id=1, name=Intellij IDE], Process[id=3, name=TextEdit]]

ComparableUtils – naturalComparator and reversedComparator

The naturalComparator returns a comparator which orders objects using their natural order. The reversedComparator takes a comparator as an argument and returns a comparator that reverses the order of the given comparator.

System.out.println(ComparatorUtils.min(5, 4, ComparatorUtils.naturalComparator())); //4

System.out.println(ComparatorUtils.min(5, 4,
                ComparatorUtils.reversedComparator(
                        ComparatorUtils.<Integer>naturalComparator()))); //5

However, rather than using these methods, we can use Comparator.naturalOrder() and Comparator.reverseOrder() from java.util.Comparator.

ComparableUtils – chainedComparator

There are two overloaded chainedComparator methods – one takes a Collection<Comparator<E>> and the other takes a var-args of Comparator<E>.

Both chains the passed list of comparators and the returned comparator applies each comparator in the same order until one returns not equal or the list is exhausted. Note: This is equivalent to using Comparator#thenComparing.

Here, we have a List<String> and two comparators (compareByLength and naturalOrderComparator). The first compares strings by their length and the second as per their natural ordering. We can then chain these two comparators using the chainedComparator method. The resultant comparator (Comparator<String>) first tries to order by the string length. If the string lengths are the same, it orders them by their natural order.

List<String> list = new ArrayList<>(
        List.of("bb", "aa", "ccc", "d")
);
Comparator<String> compareByLength = Comparator.comparingInt(String::length);
Comparator<String> naturalOrderComparator = Comparator.naturalOrder();

Comparator<String> comparator = ComparatorUtils.chainedComparator(compareByLength, naturalOrderComparator);
list.sort(comparator);
System.out.println(list); //[d, aa, bb, ccc]

In the above example, the string “d” will be the first in the sorted list. Since the strings “bb” and “aa” are of the same length, they will be sorted as per the natural order.

FixedOrderComparator

Let us now look at the FixedOrderComparator. There is no utility method in the ComparatorUtils to use it, but we can directly use it from the org.apache.commons.collections4.comparators namespace.

The FixedOrderComparator imposes a specific order on a specific set of objects. First, we setup the FixedOrderComparator with a list of objects. Then, when we compare two objects, it uses the passed list to compare the objects (as per their position in the given list).

FixedOrderComparator – Setup and Sorting

There are three overloaded constructors

  1. No-argument constructor
  2. Constructor taking a List<T>
  3. Constructor taking a var-args (of type T)

If we use the no-arg constructor, we can add items using the add() method.

In the below example, we have a list of planets. Using that list, we create a FixedOrderComparator (planetsComparator).

List<String> planets = List.of(
        "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"
);
FixedOrderComparator<String> planetsComparator = new FixedOrderComparator<>(planets);

We have a partial list of planets (“Earth”, “Neptune”, and “Mercury”) and we have to sort them as per the ordering imposed by the planets list. To achieve this, we can use the planetsComparator. The result would be that the planets in the partial list be sorted as per their position in the planets list.

List<String> planetsToSort = new ArrayList<>(List.of("Earth", "Neptune", "Mercury"));
planetsToSort.sort(planetsComparator);
System.out.println(planetsToSort); //[Mercury, Earth, Neptune]

We can add elements into the FixedOrderComparator using the add method as shown below.

List<String> planets = List.of(
        "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn"
);
FixedOrderComparator<String> planetsComparator = new FixedOrderComparator<>(planets);
planetsComparator.add("Uranus");
planetsComparator.add("Neptune");

List<String> planetsToSort = new ArrayList<>(List.of("Earth", "Neptune", "Mercury"));
planetsToSort.sort(planetsComparator);
System.out.println(planetsToSort);  //[Mercury, Earth, Neptune]

FixedOrderComparator locking

The FixedOrderComparator instance will be in an unlocked state initially on creation and we can keep adding elements to it. When we use it to compare two objects, it will become locked. Once locked, we cannot make any changes to the FixedOrderComparator, i.e., we cannot add any new elements to it.

The isLocked() method returns true if the FixedOrderComparator has been locked; and false otherwise.

As we can see, after the sorting has been done, the FixedOrderComparator becomes locked.

List<String> planets = List.of(
        "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"
);
FixedOrderComparator<String> planetsComparator = new FixedOrderComparator<>(planets);

System.out.println("Is Locked: " + planetsComparator.isLocked());

List<String> planetsToSort = new ArrayList<>(List.of("Earth", "Neptune", "Mercury"));
planetsToSort.sort(planetsComparator);
System.out.println(planetsToSort);
System.out.println("Is Locked: " + planetsComparator.isLocked());

If we attempt to add new elements to a locked FixedOrderComparator, then it will throw an UnsupportedOperationException.

//throws java.lang.UnsupportedOperationException: Cannot modify a FixedOrderComparator after a comparison
planetsComparator.add("Some-value");

FixedOrderComparator – UnknownObjectBehavior

The UnknownObjectBehavior determines how the FixedOrderComparator should handle ordering of elements which aren’t present in the passed list. For example, in the above example, passing “Pluto” (or any other string not present in the planets list) when comparing would trigger this case.

The UnknownObjectBehavior is an enum with three possible values.

public enum UnknownObjectBehavior {
    BEFORE, AFTER, EXCEPTION;
}

UnknownObjectBehavior.BEFORE means to consider the unknown object before any of the other objects. In other words, the unknown object will be less than any of the known objects.

UnknownObjectBehavior.AFTER means to consider the unknown object after any of the other objects. In other words, the unknown object will be greater than any of the known objects.

UnknownObjectBehavior.EXCEPTION would throw an IllegalArgumentException when it encounters an unknown object.

The default value of UnknownObjectBehavior is EXCEPTION. But we can change that using the setUnknownObjectBehavior method.

FixedOrderComparator – setUnknownObjectBehavior

Let us attempt to sort a partial list of planets where we have an invalid object (”UnknownPlanet”) in the list (whereas it is not present in the planets list).

As explained above, the default behaviour is to throw an IllegalArgumentException.

List<String> planets = List.of(
          "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"
  );
FixedOrderComparator<String> planetsComparator = new FixedOrderComparator<>(planets);
List<String> planetsToSort = new ArrayList<>(List.of("Earth", "UnknownPlanet"));
planetsToSort.sort(planetsComparator);

Running this, we get,

java.lang.IllegalArgumentException: Attempting to compare unknown object UnknownPlanet

Let us set the UnknownObjectBehavior as BEFORE. This will place the “UnknownPlanet” at the beginning of the sorted list.

List<String> planets = List.of(
          "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"
);
FixedOrderComparator<String> planetsComparator = new FixedOrderComparator<>(planets);
planetsComparator.setUnknownObjectBehavior(FixedOrderComparator.UnknownObjectBehavior.BEFORE);

List<String> planetsToSort = new ArrayList<>(List.of("Earth", "SomeValue"));
planetsToSort.sort(planetsComparator);
System.out.println(planetsToSort); //[UnknownPlanet, Earth]

Similarly, setting the UnknownObjectBehavior as AFTER will place the “UnknownPlanet” at the end of the sorted list.

List<String> planets = List.of(
          "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"
);
FixedOrderComparator<String> planetsComparator = new FixedOrderComparator<>(planets);
planetsComparator.setUnknownObjectBehavior(FixedOrderComparator.UnknownObjectBehavior.AFTER);

List<String> planetsToSort = new ArrayList<>(List.of("Earth", "SomeValue"));
planetsToSort.sort(planetsComparator);
System.out.println(planetsToSort); //[Earth, UnknownPlanet]

The getUnknownObjectBehavior returns the currently set value for the UnknownObjectBehavior.

System.out.println(planetsComparator.getUnknownObjectBehavior()); //BEFORE

Note that we cannot change the UnknownObjectBehavior once the FixedOrderComparator is locked.

FixedOrderComparator – addAsEqual

The final method in the FixedOrderComparator is the addAsEqual. It takes two values – the first should be an existing object (known to the FixedOrderComparator) and adds a new item, which compares as equal to the existing item (the first argument).

In the below example, we have a list with three elements and we create a FixedOrderComparator from it. Then we add a new string “A” which is equal to the existing element “a”. Thus, it will consider “a” and “A” as equal.

List<String> orderList = List.of("a", "b", "c");
FixedOrderComparator<String> fixedOrderComparator = new FixedOrderComparator<>(orderList);
fixedOrderComparator.addAsEqual("a", "A");

List<String> list = new ArrayList<>(List.of("b", "a", "A"));
list.sort(fixedOrderComparator);
System.out.println(list); //[a, A, b]

list = new ArrayList<>(List.of("b", "A", "a"));
list.sort(fixedOrderComparator);
System.out.println(list); //[A, a, b]

Since the strings “a” and “A” are equal, when both are present, it will maintain the original ordering as seen above.

Note: If the original orderList had been List.of("a", "A", "b", "c"), then the sorted result would have been [a, A, b] in both the cases as a < A as per the ordering provided by the orderList.

Conclusion

This concludes the post on the Apache Commons ComparatorUtils class. We looked at all the static utility methods in the ComparatorUtils class. We also looked at the FixedOrderComparator which is really helpful to sort (or compare) by using a pre-established order of elements.

Leave a Reply