Every time we need to sort a collection of data, we need to compare its elements with each other to determine which of them should go first and which should go last. It is not a big deal if we have to compare numbers or dates but becomes slightly complicated for many other real-world examples such as school students, social media posts, or bank accounts. This is where Comparator comes to the rescue, and we are going to discuss how to use it in detail in this topic.

Custom data types

Let’s create a class modeling a general message. For simplicity, we assume that the content of such a message can be represented by a String:

class Message {

    private final String content;

    public Message(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }

    @Override
    public String toString() {
        return content;
   }
}

Now we need to put a few such messages in a collection to work with:

List<Message> messages = new ArrayList<>();

messages.add(new Message("Hello"));
messages.add(new Message("humans!"));
messages.add(new Message("We"));
messages.add(new Message("came"));
messages.add(new Message("with"));
messages.add(new Message("peace!"));

If we try to sort the list of these Message objects, we will get a compilation error, because our Message class does not support any methods that allow for comparing its instances. To solve this issue, let’s create a Comparator that will define a sorting ordering for objects of the Message class.

Comparator

Comparator<T> is a generic interface that has a single abstract method compare and quite a few non-abstract methods that we will take a look at later. To create a Comparator, we need to define a class that implements the Comparator interface and overrides its single abstract method:

class MessageContentComparator implements Comparator<Message> {

    @Override
    public int compare(Message message1, Message message2) {
        // here we should define how these two arguments will be compared
        return 0;
    }
}

We are expected to implement this method according to the following rules:

  • It should return 0 if both arguments are equal;
  • It should return a positive number if the first argument is greater than the second one;
  • It should return a negative number if the first argument is less than the second one.

This way we can even override the natural ordering for any type that implements the Comparable interface.

In the context of Java, the natural order is the order defined by the compareTo method of the Comparable interface.

In this example, we want to sort our messages based on the length of their content:

class MessageContentComparator implements Comparator<Message> {

    @Override
    public int compare(Message message1, Message message2) {
        int firstLength = message1.getContent().length();
        int secondLength = message2.getContent().length();
        return Integer.compare(firstLength, secondLength);
    }
}

Here we used the compare static method of the Integer class to safely compare two int numbers. Let’s sort the message list using MessageContentComparator:

messages.sort(new MessageContentComparator());
messages.forEach(System.out::println);

Here is the output:

We
came
with
Hello
peace!
humans!

The messages have been printed in the order of the length of their content rather than in the order they were added to the list. Moreover, we can have multiple Comparator classes for our class and sort its instances using different orderings depending on our needs.

Multiple fields

First, let’s extend the Message class and then see how we can implement sorting:

class Message {

    private final String from;
    private final String content;
    private final LocalDate created;
    private int likes;

    public Message(String from, String content, int likes, String created) {
        this.from = from;
        this.content = content;
        this.likes = likes;
        this.created = LocalDate.parse(created);
    }

    // getters and setters

    @Override
    public String toString() {
        return created.toString() + " " + from + " wrote: " + 
                content + " (" + likes + ")";
    }
}

After that, we need to create a new list of messages to try different sorting criteria.

List<Message> messages = new ArrayList<>();

messages.add(new Message("Alien", "Hello humans!", 
        32, "2034-03-25"));
messages.add(new Message("Pirate", "All hands on deck!", 
        -2, "2034-01-05"));
messages.add(new Message("User765214", "Bump!", 
        1, "2033-02-17"));
messages.add(new Message("Unregistered", "This message was marked as spam", 
        -18, "2033-01-14"));

In addition to the comparator we already have, we will create some more:

class MessageDateComparator implements Comparator<Message> {

    @Override
    public int compare(Message message1, Message message2) {
        return message1.getCreated().compareTo(message2.getCreated());
    }
}

class MessageAuthorComparator implements Comparator<Message> {

    @Override
    public int compare(Message message1, Message message2) {
        return message1.getFrom().compareTo(message2.getFrom());
    }
}

Now we can use these classes to sort the list of Message instances by different criteria, for example:

By date:

messages.sort(new MessageDateComparator());

Output:

