Introduction

The Apache Commons Lang ComparableUtils has utility methods for translating Comparable result into a boolean. We will learn about the Apache Commons Lang ComparableUtils in this post.

Before that, let us understand what a Comparable is in Java. If you are familiar with this, you can skip to the next section.

What is a Comparable?

A Comparable is an interface which imposes a total ordering on the objects the class that implements it. When a class implements the Comparable interface, it provides a natural ordering among the objects of that class.

The Comparable interface has a method called compareTo which must be implemented to determine the ordering between the current object and the object passed to the compareTo method. It returns an integer and hence it could be an int less than 0, equal to 0 or greater than 0. Based on the returned value, the following are determined:

  • Negative integer - This object is less than the specified (passed) object.
  • Zero integer - This object is equal to the specified object.
  • Positive integer - This object is greater than the specified object.

Comparable requirements

There are certain requirements that the implementor has to follow.

Condition 1 - Signum check

The sgn(x.compareTo(y)) must be equal to -sgn(y.compareTo(x)) where sgn is the mathematical signum function. It returns one of -1, 0 or 1 according to whether the value passed is negative, zero or positive.

What does the above check mean?

It means that, when comparing x and y,

  • If x is less than y, then y must be greater than x.
  • If x is equal to y, then y must be equal to x.
  • Finally, if x is greater than y, then y must be less than x.

It doesn’t matter the exact integer value the compareTo method returns (the magnitude of the result doesn’t matter).

For example, comparing numbers 5 and 10, like, 5.compareTo(10) (you cannot write like this, you need integer objects to call the compareTo method on them), can return say -4 (just an example), to indicate that 5 is lesser than 10. If we swap the order (10.compareTo(5)), then it can return say 5, to indicate that 10 is greater than 5.

The requirement that when the order of comparison is swapped, the signum must change sign is satisfied.

Condition 2 - Comparison must be transitive

The comparable check must hold good for transitive cases i.e., (x.compareTo(y) > 0 && y.compareTo(z) >0) implies x.compareTo(z) > 0.

If x is greater than y and y is greater than z, then x must be greater than z.

Condition 3 - Swapping equal values (as per compareTo)

We must ensure that, x.compareTo(y) == 0 implies that sgn(x.compareTo(z)) == sgn(y.compareTo(z)), for all z

When we have two equal values x and y, we must get the same result for the below comparisons:

  • comparing x with z
  • comparing y with z

This is because x and y are equal. Example: If comparing x and z returns a negative number, then comparing y and z (in that order) must also return a negative number. Again, the magnitude of the result doesn’t matter.

Comparable consistent with Equals

Finally, there is another requirement, but it is not mandatory to satisfy that. It is to make comparable consistent with equals i.e., (x.compareTo(y) == 0) == (x.equals(y)).

When x and y are equal (as per the equals method), then compareTo must return 0 - else the compareTo can return a negative or a positive result.

It is always recommended to not violate this requirement. Doing so would cause confusions when using the object in a TreeMap or a TreeSet. If a class violates this requirement, it is recommended to document this in the Javadoc.

Comparable implementations in Java

Couple of well known classes in Java implement the Comparable interface - String and Integer.

Strings have a natural ordering where they are ordered lexicographically (a < aa < b) and integers are ordered as per the values (their natural ordering).

System.out.println("ab".compareTo("cd")); //-2
System.out.println("ab".compareTo("ab")); // 0
System.out.println("cd".compareTo("ab")); // 2

The first check returns -2 (a negative integer). This denotes that the string ab is less than the string cd. Comparing the string ab to itself returns 0 (equal). The last check is the reverse of the first check - it thus returns a positive result.

Similarly, for a number:

Integer i1 = 5;
Integer i2 = 10;
System.out.println(i1.compareTo(i2)); //-1
System.out.println(i1.compareTo(i1)); // 0
System.out.println(i2.compareTo(i1)); // 1

Implementing comparable for a custom class

Let us implement the Comparable interface for an university Department class. A department has a name, number of courses offered and the number of staffs in it.

Let us use only the department name to provide a natural ordering among the departments.

public class Department implements Comparable<Department> {
    private String name;
    private int courses;
    private int staffs;

    public Department(String name, int courses, int staffs) {
        this.name = name;
        this.courses = courses;
        this.staffs = staffs;
    }

    public String getName() {
        return name;
    }

    public int getCourses() {
        return courses;
    }

    public int getStaffs() {
        return staffs;
    }

    @Override 
    public int compareTo(Department o) { 
        return name.compareTo(o.name); //compare the names
    }
   
