Implementing the java.lang.Comparable Interface 2 – Object Comparison


A compareTo() method is seldom implemented to interoperate with objects of other classes. For example, this is the case for primitive wrapper classes and the String class. The calls to the compareTo() method in the last three statements below all result in a compile-time error.

Click here to view code image

Integer iRef = 10;
Double dRef = 3.14;
String str = “ten”;
StringBuilder sb = new StringBuilder(“ten”);
boolean b1 = iRef.compareTo(str) == 0 ;  // compareTo(Integer) not applicable to
                                         // arguments (String).
boolean b2 = dRef.compareTo(iRef) > 0;   // compareTo(Integer) not applicable to
                                         // arguments (Double).
boolean b3 = sb.compareTo(str) == 0; // compareTo(StringBuilder) not applicable to
                                     // arguments (String).

A straightforward implementation of the compareTo() method for version numbers is shown in Example 14.8. Note the specification of the implements clause in the class header. By parameterizing the Comparable<E> interface with the VersionNumber type, the class declaration explicitly excludes comparison with objects of other types. Only VersionNumbers can be compared.

Click here to view code image

public final class VersionNumber implements Comparable<VersionNumber> {
  …
  @Override public int compareTo(VersionNumber vno) {                // (5)
  …
  }
  …
}

The signature of the compareTo() method is compareTo(VersionNumber). In order to maintain backward compatibility with non-generic code, the compiler inserts the following bridge method with the signature compareTo(Object) into the class (ยง11.11, p. 615).

Click here to view code image

public int compareTo(Object obj) {   // NOT A GOOD IDEA TO RELY ON THIS METHOD!
  return this.compareTo((VersionNumber) obj);
}

In an implementation of the compareTo() method, the fields are compared with the most significant field first and the least significant field last. In the case of the version numbers, the release numbers are compared first, followed by the revision numbers, with the patch numbers being compared last. Note that the next lesser significant fields are only compared if the comparison of the previous higher significant fields yielded equality. Inequality between corresponding significant fields short-circuits the computation. If all significant fields are equal, a zero will be returned. This approach is shown in the implementation of the compareTo() method at (5) through (8) in Example 14.8.

Comparison of integer values in fields can be optimized. In the code for comparing the release numbers at (5) in Example 14.8, we have relied on implicit use of the Integer.compareTo() method called by the Integer.compare() method, and autoboxing of the int field values:

Click here to view code image

if (this.release != vno.release)
  return Integer.compare(this.release, vno.release);
// Next field comparison

The code above can be replaced by the following code for doing the comparison, which relies on the difference between int values:

Click here to view code image

int releaseDiff = release – vno.release;
if (releaseDiff != 0)
  return releaseDiff;
// Next field comparison

However, this code can break if the difference is a value not in the range of the int type.

Significant fields with non-boolean primitive values are normally compared using the relational operators < and >. For comparing significant fields denoting constituent objects, the main options are to either invoke the compareTo() method on them, or use a comparator.

A more compact and elegant implementation of the compareTo() method for version numbers is shown at (9a) in Example 14.8 that exclusively uses methods of the Comparator<E> interface (p. 769). The compareTo() method implementation below is an equivalent version of the compareTo() method at (9a) in Example 14.8, where the method chaining has been split up and the method references replaced with equivalent lambda expressions.

Click here to view code image

@Override public int compareTo(VersionNumber vno) {               // (9b)
  Comparator<VersionNumber> c1
      = Comparator.comparingInt(vn -> vn.getRelease());           // (10b)
  Comparator<VersionNumber> c2
      = c1.thenComparingInt(vn -> vn.getRevision());              // (11b)
  Comparator<VersionNumber> c3
      = c2.thenComparingInt(vn -> vn.getPatch());                 // (12b)
//return c3.compare(this, vno);                                   // (13b)
  return Objects.compare(this, vno, c3);                          // (13c)
}

