Comparable vs Comparator Explained in Java

Comparable vs Comparator Explained in Java

Learn How to Implement Custom Sorting for Your Java Objects Using Comparable and Comparator Interfaces

Sorting is a fundamental operation in programming, essential for organizing data in a specific order. In Java, built-in sorting methods provide efficient ways to sort primitive data types and arrays, making it easy to manage and manipulate collections of data. For instance, you can quickly sort an array of integers or a list of strings using methods like Arrays.sort() and Collections.sort().

However, when it comes to sorting custom objects, such as instances of user-defined classes, the built-in sorting methods fall short. These methods don't know how to order objects based on custom criteria. This is where Java's Comparable and Comparator interfaces come into play, allowing developers to define and implement custom sorting logic tailored to specific requirements.

In this blog post, we'll explore how to use the Comparable and Comparator interfaces to sort custom objects in Java. I'll provide examples to illustrate the differences and use cases for each approach, helping you master custom sorting in your Java applications.

Sorting Methods for Primitive Types

Java provides a variety of built-in sorting methods that make it easy to sort primitive data types. These methods are highly optimized and efficient, allowing you to sort arrays and collections with minimal code. For primitive types, such as integers, floating-point numbers, and characters, the Arrays.sort() method is commonly used.

Arrays.sort()

The Arrays.sort() method sorts the specified array into ascending numerical order. This method uses a Dual-Pivot Quicksort algorithm, which is faster and more efficient for most data sets.

Let's look at an example of sorting an array of integers and characters using Arrays.sort():

package tutorial;

import java.util.Arrays;

public class PrimitiveSorting {
    public static void main(String[] args) {
        int[] numbers = { 5, 3, 8, 2, 1 };
        System.out.println("Original array: " + Arrays.toString(numbers));

        Arrays.sort(numbers);
        System.out.println("Sorted array: " + Arrays.toString(numbers));

        char[] characters = { 'o', 'i', 'e', 'u', 'a' };
        System.out.println("Original array: " + Arrays.toString(characters));

        Arrays.sort(characters);
        System.out.println("Sorted array: " + Arrays.toString(characters));
    }
}

Output:

Original array: [5, 3, 8, 2, 1]
Sorted array: [1, 2, 3, 5, 8]
Original array: [o, i, e, u, a]
Sorted array: [a, e, i, o, u]

Collections.sort()

The Collections.sort() method is used to sort collections such as ArrayList. This method is also based on the natural ordering of the elements or a custom comparator.

package tutorial;

import java.util.ArrayList;
import java.util.Collections;

public class CollectionsSorting {
    public static void main(String[] args) {
        ArrayList<String> wordsList = new ArrayList<>();
        wordsList.add("banana");
        wordsList.add("apple");
        wordsList.add("cherry");
        wordsList.add("date");
        System.out.println("Original list: " + wordsList);

        Collections.sort(wordsList);
        System.out.println("Sorted list: " + wordsList);
    }
}

Output:

Original list: [banana, apple, cherry, date]
Sorted list: [apple, banana, cherry, date]

Limitations with Custom Classes

While Java's built-in sorting methods, such as Arrays.sort() and Collections.sort(), are powerful and efficient for sorting primitive types and objects with natural ordering (like String), they fall short when it comes to sorting custom objects. These methods do not inherently know how to order user-defined objects because there is no natural way for them to compare these objects.

For example, consider a simple Person class that has name, age, and weight attributes:

package tutorial;

public class Person {
    String name;
    int age;
    double weight;

    public Person(String name, int age, double weight) {
        this.name = name;
        this.age = age;
        this.weight = weight;
    }

    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + ", weight=" + weight + " kgs]";
    }
}

If we try to sort a list of Person objects using Arrays.sort() or Collections.sort(), we will encounter a compilation error because these methods do not know how to compare Person objects:

package tutorial;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class CustomClassSorting {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>(Arrays.asList(
                new Person("Alice", 30, 65.5),
                new Person("Bob", 25, 75.0),
                new Person("Charlie", 35, 80.0)
        ));
        System.out.println("Original people list: " + people);

        Collections.sort(people);
        System.out.println("Sorted people list: " + people);
    }
}

