Comparator<T>

interface Comparator<T> {
  int compare(T left, T right);
}

(and also its friend Comparable<T>)

Rules

comparator.compareTo(a,b) < 0 iff a precedes b

comparator.compareTo(a,b) > 0 iff b precedes a

comparator.compareTo(a,b) == 0 iff a and b have equivalent positions

Long historical precendent, with "subtract" as the typical implementation; going back to at least the qsort() function in C, CMP instructions etc.

Properties

Antisymmetry of non-equivalent members
c(a,b) < 0c(b,a) > 0
Symmetry of equivalent members
c(a,b) = 0c(b,a) = 0
Reflexivity
c(a,a) = 0
Transitivity
c(a,b) > 0 ∧ c(b,c) > 0c(a,c) > 0

Comparable

For classes where there is a "natural" ordering of all objects in the class, essentially a canonical Comparator.

If a class implements Comparable, it is recommended that a.compareTo(b) == 0 iff a.equals(b). This makes things like TreeSet work as expected, in that they would retain the "same" elements as an equivalent HashSet, for example.

Bad things

Comparators often work by delegating to a sequence of properties in succession, returning the first (most significant) ordering found.

int compare(D left, D right) {
  int comparison = openDateTimeFor(left).compareTo(openDateTimeFor(right);
  if (comparison == 0) {
    User leftOwner = ownerOf(left);
    User rightOwner = ownerOf(right);
    int leftIsCurrentUser = isCurrentUser(leftOwner);
    int rightIsCurrentUser = isCurrentUser(leftOwner);
    if (leftIsCurrentUser == 0 && rightIsCurrentUser != 0) {
      comparison = 1;
    } else if (rightIsCurrentUser == 0 && leftIsCurrentUser != 0) {
      comparison = -1;
    } else {
      comparison = leftOwner.compareTo(rightOwner);
    }
  }
  return comparison;
}

Delegation

Comparator provides a factory based on extracting a property:

Comparator<D> compareByOpenDate =
    Comparator.comparing(D::getOpenDateTime);

And also by extracting a property, and then applying another comparator to the extracted value:

Comparator<D> compareByOwnerName =
    Comparator.comparing(D::getOwner,
                         Comparator.comparing(User::getName,
                                              String.CASE_INSENSITIVE_ORDER));

Composition

Comparator also has default methods to produce a composed comparator that will perform an additional comparison:

Comparator<D> compareByOpenDateAndThenOwnerName =
    Comparator.comparing(D::getOpenDateTime)
              .thenComparing(D::getOwner,
                             Comparator.comparing(User::getName));

Better things

Comparator.comparing(D::getOpenDateTime)
          .thenComparing(D::getOwner,
                         rankEqualFirst(signedOnUser)
                         .thenComparing(Comparator.naturalOrder()));

private <T> static Comparator<T> rankEqualFirst(T other) {
  return Comparator.comparingInt(x -> x.equals(other) ? 0 : 1);
}

Note comparingInt - there are int, double and long specializations of comparing, thenCompare to avoid unboxing

More badness

int compare(D left, D right) {
  if (null != left && null != right) {
    return caseInsensitiveCompare(getName(left), getName(right));  
  }
  if (null == left && null != right) return -1;
  if (null != left && null == right) return 1;
  return 0;
}

private String getName(D obj) {
  String name = obj.getName();
  return Strings.isNullOrEmpty(name) ? "" : name;
}  

Comparator.nullsFirst(
           Comparator.comparing(D::getName,
                                Comparator.nullsFirst(
                                           String.CASE_INSENSITIVE_ORDER)))

Really bad things

int compareNonEqual(D left, D right) {
 if (rankHighly.contains(left)) return -1;
 if (rankHighly.contains(right)) return 1;
 if (left == RankLessHighly) return -1;
 if (right == RankLessHighly) return 1;
 if (left == RankEvenLessHighly) return 1;
 if (right == RankEvenLessHighly) return -1;
 if (left == RankLast) return 1;
 if (right == RankLast) return -1;
 return left.compareTo(right);
}

This breaks the expected antisymmetry behaviours: c(high1,high2) < 0 and c(high2,high1) < 0

Really bad things


int compare(D left, D right) {
 // ... various comparisons
 return left.equals(right) ? 0 : -1;
}

This also the expected antisymmetry behaviour: both c(a,b) < 0 and c(b,a) < 0 for any a and b making it to the final return.

Want more examples?

ideas.git$ git log --since=1.jan.2015 -p --grep=Comparator