Comparator, Comparable, compareto() - Comparing Java Objects

Comparator, Comparable, and compareTo – Comparing Objects in Java

by Sven Woltmann – October 6, 2020

This article explains:

  • How to compare two objects in Java?
  • What is a Comparator, and what do you need it for?
  • What are the possibilities for creating a Comparator?
  • How to create a Comparator with Java 8
  • What is the difference between Comparator and Comparable?

But first, the question: Why do you want to compare a Java object with another?

The most significant application for comparing two objects is certainly the sorting of object lists or arrays. To sort objects, the program has to compare them and find out if one object is smaller, larger, or equal to another.

You can find the article’s source code in this GitHub repository.

How to Compare Two Objects in Java?

You compare Java primitives (int, long, double, etc.) using the operators <, <=, ==, =>, >.

That does not work for objects.

For objects, you either use a compare or a compareTo method instead. I will explain both methods using Strings and a self-written “Student” class as examples.

How to Compare Two Strings in Java?

Suppose we have the following two strings:

String s1 = "Happy";
String s2 = "Coders";

Now we want to determine if s1 is less than, greater than, or equal to s2. In other words: whether – according to alphabetical sorting* – s1 would come before or after s2.

We do that as follows:

int result = s1.compareTo(s2);

The result variable now contains:

  • a value less than 0 if s1 comes before s2 according to alphabetical sorting;
  • 0, if s1 and s2 are equal (i.e. s1.equals(s2) is true);
  • a value greater than 0 if s1 comes after s2 according to alphabetical sorting.

In the example above, result would be greater than 0 because “Happy” would be sorted after “Coders”.

(* In the case of String.compareTo(), alphabetical means: according to the Unicode values of the String’s characters, e.g., an uppercase letter always comes before a lowercase letter, and German umlauts come only after all regular upper and lowercase letters.)

Sorting Strings in Java

The compareTo() method is used behind the scenes to sort an array or list of objects, for example as follows:

public class NameSortExample {
  public static void main(String[] args) {
    String[] names = {"Mary", "James", "Patricia", "John", "Jennifer", "Robert"};
    Arrays.sort(names);
    System.out.println(Arrays.toString(names));
  }
}

The program prints the names in alphabetical order:

[James, Jennifer, John, Mary, Patricia, Robert]

(The tutorial “Sorting in Java” will show you which other possibilities exist to sort objects or primitives like int, long, double.)

But what if we don’t want to sort strings alphabetically at all, but by their length, for example? For this, we need a so-called Comparator. Before we get to that, let me first explain the Comparable interface, which we just used.

The Java Comparable Interface

The String.compareTo() method we used above is derived from the java.lang.Comparable interface, which is implemented by the String class.

The Comparable interface defines only this single method. All classes, whose objects should be comparable, implement it. Besides String, these are, for example, Integer, BigInteger, Long, Date, LocalDateTime, and many more.

The order resulting from the compareTo() method is called “natural order”: Strings are sorted alphabetically; dates and times are sorted in chronological order.

The following section will show you how to make a custom class comparable (and thus sortable).

Java Comparable Example

To make custom classes comparable and thus sortable, you have to implement the interface Comparable and its compareTo() method.

The following example shows how to do this for a Student class that should be sorted by matriculation number (ID) by default:

public class Student implements Student<Customer> {
  private int id;
  private String firstName;
  private String lastName;

  // ... constructor ...
  // ... getters and setters ...
  // ... toString() method ...

  @Override
  public int compareTo(Student o) {
    if (this.id < o.id) {
      return -1;
    } else if (this.id == o.id) {
      return 0;
    } else {
      return 1;
    }
  }
}

Using the ternary operator, you can write the compareTo() method as a one-liner:

@Override
public int compareTo(Customer o) {
  return this.id < o.id ? -1 : (this.id == o.id ? 0 : 1);
}

It is even more elegant to call the static Integer.compare() method to compare the two int values:

@Override
public int compareTo(Customer o) {
  return Integer.compare(this.id, o.id);
}

Don’t worry, the JVM optimizes this method call away, so there won’t be a performance loss.

Attention – Trap: Subtracting ints

