Hi! Today we're going to talk about comparing objects. Java's Comparator class - 1 Hmm... But haven't we already talked this topic more than once? :/ We know how the == operator works, as well as the equals() and hashCode() methods. Comparison is a little different. Previously, we most likely meant "checking objects for equality". But the reasons for comparing objects with each other can be completely different! The most obvious of these is sorting. I think if you were told to sort an ArrayList<> of numbers or strings, you would be able to handle this without any problems:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       String name1 = "Masha";
       String name2 = "Sasha";
       String name3 = "Dasha";

       List<String> names = new ArrayList<>();
       names.add(name1);
       names.add(name2);
       names.add(name3);

       Collections.sort(names);
       System.out.println(names);
   }
}
Console output:
[Dasha, Masha, Sasha]
If you remembered the Collections class and its sort() method, well done! I think you'll also have no trouble with numbers. Here's a more challenging task for you:
public class Car {

   private int manufactureYear;
   private String model;
   private int maxSpeed;

   public Car(int manufactureYear, String model, int maxSpeed) {
       this.manufactureYear = manufactureYear;
       this.model = model;
       this.maxSpeed = maxSpeed;
   }

   // ...getters, setters, toString()

}

import java.util.ArrayList;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       List<Car> cars = new ArrayList<>();

       Car ferrari = new Car(1990, "Ferrari 360 Spider", 310);
       Car lambo = new Car(2012, "Lamborghini Gallardo", 290);
       Car bugatti = new Car(2010, "Bugatti Veyron", 350);

       cars.add(ferrari);
       cars.add(bugatti);
       cars.add(lambo);
   }
}
The task is actually simple. We have a Car class and 3 Car objects. Would you kindly sort the cars in the list? You will probably ask, "How should they be sorted?" By name? By year of manufacture? By maximum speed? Excellent question. At the moment, we don't know how to sort the Car objects. And, quite naturally, Java doesn't know that either! When we try to pass a list of Car objects to the Collections.sort() method, we get an error:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       List<Car> cars = new ArrayList<>();

       Car ferrari = new Car(1990, "Ferrari 360 Spider", 310);
       Car lambo = new Car(20012, "Lamborghini Gallardo", 290);
       Car bugatti = new Car(2010, "Bugatti Veyron", 350);

       cars.add(ferrari);
       cars.add(bugatti);
       cars.add(lambo);

       // Compilation error!
       Collections.sort(cars);
   }
}
And indeed, how would the language know how to sort objects of classes you've written? This depends on what your program needs to do. We must somehow teach Java to compare these objects. And to compare them just how we want. Java does have a special mechanism for this: the Comparable interface. In order to somehow compare and sort our Car objects, the class must implement this interface, which consists of a single method: compareTo():
public class Car implements Comparable<Car> {

   private int manufactureYear;
   private String model;
   private int maxSpeed;

   public Car(int manufactureYear, String model, int maxSpeed) {
       this.manufactureYear = manufactureYear;
       this.model = model;
       this.maxSpeed = maxSpeed;
   }

   @Override
   public int compareTo(Car o) {
       return 0;
   }

   // ...getters, setters, toString()

}
Please note that we specified the Comparable<Car> interface, not just Comparable. This is a parameterized interface, that is, we must specify the specific associated class. In principle, you can remove <Car> from the interface, but then the comparing will be based on Object objects by default. Instead of the compareTo(Car o) method, our class will have:
@Override
   public int compareTo(Object o) {
       return 0;
   }
Of course, it's much easier for us to work with Car. Inside the compareTo() method, we implement our logic for comparing cars. Suppose we need to sort them by year of manufacture. You probably noticed that the compareTo() method returns an int, not a boolean. Don't let this surprise you. When we compare two objects, there are 3 possibilities:
  • а < b
  • a > b
  • a == b.