    @Override
    public String toString() {
        return name;
    }
}

Since a string is already comparable, we just called the compareTo method of the string (name field of the Department).

Let us create a list of departments and create a TreeSet from it. The TreeSet will use the compareTo method of the Comparable Department objects to order them.

List<Department> departments = List.of(
        new Department("Science", 5, 20),
        new Department("Computer Science", 10, 30),
        new Department("Mathematics", 12, 20)
);
TreeSet orderedDepartments = new TreeSet<>(departments);
System.out.println(orderedDepartments); //[Computer Science, Mathematics, Science]

Note: The List.of is a factory method added to the Collections in Java 9. If you are using Java version less than 9, use Arrays.asList in its place.

Converting comparable result into a boolean

We have two objects implementing Comparable interface (of same type) in Java and want to convert the relationship/ordering between them as a Predicate (boolean). In other words, to know if a comparable is less than/greater than/equal to another comparable.

The Apache Commons Lang has an utility class called ComparableUtils we can use for this.

Importing Apache Commons Lang

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.11</version>
</dependency>

You can find the latest version of commons-lang3 from Maven.

ComparableUtils greater than and greater than or equal to

ComparableUtils has methods gt() and ge() to test for greater than and greater than or equal to respectively. These methods return a Predicate.

The object (Comparable) we pass to the gt() or the ge() method is the other or the second object with which to compare against. The object (Comparable again) to compare is passed as the argument to the Predicate.

Predicate<Integer> greaterThan = ComparableUtils.gt(20);
System.out.println(greaterThan.test(15)); //false
System.out.println(greaterThan.test(20)); //false
System.out.println(greaterThan.test(30)); //true

In the above code, we get a Predicate by calling ComparableUtils.gt by passing a value of 20 (an Integer). We make three tests (notice the order of comparison)

  1. Pass 15 - It checks if 15 is greater than 20, it returns false.
  2. Pass 20 - It checks if 20 is greater than 20, it returns false.
  3. Passing 30 - It checks if 30 is greater than 20, it returns true.

Similarly, using ComparableUtils.ge() is shown below:

Predicate<Integer> greaterThanOrEqualTo = ComparableUtils.ge(20);
System.out.println(greaterThanOrEqualTo.test(15)); //false
System.out.println(greaterThanOrEqualTo.test(20)); //true
System.out.println(greaterThanOrEqualTo.test(30)); //true
  • Checks if 15 >= 20 - results in false
  • Checks if 20 >= 20 - results in true
  • The last check is 30 >= 20 - results in true

ComparableUtils less than and less than or equal to

Similar to the gt() and the ge() methods, there are lt() and le() methods to check if the tested object (passed to the returned Predicate) is less than (or less than or equal) to the passed parameter to the lt() or the le() methods.

Predicate<Integer> lesserThan = ComparableUtils.lt(20);
System.out.println(lesserThan.test(15)); //true
System.out.println(lesserThan.test(20)); //false
System.out.println(lesserThan.test(30)); //false
System.out.println();

Predicate<Integer> lesserThanOrEqualTo = ComparableUtils.le(20);
System.out.println(lesserThanOrEqualTo.test(15)); //true
System.out.println(lesserThanOrEqualTo.test(20)); //true
System.out.println(lesserThanOrEqualTo.test(30)); //false

ComparableUtils between and betweenExclusive

The ComparableUtils.between accepts two comparable as arguments (say b and c) and returns a Predicate. It checks if the argument passed to the predicate (say a) is between b and c i.e., it checks if b <= a<= c or b >=a >=c.

Predicate<Integer> isBetween = ComparableUtils.between(15, 20);
System.out.println(isBetween.test(18)); //true
System.out.println(isBetween.test(25)); //false
System.out.println(isBetween.test(15)); //true
System.out.println(isBetween.test(20)); //true

The ComparableUtils.betweenExclusive is similar to the between method, but does not include the endpoints or extremes. It checks if b < a < c or b > a > c.

Predicate<Integer> isBetweenExclusive = ComparableUtils.betweenExclusive(15, 20);
System.out.println(isBetweenExclusive.test(18)); //true
System.out.println(isBetweenExclusive.test(25)); //false
System.out.println(isBetweenExclusive.test(15)); //false
System.out.println(isBetweenExclusive.test(20)); //false

Conclusion

We started with a brief, but a complete tour of what a Comparable is. Then we learnt about the utility methods from the Apache Commons Lang ComparableUtils class in this post.