Eric Rybarczyk

Eric Rybarczyk

Enthusiastic and motivated software engineer with diverse experience

AWS & Java Certified Developer

Comparable vs Comparator

A bit of fundamental Java worth remembering

Eric Rybarczyk

pair of colorful birds
Feature photo by David Clode on Unsplash

Here we have two type names that seem awfully similar: Comparable<T> and Comparator<T>. How do they relate? When would we use one versus the other in our Java code?

In some important ways, they actually are quite similar. Both provide the means to perform a consistent comparison between two objects. More concretely, they identify which of the two objects should be sorted ahead of the other, according to the logic we define - typically by comparing one or more fields in the object.

However, the particulars of these two options are a bit different, and we also put them to use a bit differently in code.

Comparable<T>

Comparable is an interface, and when you implement it on your class you are defining the natural ordering which is used when collections of your objects are sorted with methods like Collections.sort() or Arrays.sort(). We call this natural ordering because it is inherent to your objects when they are used.

Suppose we have a Book object. Sorting a List<Book> might simply be alphabetical by Title. We can achieve this quite simply:

public class Book implements Comparable<Book> {

  // getters & setters omitted for brevity

  private String title;
  private String author;
  private String isbn;
  
  @Override
  public int compareTo(Book other) {
      return this.title.compareTo(other.getTitle());
  }

}

There is a simple and consistent logic to interpret the meaning of the int return value from the compareTo() method. A negative value indicates the ‘this’ object should be sorted before the ‘other’ object. A positive value means ‘this’ should be sorted after the ‘other’ object. Zero simply means the objects are seen as equal in terms of sorting.

Tip: a quick way to implement simple sorting based on a numeric field in your compareTo() method is to remember ‘this minus other’ for ascending order, and ‘other minus this’ for descending order. Suppose your ‘this’ field value is 7 and the ‘other’ value is 12… 7 minus 12 is -5, indicating the ‘this’ object should be sorted first, ahead of the ‘other’ object. The same pattern holds in reverse for a descending sort.

So, given a List<Book> we can easily sort them, as shown in the simple unit test below.

class BookTest {

  private List<Book> bookList;

  @BeforeEach
  void setUp() {
      bookList = new ArrayList<>();
      bookList.add(new Book("MMM Title", "Author M", "777-000"));
      bookList.add(new Book("AAA Title", "Author A", "111-000"));
      bookList.add(new Book("RRR Title", "Author R", "999-000"));
      bookList.add(new Book("CCC Title", "Author C", "444-000"));
  }

  @Test
  void sortListOfBooks_byNaturalOrder_resultIsAlphabeticalOrder() throws Exception {
      Collections.sort(bookList);
      assertThat(bookList).isSortedAccordingTo(Comparator.comparing(Book::getTitle)); 
      // Look into AssertJ if you're not familiar with the assertion above
  }
}

You should also keep in mind that there are some collection types, such as SortedSet<E> and SortedMap<K,V> which maintain their sorting based on the Comparable implementation.

Important Documentation Note: It is strongly recommended (though not required) that natural orderings be consistent with equals. This is so because sorted sets (and sorted maps) without explicit comparators behave “strangely” when they are used with elements (or keys) whose natural ordering is inconsistent with equals. In particular, such a sorted set (or sorted map) violates the general contract for set (or map), which is defined in terms of the equals method.

OK, so we can sort our objects according to a natural order but this is only a single implementation. What if we need to support more than one sorting option? This brings us to our next topic…

Comparator<T>

Comparator<T> is also an interface, but it is defined separately from your class definition and it’s compare(T o1, T o2) method operates on two instances of your type, rather than being coupled to a specific instance. Furthermore, it is a functional interface as well, which opens up all sorts of lambda goodness.

Looking back at our Book object, suppose we want to sort by the isbn field. First, we define our Comparator<Book>:

public class BookIsbnComparator implements Comparator<Book> {

  @Override
  public int compare(Book o1, Book o2) {
      return o1.getIsbn().compareTo(o2.getIsbn());
  }

}

Some developers might suggest this is a bit verbose and could be made much shorter, but we will start with this a implementation to be explicit about what a Comparator actually is - a stand-alone object that exists only to compare two instances of our type.

To demonstrate (and verify) our BookIsbnComparator we write a unit test:

class BookTest {

  // test setup code omitted, see prior code example

  @Test
  void sortListOfBooks_viaBookIsbnComparator_resultIsSortedByIsbnField() throws Exception {
      bookList.sort(new BookIsbnComparator());
      assertThat(bookList).isSortedAccordingTo(Comparator.comparing(Book::getIsbn));
  }

}

Getting back to how verbose the BookIsbnComparator seems… we can replace all that code with a simple lambda:

bookList.sort((o1, o2) -> o1.getIsbn().compareTo(o2.getIsbn()));

We can go a bit further and pass a method reference to the static Comparator.comparing(...) method.

bookList.sort(Comparator.comparing(Book::getIsbn));

Lambda expressions and method references are great for simplifying this kind of code, but also keep the DRY principle in mind: don’t repeat yourself. If you’re going to have multiple places in your application where you write the same lambda or method reference like these, another option is to create a class with each in a static method and then use this single definition where you need it.

public class BookComparators {

  public static Comparator<Book> byTitle() {
      return Comparator.comparing(Book::getTitle);
  }

  public static Comparator<Book> byAuthor() {
      return Comparator.comparing(Book::getAuthor);
  }

  public static Comparator<Book> byIsbn() {
      return Comparator.comparing(Book::getIsbn);
  }

}

Your use of this class is simply:

bookList.sort(BookComparators.byIsbn());
Mind.blow();

You can even make use of Comparators within your Comparable implementation:

@Override
public int compareTo(Book other) {
    return BookComparators.byTitle()
            .thenComparing(BookComparators.byAuthor())
            .compare(this, other);
}

This also illustrates chaining Comparators to form a composite sorting. In other words, if the first field used in the comparison is equal between the two objects, thenComparing() adds a secondary field to further refine the desired sort order.

Happy coding!


Article Series

Software Projects

Recent Posts

Categories

About

Enthusiastic and motivated software engineer