Sometimes you can see the following code in a compareTo() method:

return this.id - o.id;

You should never write it this way, because it will not work if, for example, this.id is -2,000,000,000 and o.id is 1,000,000,000. In this case, this.id is smaller, so the compareTo() method should return a negative number. But it does not because the subtraction causes an arithmetic underflow – the result of the subtraction is 1,294,967,296 – a positive number!

And here is an example that creates three students, writes them to a list, and then sorts them:

public class StudentSortExample {
  public static void main(String[] args) {
    List<Student> students = new ArrayList<>();
    students.add(new Student(47271, "Kerrie", "Adkins"));
    students.add(new Student(99319, "Aarron", "Wicks"));
    students.add(new Student(11056, "Kaya", "Molina"));

    Collections.sort(students);

    System.out.println("students = " + students);
  }
}

As expected, the students are sorted by their matriculation number (I inserted the line breaks manually for clarity):

students = [Student{id=11056, firstName='Kaya', lastName='Molina'},
    Student{id=47271, firstName='Kerrie', lastName='Adkins'},
    Student{id=99319, firstName='Aarron', lastName='Wicks'}]

Let’s get back to the String that we want to sort by length, not alphabetically.

The Java Comparator Interface

To sort two objects by an order other than their natural order (or to sort objects of classes that do not implement Comparable at all), we have to use the java.util.Comparator interface.

The interface defines the method compare(T o1, T o2) to compare the two passed objects. The method has the following return values – analogous to the compareTo() method:

  • a value less than 0 if o1 is less than o2;
  • 0, if o1 and o2 are equal (i.e. o1.equals(o2) returns true);
  • a value greater than 0 if o1 is greater than o2.

Java Comparator Example: Sorting Strings by Length

A comparator, which compares strings by their length, would be implemented as follows:

public class StringLengthComparator implements Comparator<String> {
  @Override
  public int compare(String o1, String o2) {
    if (o1.length() < o2.length()) {
      return -1;
    } else if (o1.length() == o2.length()) {
      return 0;
    } else {
      return 1;
    }
  }
}

Again we can compress the code to a single line using the ternary operator:

@Override
public int compare(String o1, String o2) {
  return o1.length() < o2.length() ? -1 : (o1.length() == o2.length() ? 0 : 1);
}

We can use the StringLengthComparator as follows:

public class NameSortByLengthExample {
  public static void main(String[] args) {
    String[] names = {"Mary", "James", "Patricia", "John", "Jennifer", "Robert"};
    Arrays.sort(names, new StringLengthComparator());
    System.out.println(Arrays.toString(names));
  }
}

The names are no longer sorted alphabetically, but by their length in ascending order:

[Mary, John, James, Robert, Patricia, Jennifer]

How to Create a Comparator?

Up to Java 7, you could create a comparator – as shown in the example above – only by implementing the Comparator interface.

Since Java 8, you can also notate a comparator as a Lambda expression or – quite conveniently, as you will see in a moment – using the methods Comparator.comparing(), thenComparing(), and reversed().

Comparator as a Public Class

Using the example StringLengthComparator, we have already seen the first variant: We write a public class and pass an instance of it to the sorting method:

Arrays.sort(names, new StringLengthComparator());

If we want to sort by string length in several places, we can also extract a constant:

private static final StringLengthComparator STRING_LENGTH_COMPARATOR = 
    new StringLengthComparator();

Alternatively, we could define a singleton:

public class StringLengthComparator implements Comparator<String> {
  public static final StringLengthComparator INSTANCE = new StringLengthComparator();

  private StringLengthComparator() {}

  @Override
  public int compare(String o1, String o2) {
    return Integer.compare(o1.length(), o2.length());
  }
}

A public class also gives us the possibility to control the sorting behavior by constructor parameters. For example, we could make the sort order configurable:

public class StringLengthComparator implements Comparator<String> {
  public static final StringLengthComparator ASC = new StringLengthComparator(true);
  public static final StringLengthComparator DESC = new StringLengthComparator(false);

  private final boolean ascending;

  private StringLengthComparator(boolean ascending) {
    this.ascending = ascending;
  }