The method implementations at (9a) and (9b) essentially use a conditional comparator that applies its constituent comparators conditionally in the order in which it is composed. The lambda expressions specified as arguments in the method calls above at (10b), (11b), and (12b) will extract the int value of a particular field in a version number when the expression is executed.

  • The call to the static method comparingInt() at (10b) returns a Comparator that compares the version numbers by the int value of their release field.
  • The call to the default method thenComparingInt() at (11b) on Comparator c1 returns a composed Comparator that first compares version numbers using Comparator c1, and if the comparison result is 0, compares them by the revision field.
  • Analogous to (11b), the call to the default method thenComparingInt() at (12b) on Comparator c2 returns a composed Comparator that first compares version numbers using Comparator c2, and if the comparison result is 0, compares them by the patch field.
  • It is the call to the functional method compare() at (13b) on the composed Comparator c3 that does the actual comparison on the current version number (this) and the argument object (vno) according to the natural ordering of the int values of their fields, and where the order of the individual field comparisons is given by the composed Comparator c3.

Equivalently, the return statement at (13b) can be written as (13c), where the convenience method Objects.compare() takes as arguments the current version number (this), the argument object (vno), and the composed Comparator c3. The Objects.compare() method calls the functional method compare() on the composed Comparator c3, passing it the current version number (this) and the argument object (vno).

What is different about this implementation of version numbers is that the class VersionNumber now overrides both the equals() and the hashCode() methods, and implements the compareTo() method of the parameterized Comparable<VersionNumber> interface. In addition, the compareTo() method is consistent with the equals() method. Following general class design principles, the class has been declared final so that it cannot be extended.

Example 14.9 Implications of Implementing the compareTo() Method

Click here to view code image

import static java.lang.System.out;
import java.util.*;
public class TestVersionNumber {
  public static void main(String[] args) {
    // Print name of version number class:
    out.println(VersionNumber.class);
    // Create an unsorted array of version numbers:
    VersionNumber[] versions =  new VersionNumber[] {                      // (1)
        new VersionNumber( 3,49, 1), new VersionNumber( 8,19,81),
        new VersionNumber( 2,48,28), new VersionNumber(10,23,78),
        new VersionNumber( 9, 1, 1)};
    out.println(“Unsorted array: ” + Arrays.toString(versions));
    // Create an unsorted list:
    List<VersionNumber> vnoList = Arrays.asList(versions);                 // (2)
    out.println(“Unsorted list:  ” + vnoList);
    // Create an unsorted map:
    Map<VersionNumber, Integer> versionStatistics = new HashMap<>();       // (3)
    versionStatistics.put(versions[0], 2000);
    versionStatistics.put(versions[1], 3000);
    versionStatistics.put(versions[2], 4000);
    versionStatistics.put(versions[3], 5000);
    versionStatistics.put(versions[4], 6000);
    out.println(“Unsorted map: ” + versionStatistics);
    // Sorted set:
    Set<VersionNumber> sortedSet = new TreeSet<>(vnoList);                 // (4)
    out.println(“Sorted set: ” + sortedSet);
    // Sorted map:
    Map<VersionNumber, Integer> sortedMap = new TreeMap<>(versionStatistics);//(5)
    out.println(“Sorted map: ” + sortedMap);
    // Sorted list:
    Collections.sort(vnoList);                                             // (6)
    out.println(“Sorted list:    ” + vnoList);
    // Searching in sorted list:
    VersionNumber searchKey = new VersionNumber( 9, 1, 1);                 // (7)
    int resultIndex = Collections.binarySearch(vnoList, searchKey);        // (8)
    out.printf(“Binary search in sorted list found key %s at index: %d%n”,
                searchKey, resultIndex);
    // Sorted array:
    Arrays.sort(versions);                                                 // (9)
    out.println(“Sorted array:   ” + Arrays.toString(versions));
    // Searching in sorted array:
    int resultIndex2 = Arrays.binarySearch(versions, searchKey);           // (10)
    out.printf(“Binary search in sorted array found key %s at index: %d%n”,
                searchKey, resultIndex2);
  }
}

Leave a Reply

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