Java Comparator Example: A Comprehensive Guide to Sorting Objects

In Java, sorting objects is a common task, especially when dealing with collections of custom objects. The Comparator interface in Java is a powerful tool that allows you to define custom sorting logic for objects of user-defined classes. This guide will explore the Comparator interface with practical examples, demonstrating how to sort objects based on different criteria.

The Comparator interface is part of the java.util package and provides a way to compare two objects of the same class. This is particularly useful when the natural ordering of objects (defined by Comparable) is not sufficient, or when you need to sort objects in different ways.

Understanding the Comparator Interface

The core of the Comparator interface lies in its compare(Object obj1, Object obj2) method. This method dictates how two objects, obj1 and obj2, are compared for sorting purposes.

Syntax:

public int compare(Object obj1, Object obj2);

The compare() method returns an integer value based on the comparison between obj1 and obj2:

  • Negative integer: If obj1 is less than obj2.
  • Zero: If obj1 is equal to obj2.
  • Positive integer: If obj1 is greater than obj2.

This return value is crucial for sorting algorithms like Collections.sort() to determine the order of objects in a collection.

Methods to Implement Comparator

There are a couple of primary ways to implement the Comparator interface to sort objects in Java. Let’s explore these methods with examples.

Method 1: Implementing Comparator using a Separate Class

One common approach is to create a separate class that implements the Comparator interface. This class will contain the logic for comparing objects of your custom class.

Example: Sorting Student Objects by Roll Number

Let’s consider a Student class with rollno and name as attributes. We want to sort a list of Student objects based on their roll numbers.

// Define the Student class
class Student {
    int rollno;
    String name;

    // Constructor
    Student(int rollno, String name) {
        this.rollno = rollno;
        this.name = name;
    }

    // Method to print Student details
    @Override
    public String toString() {
        return rollno + ": " + name;
    }
}

// Helper class implementing Comparator interface
class SortbyRoll implements Comparator<Student> {
    // Compare by roll number in ascending order
    public int compare(Student a, Student b) {
        return a.rollno - b.rollno;
    }
}

public class GFG {
    public static void main(String[] args) {
        // List of Students
        List<Student> students = new ArrayList<>();

        // Add Elements in List
        students.add(new Student(111, "Mayank"));
        students.add(new Student(131, "Anshul"));
        students.add(new Student(121, "Solanki"));
        students.add(new Student(101, "Aggarwal"));

        // Sort students by roll number
        // using SortbyRoll comparator
        Collections.sort(students, new SortbyRoll());

        System.out.println("Sorted by Roll Number");
        // Iterating over entries to print them
        for (Student student : students) {
            System.out.println(student);
        }
    }
}

Output:

Sorted by Roll Number
101: Aggarwal
111: Mayank
121: Solanki
131: Anshul

In this example, SortbyRoll is a separate class that implements Comparator<Student>. The compare() method within SortbyRoll compares the rollno of two Student objects. Collections.sort(students, new SortbyRoll()) then uses this comparator to sort the students list.

Method 2: Implementing Comparator using Lambda Expressions

Java 8 introduced lambda expressions, providing a more concise way to implement functional interfaces like Comparator. Lambda expressions can simplify comparator implementation, especially for simple comparison logic.

Example: Sorting Student Objects by Roll Number using Lambda

We can achieve the same sorting by roll number using a lambda expression directly within the sort() method.

// Define the Student class (same as above)
class Student {
    int rollno;
    String name;

    // Constructor and toString() method (same as above)
    Student(int rollno, String name) {
        this.rollno = rollno;
        this.name = name;
    }

    @Override
    public String toString() {
        return rollno + ": " + name;
    }
}


public class LambdaComparatorExample {
    public static void main(String[] args) {
        // List of Students
        List<Student> students = new ArrayList<>();

        // Add Elements in List (same as above)
        students.add(new Student(111, "Mayank"));
        students.add(new Student(131, "Anshul"));
        students.add(new Student(121, "Solanki"));
        students.add(new Student(101, "Aggarwal"));

        // Sort students by roll number using lambda
        students.sort((Student a, Student b) -> a.rollno - b.rollno);

        System.out.println("Sorted by Roll Number (Lambda)");
        // Iterating over entries to print them
        for (Student student : students) {
            System.out.println(student);
        }
    }
}

Output:

Sorted by Roll Number (Lambda)
101: Aggarwal
111: Mayank
121: Solanki
131: Anshul

Here, students.sort((Student a, Student b) -> a.rollno - b.rollno) directly provides the comparison logic as a lambda expression. This approach is more compact and often preferred for simple comparators.

Sorting Collection by More Than One Field

The power of Comparator truly shines when you need to sort collections based on multiple fields. You can define a comparator that considers multiple attributes in a specific order to determine the object ordering.

Example: Sorting Student Objects by Name then Age