boolean has only 2 values: true and false, which doesn't work well for comparing objects. With int, everything is much simpler. If the return value is > 0, then a > b. If the result of compareTo is < 0, then a < b. And, if the result is == 0, then two objects are equal: a == b. Teaching our class to sort cars by year of manufacture is easy peasy:
@Override
public int compareTo(Car o) {
   return this.getManufactureYear() - o.getManufactureYear();
}
But what's going on here? We take one Car object (this), get this car's year of manufacture, and subtract from it the year of manufacture of another car (the one with which the object is being compared). If the first car's year of manufacture is greater, the method will return an int > 0. This means that the this car > the o car. Conversely, if the year of manufacture of the second car (о) is greater, then the method will return a negative number, which means that o > this. Finally, if they are equal, then the method will return 0. This simple mechanism is already enough for us to sort collections of Car objects! You don't have to do anything else. Check it out:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       List<Car> cars = new ArrayList<>();

       Car ferrari = new Car(1990, "Ferrari 360 Spider", 310);
       Car lambo = new Car(2012, "Lamborghini Gallardo", 290);
       Car bugatti = new Car(2010, "Bugatti Veyron", 350);

       cars.add(ferrari);
       cars.add(bugatti);
       cars.add(lambo);

       // There was previously an error here
       Collections.sort(cars);
       System.out.println(cars);
   }
}
Console output:
[Car{manufactureYear=1990, model='Ferrari 360 Spider', maxSpeed=310},
Car{manufactureYear=2010, model='Bugatti Veyron', maxSpeed=350},
Car{manufactureYear=2012, model='Lamborghini Gallardo', maxSpeed=290}]
The cars are sorted like we want! :) Java's Comparator class - 2When should I use Comparable? The comparison method implemented in Comparable is called natural ordering. This is because in the compareTo() method you define the most common, or natural, way of comparing objects of this class. Java already has a natural ordering. For example, Java knows that strings are most often sorted alphabetically, and numbers by increasing numeric value. Therefore, if you call the sort() method on a list of numbers or strings, they will be sorted. If our program will usually compare and sort cars by year of manufacture, then we should define the natural sorting for Cars using the Comparable<Car> interface and the compareTo() method. But what if this is not enough for us? Let's imagine that our program is not so simple. In most cases, the natural sorting of cars (which we have set to be performed by year of manufacture) suits us. But sometimes our customers are aficionados of fast driving. If we are preparing a car catalog for them to peruse, the cars should be sorted by maximum speed. Java's Comparator class - 3For example, suppose we need to sort like this 15% of the time. This is clearly not enough for us to set the Car class's natural sorting to be by speed instead of by year of manufacture. But we can't ignore 15% of our customers. So what do we do? Another interface comes to our aid here: Comparator. Just like Comparable, it is a parameterized interface. What's the difference? Comparable makes our objects "comparable" and defines their most natural sort order, i.e. the sort order that will be used in most cases. Comparator is a separate "comparing" interface. If we need to implement some kind of special sorting order, we don't need to go into the Car class and change the logic of compareTo(). Instead, we can create a separate class that implements Comparator and teach it how to perform the sorting we need!
import java.util.Comparator;

public class MaxSpeedCarComparator implements Comparator<Car> {

   @Override
   public int compare(Car o1, Car o2) {
       return o1.getMaxSpeed() - o2.getMaxSpeed();
   }
}
As you can see, our Comparator is pretty simple. We need to implement only one interface method: invoke(). It takes two Car objects as inputs and compares their maximum speeds in the usual way (by subtraction). Like compareTo(), it returns the an int, and the principle of comparison is the same. How do we use this? It's all straightforward:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       List<Car> cars = new ArrayList<>();

       Car ferrari = new Car(1990, "Ferrari 360 Spider", 310);
       Car lambo = new Car(2012, "Lamborghini Gallardo", 290);
       Car bugatti = new Car(2010, "Bugatti Veyron", 350);

       cars.add(ferrari);
       cars.add(bugatti);
       cars.add(lambo);

       Comparator speedComparator = new MaxSpeedCarComparator();
       Collections.sort(cars, speedComparator);

       System.out.println(cars);
   }
}
Console output:
[Car{manufactureYear=2012, model='Lamborghini Gallardo', maxSpeed=290},
Car{manufactureYear=1990, model='Ferrari 360 Spider', maxSpeed=310},
Car{manufactureYear=2010, model='Bugatti Veyron', maxSpeed=350}]
We simply create a comparator object and pass it to the Collections.sort() method along with the list to be sorted. When the sort() method receives a comparator, it does not use the natural sorting defined in the Car class's compareTo() method. Instead, it applies the sorting algorithm defined by the comparator passed to it. Java's Comparator class - 4What are the advantages of doing this? First, compatibility with existing code. We created a new, special sorting method, while retaining the existing one that will be used most of the time. We didn't touch the Car class at all. It was a Comparable, and so it remains:
public class Car implements Comparable<Car> {

   private int manufactureYear;
   private String model;
   private int maxSpeed;

   public Car(int manufactureYear, String model, int maxSpeed) {
       this.manufactureYear = manufactureYear;
       this.model = model;
       this.maxSpeed = maxSpeed;
   }

   @Override
   public int compareTo(Car o) {
       return this.getManufactureYear() - o.getManufactureYear();
   }

   // ...getters, setters, toString()

}
Second, flexibility. We can add as many sorting algorithms as we like. For example, we can sort cars by color, speed, weight, or by how many times a car has been used in Batman movies. All we need to do is create an additional Comparator. That's it! Today you have studied two very important mechanisms that you will often use in real projects at work. But, as you know, theory without practice is nothing. Now it's time to consolidate your knowledge and complete some tasks!