Compilation Error:

java: no suitable method found for sort(java.util.List<tutorial.Person>)
    method java.util.Collections.<T>sort(java.util.List<T>) is not applicable
      (inference variable T has incompatible bounds
        equality constraints: tutorial.Person
        lower bounds: java.lang.Comparable<? super T>)
    method java.util.Collections.<T>sort(java.util.List<T>,java.util.Comparator<? super T>) is not applicable
      (cannot infer type-variable(s) T
        (actual and formal argument lists differ in length))

The error occurs because the Person class does not implement the Comparable interface, and there is no way for the sorting method to know how to compare two Person objects.

To sort custom objects like Person, we need to provide a way to compare these objects. Java offers two main approaches to achieve this:

  1. Implementing the Comparable Interface: This allows a class to define its natural ordering by implementing the compareTo method.

  2. Using the Comparator Interface: This allows us to create separate classes or lambda expressions to define multiple ways of comparing objects.

We will explore both approaches in the upcoming sections, starting with the Comparable interface.

Comparable Interface

Java provides a Comparable interface to define a natural ordering for objects of a user-defined class. By implementing the Comparable interface, a class can provide a single natural ordering that can be used to sort its instances. This is particularly useful when you need a default way to compare and sort objects.

Overview

The Comparable interface contains a single method, compareTo(), which compares the current object with the specified object for order. The method returns:

  • A negative integer if the current object is less than the specified object.

  • Zero if the current object is equal to the specified object.

  • A positive integer if the current object is greater than the specified object.

How Comparable Allows for a Single Natural Ordering of Objects

By implementing the Comparable interface, a class can ensure that its objects have a natural ordering. This allows the objects to be sorted using methods like Arrays.sort() or Collections.sort() without the need for a separate comparator.

Let's implement the Comparable interface in a new PersonV2 class, comparing by age.

package tutorial;

public class PersonV2 implements Comparable<PersonV2> {
    String name;
    int age;
    double weight;

    public PersonV2(String name, int age, double weight) {
        this.name = name;
        this.age = age;
        this.weight = weight;
    }

    @Override
    public String toString() {
        return "PersonV2 [name=" + name + ", age=" + age + ", weight=" + weight + " kgs]";
    }

    @Override
    public int compareTo(PersonV2 other) {
        return this.age - other.age;
    }
}

In this implementation, the compareTo() method compares the age attribute of the current PersonV2 object with the age attribute of the specified PersonV2 object by subtracting one age from the other. By using the expression this.age - other.age, we’re effectively implementing this logic as follows:

  • If this.age is less than other.age, the result will be negative.

  • If this.age is equal to other.age, the result will be zero.

  • If this.age is greater than other.age, the result will be positive.

Note: We can also use Integer.compare(this.age, other.age) instead of performing the arithmetic operation manually.

Now that the PersonV2 class implements the Comparable interface, we can sort a list of PersonV2 objects using Collections.sort():

package tutorial;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class CustomClassSortingV2 {
    public static void main(String[] args) {
        List<PersonV2> people = new ArrayList<>(Arrays.asList(
                new PersonV2("Alice", 30, 65.5),
                new PersonV2("Bob", 25, 75.0),
                new PersonV2("Charlie", 35, 80.0)
        ));
        System.out.println("Original people list: " + people);

        Collections.sort(people);
        System.out.println("Sorted people list: " + people);
    }
}

Output:

Original people list: [PersonV2 [name=Alice, age=30, weight=65.5 kgs], PersonV2 [name=Bob, age=25, weight=75.0 kgs], PersonV2 [name=Charlie, age=35, weight=80.0 kgs]]
Sorted people list: [PersonV2 [name=Bob, age=25, weight=75.0 kgs], PersonV2 [name=Alice, age=30, weight=65.5 kgs], PersonV2 [name=Charlie, age=35, weight=80.0 kgs]]