Let’s extend our Student class to include name and age. We want to sort students primarily by name (alphabetically) and then by age for students with the same name.

// Define the Student class
class Student {
    String name;
    Integer age;

    // Constructor
    Student(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public Integer getAge() {
        return age;
    }

    // Method to print student details
    @Override
    public String toString() {
        return name + " : " + age;
    }
}

// Comparator in a Helper Class
class CustomerSortingComparator implements Comparator<Student> {
    // Compare first by name, then by age
    public int compare(Student student1, Student student2) {
        // Compare by name first
        int nameCompare = student1.getName().compareTo(student2.getName());

        // If names are the same, compare by age
        int ageCompare = student1.getAge().compareTo(student2.getAge());

        // Return the result: first by name, second by age
        return (nameCompare == 0) ? ageCompare : nameCompare;
    }
}

public class ComparatorHelperClassExample {
    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        students.add(new Student("Ajay", 27));
        students.add(new Student("Sneha", 23));
        students.add(new Student("Simran", 37));
        students.add(new Student("Ankit", 22));
        students.add(new Student("Anshul", 29));
        students.add(new Student("Sneha", 22));

        // Original List
        System.out.println("Original List");
        for (Student it : students) {
            System.out.println(it);
        }
        System.out.println();

        // Sort students by name, then by age
        // using the CustomerSortingComparator
        Collections.sort(students, new CustomerSortingComparator());

        // Display message only
        System.out.println("After Sorting");
        // Iterating using enhanced for-loop
        // after Sorting ArrayList
        for (Student it : students) {
            System.out.println(it);
        }
    }
}

Output:

Original List
Ajay : 27
Sneha : 23
Simran : 37
Ankit : 22
Anshul : 29
Sneha : 22

After Sorting
Ajay : 27
Ankit : 22
Anshul : 29
Simran : 37
Sneha : 22
Sneha : 23

In CustomerSortingComparator, we first compare students by name using compareTo(). If the names are the same (nameCompare == 0), we then compare them by age. This ensures a secondary sorting criterion.

Alternative Method: Chaining Comparators with thenComparing()

Java 8 and later versions offer a more elegant way to handle multi-field sorting using the thenComparing() method of the Comparator interface. This allows you to chain comparators for different fields in a readable and concise manner.

Example: Sorting Student Objects by Name then Age using thenComparing()

// Define the Student class (same as above)
class Student {
    String name;
    Integer age;

    // Constructor, getName(), getAge(), and toString() methods (same as above)
    Student(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public Integer getAge() {
        return age;
    }

    @Override
    public String toString() {
        return name + " : " + age;
    }
}


public class ComparatorThenComparingExample {
    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        students.add(new Student("Ajay", 27));
        students.add(new Student("Sneha", 23));
        students.add(new Student("Simran", 37));
        students.add(new Student("Ankit", 22));
        students.add(new Student("Anshul", 29));
        students.add(new Student("Sneha", 22));

        // Original List
        System.out.println("Original List:");
        for (Student it : students) {
            System.out.println(it);
        }
        System.out.println();

        // Sort students by name, then by age
        students.sort(Comparator.comparing(Student::getName).thenComparing(Student::getAge));

        // Display message after sorting
        System.out.println("After Sorting:");
        for (Student it : students) {
            System.out.println(it);
        }
    }
}

Output:

Original List:
Ajay : 27
Sneha : 23
Simran : 37
Ankit : 22
Anshul : 29
Sneha : 22

After Sorting:
Ajay : 27
Ankit : 22
Anshul : 29
Simran : 37
Sneha : 22
Sneha : 23

Comparator.comparing(Student::getName).thenComparing(Student::getAge) creates a comparator that first compares by name using comparing(Student::getName) and then, for elements with the same name, uses thenComparing(Student::getAge) to compare by age. This method is clean, readable, and leverages method references for conciseness.

Comparator vs Comparable

While both Comparator and Comparable are used for sorting in Java, they serve different purposes and have key distinctions.

Feature Comparator Comparable
Sorting Logic Location Defined externally (separate class or lambda) Defined within the class itself
Multiple Sorting Orders Supported (easily create different comparators) Not directly supported (single natural order)
Interface Methods compare(obj1, obj2) compareTo(anotherObject)
Functional Interface Yes No
Usage Flexible, reusable, for external sorting Simple, for defining natural ordering

In essence, use Comparable when you want to define a natural, default sorting order for your class. Use Comparator when you need more flexibility, want to sort based on different criteria, or when you can’t modify the class itself (e.g., sorting objects from a library).

Conclusion

The Java Comparator interface is an essential tool for sorting objects in Java. It provides the flexibility to define custom sorting logic based on various criteria, making it invaluable for complex sorting requirements. Whether you choose to implement comparators using separate classes or concise lambda expressions, understanding Comparator is crucial for effective object sorting in Java applications.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

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