2033-01-14 Unregistered wrote: This message was marked as spam (-18)
2033-02-17 User765214 wrote: Bump! (1)
2034-01-05 Pirate wrote: All hands on deck! (-2)
2034-03-25 Alien wrote: Hello humans! (32)

By author’s name:

messages.sort(new MessageAuthorComparator());

Output:

2034-03-25 Alien wrote: Hello humans! (32)
2034-01-05 Pirate wrote: All hands on deck! (-2)
2033-01-14 Unregistered wrote: This message was marked as spam (-18)
2033-02-17 User765214 wrote: Bump! (1)

As you can see, with a suitable Comparator, we can apply any sorting logic to any class.

Java 8 features

Since Comparator has only a single abstract method (SAM) and therefore is a functional interface, we can use lambda functions to create Comparator instances. For example, instead of the full class declaration, we can rewrite MessageDateComparator as follows:

Comparator<Message> dateComparator = (m1, m2) -> 
        m1.getCreated().compareTo(m2.getCreated());
messages.sort(dateComparator);

We can even avoid using the named declaration and pass the lambda directly to the sort method as an argument:

messages.sort((m1, m2) -> m1.getCreated().compareTo(m2.getCreated()));

If you are not going to reuse a Comparator object, declaring it as a standalone class would be unnecessary, so you can just define it as a lambda function and use it immediately.

Utility methods

Comparator also has several non-abstract methods that can be used for combining comparators to create complex conditions for comparing objects. Let’s take a look at some of them.

Comparator.naturalOrder returns Comparator of the applicable type that compares Comparable objects in the natural order. This means that if the class you want to compare using this method does not implement the Comparable interface, you will get a compilation error.

Comparator.reverseOrder similar to the above, but compares Comparable objects using the reverse of the natural ordering.

reversed is called on a Comparator and returns a new Comparator that imposes the reverse ordering of the affected Comparator.

Comparator.comparing accepts a function that extracts a Comparable sort key and returns a Comparator that compares objects by that key.

thenComparing returns a lexicographic-order comparator with a function that extracts a Comparable sort key.

Be careful when using the reversed() method: it will reverse the whole chain of preceding comparators. Use parentheses to limit its scope if needed.

Here are some examples of how we can use these methods to sort a collection according to our needs:

By date, new first, using a method reference to pass the sort key extractor function:

messages.sort(Comparator.comparing(Message::getCreated).reversed());

Output:

2034-03-25 Alien wrote: Hello humans! (32)
2034-01-05 Pirate wrote: All hands on deck! (-2)
2033-02-17 User765214 wrote: Bump! (1)
2033-01-14 Unregistered wrote: This message was marked as spam (-18)

By the number of likes, in descending order, and then, for messages with equal numbers of likes, by the author’s name, in ascending order:

messages.sort(Comparator.comparing(Message::getLikes)
        .reversed()
        .thenComparing(Message::getFrom));

And so on. Try it on a bigger collection to see how it works. This way you can implement any sorting of messages on a message board.

Comparator vs Comparable

Both these interfaces provide similar functionality: they allow for comparing objects of the same class. Which one should you choose? It depends on many factors.

Comparable defines the natural ordering for objects of a class implementing it. Therefore, it suits perfectly in the cases where we compare objects that inherently have a certain natural order, such as numbers or dates. If your class has an obvious natural ordering, then using Comparable is the way to go.

In other cases, when your class has multiple properties, for example, name and age or price and userRating, you might be unable to define the natural order for such objects. Also, there may be a situation where a class whose instances are to be compared does not implement the Comparable interface and you cannot modify the source code of that class. In all such cases, the Comparator interface is your choice.

Also, Comparator serves as an extension and allows for customizing the sorting process. With its help, you can easily redefine the natural ordering or add new rules for sorting objects. Further, it makes it possible to combine sorting orderings to create complex sorting logic based on different properties of objects.

Conclusion

Comparator is a generic interface similar to Comparable, which can define rules for comparing Java objects. It has a single abstract method and therefore is a functional interface that allows you to utilize lambda functions to override its compare method.

In contrast to the Comparable interface, Comparator can be used to compare objects of a class that does not implement any default ordering. In addition, Comparator provides several non-abstract methods that you can employ to combine different Comparator objects or extract Comparable sort keys.

Leave a Reply

Your email address will not be published.