Implementing the java.lang.Comparable Interface – Object Comparison
14.4 Implementing the java.lang.Comparable<E> Interface
In order to sort the objects of a class, it should be possible to compare the objects. A total ordering allows objects to be compared. The criteria used to do the comparison depend on what is meaningful for the class. For example, comparison of Integer objects is based on the int value in an Integer object, whereas comparison of String objects is based on the Unicode value of the characters comprising the string in a String object. There can be more than one total ordering to compare the objects of a class. For example, another total ordering for String objects can be case insensitive—that is, treats uppercase characters as lowercase when comparing String objects.
The total ordering for objects of a class that is designated as the default ordering is called the natural ordering. A class defines the natural ordering for its object by implementing the java.lang.Comparable<E> generic interface. Other total orderings can be defined by providing a comparator that implements the java.util.Comparator<E> generic interface.
We will look at the two generic interfaces Comparable<E> and Comparator<E> for comparing objects in the rest of this chapter. The Comparable<E> interface is implemented by the objects of the class—in other words, the class provides the implementation for comparing its objects according to its natural ordering. The class usually leaves the implementation of a total ordering to its clients who can decide which total ordering is desirable.
The general contract for the Comparable<E> interface is defined by its only abstract method compareTo(), making this interface technically a functional interface. However, it is not annotated with the @FunctionalInterface annotation, and it is not intended to be implemented by lambda expressions.
int compareTo(E other)
Returns a negative integer, zero, or a positive integer if the current object is less than, equal to, or greater than the specified object, respectively, based on the natural ordering. It throws a ClassCastException if the reference value passed in the argument cannot be compared to the current object. It throws a Null-PointerException if the argument is null.
Many of the standard classes in the Java SE APIs, such as the primitive wrapper classes, enum types, String, LocalDate, LocalDateTime, LocalTime, and File, implement the Comparable<E> interface. Objects implementing this interface can be used as
- Elements in a sorted set
- Keys in a sorted map
- Elements in lists that are sorted using the Collections.sort() or List.sort() method
- Elements in arrays that are sorted using the overloaded Arrays.sort() methods
The natural ordering for String objects (and Character objects) is lexicographical ordering—that is, their comparison is based on the Unicode value of each corresponding character in the strings. Objects of the String and Character classes will be lexicographically maintained as elements in a sorted set, or as keys in a sorted map that uses their natural ordering.
The natural ordering for objects of a numerical wrapper class is in ascending order of the values of the corresponding numerical primitive type. As elements in a sorted set or as keys in a sorted map that uses their natural ordering, the objects will be maintained in ascending order.
According to the natural ordering for objects of the Boolean class, a Boolean object representing the value false is less than a Boolean object representing the value true.
An implementation of the compareTo() method for the objects of a class should meet the following criteria:
- For any two objects of the class, if the first object is less than, equal to, or greater than the second object, then the second object must be greater than, equal to, or less than the first object, respectively—that is, the comparison is anti-symmetric.
- All three comparison relations (less than, equal to, greater than) embodied in the compareTo() method must be transitive. For example, for any objects obj1, obj2, and obj3 of a class, if obj1.compareTo(obj2) > 0 and obj2.compareTo(obj3) > 0, then obj1.compareTo(obj3) > 0.
- For any two objects of the class that compare as equal, the compareTo() method must return the same result if these two objects are compared with any other object—that is, the comparison is congruent.
- The compareTo() method is strongly recommended (but not required) to be consistent with equals—that is, (obj1.compareTo(obj2) == 0) == (obj1.equals(obj2)) is true. This is recommended if the objects will be maintained in sorted sets or sorted maps.
The magnitude of non-zero values returned by the compareTo() method is immaterial; the sign indicates the result of the comparison. The general contract of the compareTo() method augments the general contract of the equals() method, providing a natural ordering of the compared objects. The equality test of the compareTo() method has the same provisions as that of the equals() method.
Implementing the compareTo() method is not much different from implementing the equals() method. In fact, given that the functionality of the equals() method is a subset of the functionality of the compareTo() method, the equals() implementation can call the compareTo() method. This guarantees that the two methods are always consistent with each other.
@Override public boolean equals(Object obj) {
return (this == obj)
|| (obj instanceof Whatever other
&& this.compareTo(other) == 0);
}
Example 14.8 Implementing the compareTo() Method of the Comparable<E> Interface
import java.util.Comparator;
import java.util.Objects;
public final class VersionNumber implements Comparable<VersionNumber> {
private final int release;
private final int revision;
private final int patch;
public VersionNumber(int release, int revision, int patch) {
this.release = release;
this.revision = revision;
this.patch = patch;
}
public int getRelease() { return this.release; }
public int getRevision() { return this.revision; }
public int getPatch() { return this.patch; }
@Override public String toString() {
return “(” + release + “.” + revision + “.” + patch + “)”;
}
@Override public boolean equals(Object obj) { // (1)
return (this == obj) // (2)
|| (obj instanceof VersionNumber vno // (3)
&& this.patch == vno.patch
&& this.revision == vno.revision
&& this.release == vno.release);
}
@Override public int hashCode() {
return Objects.hash(this.release, this.revision, this.patch); // (4)
}
@Override public int compareTo(VersionNumber vno) { // (5)
// Compare the release numbers. (6)
if (this.release != vno.release)
return Integer.compare(this.release, vno.release);
// Release numbers are equal, (7)
// must compare revision numbers.
if (this.revision != vno.revision)
return Integer.compare(this.revision, vno.revision);
// Release and revision numbers are equal, (8)
// patch numbers determine the ordering.
return Integer.compare(this.patch, vno.patch);
}
/*
@Override public int compareTo(VersionNumber vno) { // (9a)
return Comparator.comparingInt(VersionNumber::getRelease) // (10a)
.thenComparingInt(VersionNumber::getRevision) // (11a)
.thenComparingInt(VersionNumber::getPatch) // (12a)
.compare(this, vno); // (13a)
}
*/
}