TIL: Comparable and Comparator

Today I learned about the Comparable and Comparator interfaces and how to use them for better sorting functionality.

One of the really nice benefits of Collections is that they provide you the ability to sort all of the items in your collection. One of my examples from a few days back actually used a Comparator implementation when doing the sorting of a class that I had defined, but I really didn’t understand how it was working. The instructor eventually circled back around to briefly touch on it, but I was lost and off on my own for quite a bit doing some extra research into what it was.

I was able to find a really great article over on the baeldung blog discussing not only the Comparator interface, but also the Comparable interface. The second one was even more news to me, so I decided to go ahead and pull it into covering this post.

For all of the examples in this post, I’m going to be using a Student class that looks something like this:

1
2
3
4
5
6
7
8
public class Student {
   int age;
   int grade;
   double gpa;
   String name;

   // plus constructor, getters, setters, toString, etc
}

What do they do?

While Collections give us the ability to perform sorting, we have to give the compiler some way of knowing how to do the sorting for us. As we’ve been covering up to this point, our lists are usually lists of objects that we have defined, each with their own set of properties. There’s no way for the application to just know what we want to compare against, and that’s where Comparable and Comparator come into play. Once we have implemented these interfaces in our code, we will be able to execute the sort specifically as we define it to do.

When the Comparator method ends up getting called, it’s going to be doing a check between two values. The return from this will be an int that is either positive, negative, or 0. The value will determine whether or not the values being compared need to be swapped or if they’re good as they are. You’ll notice as we go along that the Comparator methods are always implemented with returning an int.

The only thing you need to worry about is returning back that positive, negative, or zero value. The sort method is going to handle the rest for you.

A note on implementation

While researching this I found that there are MANY ways to actually implement Comparable and Comparator for your classes. So many that it’s a bit overwhelming. I’m going to try to cover all of the ways that I came across, but I’m sure there are many more that exist. Don’t be afraid to do your own research if these don’t fit your style.

Comparable

The Comparable interface is designed to compare specific items within a specific class. It utilizes the this keyword to compare two objects to one another. When calling the sort method on a Collection, it’s going to go looking for a compareTo method in the supplied classes to see how the sort should be performed.

When defining your class, you’re going to define the class as implementing Comparable<class>, and creating and override method for compareTo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class Student implements Comparable<Student> {
   int age;
   int grade;
   double gpa;
   String name;

   @Override
   public int compareTo(Student o) {
       return this.getAge() - o.getAge();
   }

   // plus constructor, getters, setters, toString, etc
}

With the Comparable implemented in the class, you can use it by calling Collections.sort(<List>), like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class StudentTest {

   List<Student> students = new ArrayList<Student>();

   @Before
   public void setUp() {
       students.add(new Student(17, 12, 3.5, "James"));
       students.add(new Student(17, 12, 3.25, "Jill"));
       students.add(new Student(16, 10, 2.4, "Bill"));
       students.add(new Student(15, 9, 3.9, "Sally"));
       students.add(new Student(18, 12, 3.8, "Emily"));
   }

   @Test
   @Ignore
   public void comparatorSorting() {
       System.out.println(students.toString());
       Collections.sort(students);
       System.out.println(students.toString());
   }
}

In this case, we’ll get the following output returned:

1
2
[Student{age=17, grade=12, gpa=3.5, name='James'}, Student{age=17, grade=12, gpa=3.25, name='Jill'}, Student{age=16, grade=10, gpa=2.4, name='Bill'}, Student{age=15, grade=9, gpa=3.9, name='Sally'}, Student{age=18, grade=12, gpa=3.8, name='Emily'}]
[Student{age=15, grade=9, gpa=3.9, name='Sally'}, Student{age=16, grade=10, gpa=2.4, name='Bill'}, Student{age=17, grade=12, gpa=3.5, name='James'}, Student{age=17, grade=12, gpa=3.25, name='Jill'}, Student{age=18, grade=12, gpa=3.8, name='Emily'}]

But as you may have noticed, this really limits what we’re able to do as far as sorting is concerned. We are only able to define one way that the sorting can take place. What if we wanted to sort by grade? What about GPA and name? Well, that’s where Comparator comes into play.

Comparators

Unlike Comparable which must be implemented within the class itself, Comparators can be standalone sorting functions. You could define them inside of the class, or even create new classes specifically for the filter. For the sake of clarity, I’m just going to be defining my Comparators as static methods within the Student class.

When using Comparators, we’re going to be dealing with two inputs rather than just the one with Comparables. However, we’re still going to be doing the same thing. The goal is still to return back an int to the sort method that’s either positive, negative, or zero.