In this example, the PersonV2 objects are sorted in ascending order of age using the Collections.sort() method, which relies on the natural ordering defined by the compareTo() method in the PersonV2 class.

Limitations of Comparable

While the Comparable interface provides a way to define a natural ordering for objects, it has several limitations that can restrict its use in practical applications. Understanding these limitations can help us determine when to use other mechanisms, such as the Comparator interface, to achieve more flexible sorting.

  • Single Natural Ordering: The primary limitation of Comparable is that it allows only one natural ordering for the objects of a class. When you implement Comparable, you define a single way to compare objects, which is used whenever the objects are sorted or compared. This can be restrictive if you need to sort objects in multiple ways.

  • Inflexibility: If you need to sort objects by different attributes or in different orders, you will have to modify the class or create new implementations of Comparable. This inflexibility can lead to a proliferation of comparison methods and can make the code harder to maintain.

  • Non-Adaptable: Once a class implements Comparable, the natural ordering is fixed and cannot be easily changed. For instance, if your PersonV2 class initially sorts by age but later you need to sort by weight or name, you have to either change the compareTo() method or create a new version of the class.

This is where the Comparator interface comes into play. To define multiple ways of comparing objects, we can use the Comparator interface, which we will explore in the next section.

Comparator Interface

The Comparator interface in Java provides a way to define multiple ways to compare and sort objects. Unlike the Comparable interface, which allows only a single natural ordering, Comparator is designed to offer flexibility by allowing multiple sorting strategies. This makes it particularly useful for scenarios where objects need to be sorted in different ways.

Overview

The Comparator interface defines a single method, compare(), which compares two objects and returns:

  • A negative integer if the first object is less than the second object.

  • Zero if the first object is equal to the second object.

  • A positive integer if the first object is greater than the second object.

This method provides a way to define custom ordering for objects without modifying the class itself.

How Comparator Allows for Multiple Ways of Ordering Objects

The Comparator interface allows you to create multiple Comparator instances, each defining a different ordering for objects. This flexibility means you can sort objects by various attributes or in different orders without altering the object's class.

Let's implement multiple Comparator instances for the Person class. We'll define comparators for sorting by name, by age, and by weight. First, we need to update the Person class to include getters and ensure that attributes are accessible.

package tutorial;

public class Person {
    String name;
    int age;
    double weight;

    public Person(String name, int age, double weight) {
        this.name = name;
        this.age = age;
        this.weight = weight;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public double getWeight() {
        return weight;
    }

    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + ", weight=" + weight + " kgs]";
    }
}

Comparator by Name

This comparator sorts Person objects alphabetically by their name.

package tutorial.comparator;

import tutorial.Person;

import java.util.Comparator;

public class PersonNameComparator implements Comparator<Person> {

    @Override
    public int compare(Person p1, Person p2) {
        return p1.getName().compareTo(p2.getName());
    }
}

Comparator by Age

This comparator sorts Person objects by their age, in ascending order.

package tutorial.comparator;

import tutorial.Person;

import java.util.Comparator;

public class PersonAgeComparator implements Comparator<Person> {

    @Override
    public int compare(Person p1, Person p2) {
        return p1.getAge() - p2.getAge();
    }
}

Comparator by Weight

This comparator sorts Person objects by their weight, in ascending order.

package tutorial.comparator;

import tutorial.Person;

import java.util.Comparator;

public class PersonWeightComparator implements Comparator<Person> {

    @Override
    public int compare(Person p1, Person p2) {
        return (int) (p1.getWeight() - p2.getWeight());
    }
}

Now, here’s how you can use these Comparator instances to sort a list of Person objects:

package tutorial;

import tutorial.comparator.PersonAgeComparator;
import tutorial.comparator.PersonNameComparator;
import tutorial.comparator.PersonWeightComparator;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class CustomClassSortingV3 {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>(Arrays.asList(
                new Person("Alice", 30, 65.5),
                new Person("Bob", 25, 75.0),
                new Person("Charlie", 35, 80.0)
        ));
        System.out.println("Original people list: " + people);

        Collections.sort(people, new PersonNameComparator());
        System.out.println("Sorted people list by name: " + people);

        Collections.sort(people, new PersonAgeComparator());
        System.out.println("Sorted people list by age: " + people);

        Collections.sort(people, new PersonWeightComparator());
        System.out.println("Sorted people list by weight: " + people);
    }
}