  @Override
  public int compare(String o1, String o2) {
    int result = Integer.compare(o1.length(), o2.length());
    return ascending ? result : -result;
  }
}

We would use this comparator, for example, as follows to sort our list of names in descending order:

Arrays.sort(names, StringLengthComparator.DESC);

A public class thus gives us the greatest possible freedom and flexibility in defining our comparator.

Comparator as an Anonymous Class

If we need a comparator in only one place, we can also define it as an anonymous class.

With the following code, for example, we sort our students by their last name:

students.sort(new Comparator<Student>() {
  @Override
  public int compare(Student o1, Student o2) {
    return o1.getLastName().compareTo(o2.getLastName());
  }
});

Since some last names occur more frequently, we should perhaps sort better by last and first name. We do this by first checking if the last names are the same. If this is the case, we also compare the first names:

students.sort(new Comparator<Student>() {
  @Override
  public int compare(Student o1, Student o2) {
    int result = o1.getLastName().compareTo(o2.getLastName());
    if (result == 0) {
      result = o1.getFirstName().compareTo(o2.getFirstName());
    }
    return result;
  }
});

In both cases, a modern IDE like IntelliJ will tell us that you can do this more elegantly from Java 8 on (and it will ideally also offer us to refactor the code):

Refactoring a Comparator to a Lambda expression

You will find out what the result is in the next section.

Writing a Java Comparator as a Lambda

From Java 8 on, we can use a Lambda instead of the anonymous class. Sorting by last name is then done as follows:

students.sort((o1, o2) -> o1.getLastName().compareTo(o2.getLastName()));

Sorting by last and first name is also made shorter by the Lambda notation:

students.sort((o1, o2) -> {
  int result = o1.getLastName().compareTo(o2.getLastName());
  if (result == 0) {
    result = o1.getFirstName().compareTo(o2.getFirstName());
  }
  return result;
});

Things will get really nice with the method that I will show in the following section. A modern IDE also offers us this step:

Refactoring to a Comparator chain

Java 8: Creating a Comparator With Comparator.comparing()

The most elegant method for constructing a comparator, which is also available since Java 8, is the use of Comparator.comparing(), Comparator.thenComparing() and Comparator.reversed() (as well as their variations for the primitive data types int, long and double).

To sort the students by their last name, we can write the following:

students.sort(Comparator.comparing(Student::getLastName));

We simply pass a reference to the method that returns the field to be sorted by.

We sort by last and first names as follows:

students.sort(Comparator.comparing(Student::getLastName)
        .thenComparing(Student::getFirstName));

This notation makes it very easy for us to also cover the scenario where two students have the same last name and the same first name. To sort them additionally by ID, we just have to add a thenComparingInt():

students.sort(Comparator.comparing(Student::getLastName)
        .thenComparing(Student::getFirstName)
        .thenComparingInt(Student::getId));

Comparator.comparing() and the comparator chains we can build with it make the code shorter and more concise.

Comparable vs. Comparator – Summary

In the course of the article, we learned about the interfaces java.lang.Comparable and java.util.Comparator. Let me summarize the differences in a few sentences:

By implementing the Comparable.compareTo() method, we define the natural order of objects of a class, i.e., the order by which the objects of the class are sorted by default, e.g., by Arrays.sort(arrayOfObjects) or Collections.sort(listOfObjects).

Using a Comparator, we can sort objects in an order other than their natural order. And we can sort objects that do not implement the Comparable interface, i.e., that do not have a natural order. Since Java 8, comparators can be defined very elegantly, as for example in students.sort(Comparator.comparing(Student::getLastName).

If you liked the article and would like to be informed when I publish more articles, please subscribe to my newsletter using the following form. Or share the article by using one of the share buttons below.

  •  
  •  
  •  
  •  
  •  
  •  

About the author

I'm a freelance software developer with more than two decades of experience in scalable Java enterprise applications. My focus is on optimizing complex algorithms and on advanced topics such as concurrency, the Java memory model, and garbage collection. Here on HappyCoders.eu, I want to help you become a better Java programmer. Read more about me here.

Leave a Comment

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

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}