In the most basic (verbose) example, the defined comparator is going to be overriding the compare method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class Student implements Comparable<Student> {
   int age;
   int grade;
   double gpa;
   String name;

   @Override
   public int compareTo(Student o) {
       return this.getAge() - o.getAge();
   }

   public static Comparator<Student> COMPARE_GRADE = new Comparator<Student>() {
       @Override
       public int compare(Student o1, Student o2) {
           return o1.getGrade() - o2.getGrade();
       }
   };
  
   // plus constructor, getters, setters, toString, etc
}

In order to use this Comparator, all we need to do is plug it into our sort method call from before:

1
2
3
4
5
   public void compareGrade() {
       System.out.println(students.toString());
       students.sort(Student.COMPARE_GRADE);
       System.out.println(students.toString());
   }

And as you can see in the output below, we’ve successfully sorted by the grade property of the students:

1
2
[Student{age=17, grade=12, gpa=3.25, name='James'}, Student{age=17, grade=12, gpa=3.5, name='Jill'}, Student{age=16, grade=10, gpa=2.4, name='Bill'}, Student{age=15, grade=9, gpa=3.9, name='Sally'}, Student{age=18, grade=12, gpa=3.8, name='Emily'}]
[Student{age=16, grade=10, gpa=2.4, name='Bill'}, Student{age=18, grade=12, gpa=3.8, name='Emily'}, Student{age=17, grade=12, gpa=3.25, name='James'}, Student{age=17, grade=12, gpa=3.5, name='Jill'}, Student{age=15, grade=9, gpa=3.9, name='Sally'}]

Comparators in Java 8

And using the capabilities of Java 8, we can actually reduce the syntax on Comparators even further. Take a look at the following two examples. Both do the exact same sort comparison on grade:

1
2
3
   public static Comparator<Student> COMPARE_GRADE_LAMBDA = (o1, o2) -> o1.getGrade() - o2.getGrade();

   public static Comparator<Student> COMPARE_GRADE_LAMDA_KEY = Comparator.comparingInt(Student::getGrade);

In my opinion this makes things even easier to read, especially using the methods available from Comparator.

Everything so far has just been examples of numbers. But what if you want to do a comparison with Strings? Just use the Strings compareTo method to get the exact same functionality:

1
2
3
4
5
6
7
8
   public static Comparator<Student> COMPARE_NAMES = new Comparator<Student>() {
       @Override
       public int compare(Student o1, Student o2) {
           return o1.getName().compareTo(o2.getName());
       }
   };

   public static Comparator<Student> COMPARE_NAMES_LAMBDA = Comparator.comparing(Student::getName);

Multi-comparison

What if I want to sort not only by age, but also by gpa? Let’s take a look:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
   public static Comparator<Student> COMPARE_AGE_GPA = (o1, o2) -> {
       int age = o1.getAge() - o2.getAge();
       double gpa =  (o1.getGpa() - o2.getGpa());

       if (age == 0) {
           return gpa == 0 ? age : gpa > 0 ? 1 : -1;
       } else {
           return age;
       }
   };

You can see that we are able to do our comparisons up front and then nest our checks to see in which order they satisfy our condition. Whe then execute it exactly the same way as we were doing before:

1
2
3
4
5
   public void compareAgeGpa() {
       System.out.println(students.toString());
       students.sort(Student.COMPARE_AGE_GPA);
       System.out.println(students.toString());
   }

And in the output, we can see that Jill will come before James based on the GPA:

1
2
[Student{age=17, grade=12, gpa=3.5, name='James'}, Student{age=17, grade=12, gpa=3.25, name='Jill'}, Student{age=16, grade=10, gpa=2.4, name='Bill'}, Student{age=15, grade=9, gpa=3.9, name='Sally'}, Student{age=18, grade=12, gpa=3.8, name='Emily'}]
[Student{age=15, grade=9, gpa=3.9, name='Sally'}, Student{age=16, grade=10, gpa=2.4, name='Bill'}, Student{age=17, grade=12, gpa=3.25, name='Jill'}, Student{age=17, grade=12, gpa=3.5, name='James'}, Student{age=18, grade=12, gpa=3.8, name='Emily'}]

Conclusion

This started out as being pretty daunting to me, it seemed like way too much magic was going on. But after working through some examples, it doesn’t seem to bad. Like with everything, it’s about knowing the tools that are available to you.

Hopefully you found this useful and were able to learn something along with me. As always, the code for this post can be found in my gethub repo.

💚 A.B.L.