Output:

Original people list: [Person [name=Alice, age=30, weight=65.5 kgs], Person [name=Bob, age=25, weight=75.0 kgs], Person [name=Charlie, age=35, weight=80.0 kgs]]
Sorted people list by name: [Person [name=Alice, age=30, weight=65.5 kgs], Person [name=Bob, age=25, weight=75.0 kgs], Person [name=Charlie, age=35, weight=80.0 kgs]]
Sorted people list by age: [Person [name=Bob, age=25, weight=75.0 kgs], Person [name=Alice, age=30, weight=65.5 kgs], Person [name=Charlie, age=35, weight=80.0 kgs]]
Sorted people list by weight: [Person [name=Alice, age=30, weight=65.5 kgs], Person [name=Bob, age=25, weight=75.0 kgs], Person [name=Charlie, age=35, weight=80.0 kgs]]

In this example, the Comparator instances allow sorting the Person objects by different attributes: name, age, and weight. This demonstrates how the Comparator interface enables flexible and versatile sorting strategies for a class.

Comparable vs Comparator

When sorting objects in Java, you have two primary options: the Comparable and Comparator interfaces. Understanding the differences between these two interfaces can help you choose the right approach for your needs. Please note that this is also a very important interview question.

Comparison

Here’s a table comparing and contrasting the Comparable and Comparator interfaces in Java:

FeatureComparableComparator
DefinitionProvides a single, natural ordering for objectsProvides multiple ways to compare objects
MethodcompareTo(T o)compare(T o1, T o2)
ImplementationImplemented within the class itselfImplemented outside the class
Sorting CriteriaOne default natural orderingMultiple sorting criteria
FlexibilityLimited to one way of comparing objectsFlexible; multiple comparators can be defined
Class ModificationRequires modifying the class to implement ComparableDoes not require modifying the class
Use CaseUse when there is a clear, natural ordering (e.g., sorting employees by ID)Use when different sorting orders are needed or when you cannot modify the class

Benefits and Drawbacks of Each Approach

Comparable

  • Benefits:

    • Simplicity: Provides a default sorting order that is easy to implement and use.

    • Built-in: The natural ordering is part of the class itself, so it is always available and used by default in sorting methods.

  • Drawbacks:

    • Single Ordering: Can only define one way to compare objects. If different sorting orders are needed, the class must be modified or additional Comparator instances must be used.

    • Class Modification: Requires altering the class to implement Comparable, which might not be feasible if the class is part of a library or if its natural ordering is not clear.

Comparator

  • Benefits:

    • Flexibility: Allows for multiple sorting orders and criteria, which can be defined externally and used as needed.

    • Non-invasive: Does not require modification of the class itself, making it suitable for classes you do not control or when you need different sorting options.

  • Drawbacks:

    • Complexity: Requires creating and managing multiple Comparator instances, which can add complexity to the code.

    • Overhead: Might introduce additional overhead if many comparators are used, especially if they are created on the fly.

In summary, Comparable is best used when a class has a natural ordering that makes sense for most use cases. Comparator, on the other hand, provides flexibility for sorting by multiple criteria and is useful when the class does not have a natural ordering or when different sorting orders are needed. Choosing between Comparable and Comparator depends on your specific sorting needs and whether you need a single default order or multiple flexible sorting options.

Wrapping Up

Understanding and utilizing both Comparable and Comparator can significantly enhance your ability to manage and manipulate object collections in Java. By applying these concepts, you can create more flexible and powerful sorting mechanisms.

To solidify your understanding, try implementing both Comparable and Comparator in real-world scenarios. Experiment with different classes and sorting criteria to see how each approach works in practice.

Links to Official Java Documentation:

Did you find this article valuable?

Support Ashutosh Writes by becoming a sponsor. Any amount is appreciated!