Composite comparators in Java

Some time ago a fellow developer wrote a really comprehensive blog post (unfortunately only available in german) about comparator implementations in Java. More specifically it is about composite comparators used to compare entities according to different attributes. The best solution for Java 7 he comes up with is a comparator

class FoobarComparator implements Comparator {
  @Override
  public int compare(Foobar lhs, Foobar rhs) {
    return compoundCompare(
      lhs.getLastName().compareTo(rhs.getLastName()),
      lhs.getFirstName().compareTo(rhs.getFirstName()),
      lhs.getPlaceOfBirth().compareTo(rhs.getPlaceOfBirth()),
      lhs.getDateOfBirth().compareTo(rhs.getDateOfBirth()));
  }
}

with a reusable compoundCompare()-method

// utility method used with static import
int compoundCompare(int... results) {
  for (int result : results) {
    if (result != 0) {
      return result;
    }
  }
  return 0;
}

While this solution is quite clean and a vast improvement over the critized implementations it has the flaw that it eagerly evaluates all attributes even though short-circuiting may be possible for many entities. This may lead to performance problems in some cases. So he goes on to explain how Java 8 will fix this problem with Lambdas or another solution he calls “KeyMethodComparator”.

Now I want to show you an implementation very similar to his approach above but without the performance penalty and possible in Java 7 using the composite pattern:

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

class FoobarComparator implements Comparator<Foobar> {

  private List<Comparator<Foobar>> defaultFoobarComparison =
    Arrays.<Comparator<Foobar>>asList(
      new Comparator<Foobar>() {
        @Override
        public int compare(Foobar lhs, Foobar rhs) {
          return lhs.getLastName().compareTo(rhs.getLastName());
        }
      },
      new Comparator<Foobar>() {
        @Override
        public int compare(Foobar lhs, Foobar rhs) {
          return lhs.getFirstName().compareTo(rhs.getFirstName());
        }
      },
      new Comparator<Foobar>() {
        @Override
        public int compare(Foobar lhs, Foobar rhs) {
          return lhs.getPlaceOfBirth().compareTo(rhs.getPlaceOfBirth());
        }
      },
      new Comparator<Foobar>() {
        @Override
        public int compare(Foobar lhs, Foobar rhs) {
          return lhs.getDateOfBirth().compareTo(rhs.getDateOfBirth());
        }
      });

  @Override
  public int compare(Foobar lhs, Foobar rhs) {
    for (Comparator<Foobar> comp : defaultFoobarComparison) {
      int result = comp.compare(lhs, rhs);
      if (result != 0) {
        return result;
      }
    }
    return 0;
  }
}

It features the lazy evaluation demanded by my fellow for performance and allows flexible construction of different composite comparators if you, e.g. add a constructor accepting a list of comparators.
Imho, it is a quite elegant solution using standard object-oriented programming in Java today and not only in the future.

4 thoughts on “Composite comparators in Java”

  1. Hmm, why not make it shorter, like so?
    public class GenericCompositeComparator implements Comparator{
    Comparator[] comparators;

    public GenericCompositeComparator(Comparator… comparators) {
    this.comparators = comparators;
    }

    @Override
    public int compare(V lhs, V rhs) {
    for (Comparator comp : comparators) {
    int result = comp.compare(lhs, rhs);
    if (result != 0) {
    return result;
    }
    }
    return 0;
    }
    }

    and use it like so:
    public class FoobarComparator extends GenericCompositeComparator {
    @SuppressWarnings(“unchecked”)
    public FoobarComparator() {
    super(
    new Comparator() {
    @Override
    public int compare(Foobar lhs, Foobar rhs) {
    return lhs.getLastName().compareTo(rhs.getLastName());
    }
    },
    new Comparator() {
    @Override
    public int compare(Foobar lhs, Foobar rhs) {
    return lhs.getFirstName().compareTo(rhs.getFirstName());
    }
    });
    }
    }

    Now imagine that with lamda expressions or method references…

    1. Yes, you are right. Your refactoring takes the whole approach one step further resulting in a generic and reusable CompositeComparator. I wanted to stay as close as possible to the original code in the mentioned blog.
      Lambdas will reduce noise dramatically in such cases so I am really looking forward to them….

  2. Oops, these classes were supposed to use generics, but the types were stripped during posting.
    The class was supposed to use a type T and the constructor to accept comparators of ? super T

  3. /**
    * The purpose of this class is to ease implementation of {@link Comparable#compareTo(Object)}
    * or {@link java.util.Comparator#compare(Object, Object)} when we need to compare multiple
    * attributes.
    *
    * Example:
    *

    {@code
     *   @Override
     *   public int compareTo(Obj other) {
     *     return Pair.compare(new Pair[] {
     *         new Pair(this.age, other.age),
     *         new Pair(this.firstName, other.firstName),
     *         new Pair(this.lastName, other.lastName)
     *     });
     *   }
     * }

    * @param pair type
    */
    public class Pair {

    private final Comparable p1;
    private final T p2;

    public Pair(Comparable p1, T p2) {
    this.p1 = p1;
    this.p2 = p2;
    }

    public int compare() {

    if (p1 == null && p2 == null) {
    return 0;
    }
    if (p1 == null) {
    return -1;
    }
    if (p2 == null) {
    return 1;
    }
    return p1.compareTo(p2);
    }

    public static int compare(Pair[] pairs) {

    for (Pair pair : pairs) {
    int result = pair.compare();
    if (result != 0) {
    return result;
    }
    }
    return 0;
    }

    }

Leave a Reply to Frisian Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.