The Comparator interface in Java provides a mechanism to define custom orderings for collections of objects, beyond their natural ordering. This becomes essential when you want to sort or order a collection based on specific attributes or fields of objects.

Key Points:

Functional Interface: Comparator is a functional interface, meaning it can be used with lambda expressions.

Comparison: The main method in the Comparator interface is compare(T o1, T o2). It returns

  • a negative integer if o1 is less than o2
  • zero if o1 is equal to o2
  • a positive integer if o1 is greater than o2

Nulls: The Comparator interface provides a nullsFirst() and nullsLast() method to handle null values in a specific order.

Chaining: You can chain multiple comparators using the thenComparing() method to sort by primary, secondary, etc., criteria.

Reverse Order: The Comparator interface provides a reversed() method to reverse the current sorting sequence.

Type-specific Comparators: Java provides type-specific comparators like Integer::compare for primitives, which are more efficient than generic ones.

Use with Collections: The Collections.sort() and Arrays.sort() methods, among others, can accept a Comparator to define the ordering of the elements.

Java Comparator Interface – Important Methods

import java.util.Arrays;
import java.util.Comparator;

class Student {
    String name;
    int age;

    Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

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

public class ComparatorMethodsDemo {
    public static void main(String[] args) {
        Student[] students = {
            new Student("Rohan", 20),
            new Student("Amit", 22),
            new Student("Pooja", 19)
        };

        // 1. Using comparing() method
        Arrays.sort(students, Comparator.comparing(Student::getAge));
        System.out.println("Students sorted by age: " + Arrays.toString(students));

        // 2. Using thenComparing() method
        Arrays.sort(students, Comparator.comparing(Student::getAge).thenComparing(Student::getName));
        System.out.println("Students sorted by age, then name: " + Arrays.toString(students));

        // 3. Using reversed() method
        Arrays.sort(students, Comparator.comparing(Student::getAge).reversed());
        System.out.println("Students sorted by age in reverse: " + Arrays.toString(students));

        // 4. Using naturalOrder() method
        String[] fruits = {"Apple", "Mango", "Banana"};
        Arrays.sort(fruits, Comparator.naturalOrder());
        System.out.println("Fruits in natural order: " + Arrays.toString(fruits));
    }
}

Output:

Students sorted by age: [Pooja (19), Rohan (20), Amit (22)]
Students sorted by age, then name: [Pooja (19), Rohan (20), Amit (22)]
Students sorted by age in reverse: [Amit (22), Rohan (20), Pooja (19)]
Fruits in natural order: [Apple, Banana, Mango]

Explanation:

1. comparing(): This method accepts a function that extracts a key to be compared. Here, we're extracting the age of students.

2. thenComparing(): After the primary comparison (in our case by age), this method performs a secondary comparison (by name here) if the primary comparison results in a tie.

3. reversed(): It reverses the order of the elements based on the previous comparison criteria.

4. naturalOrder(): This method returns a comparator that imposes the natural ordering of the elements. It's useful when the elements themselves implement Comparable, as String does.

Comparator provides a set of utility methods that allow us to create and combine comparators in a more readable and flexible way.

Java Comparator Interface with Lambda Expressions

import java.util.Arrays;
import java.util.Comparator;

class Student {
    String name;
    int age;

    Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

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

public class ComparatorLambdaDemo {
    public static void main(String[] args) {
        Student[] students = {
            new Student("Rohan", 20),
            new Student("Amit", 22),
            new Student("Pooja", 19)
        };

        // 1. Sorting by age using lambda
        Arrays.sort(students, (s1, s2) -> s1.age - s2.age);
        System.out.println("Students sorted by age: " + Arrays.toString(students));

        // 2. Sorting by name using lambda
        Arrays.sort(students, (s1, s2) -> s1.name.compareTo(s2.name));
        System.out.println("Students sorted by name: " + Arrays.toString(students));

        // 3. Sorting by age in descending order using lambda
        Arrays.sort(students, (s1, s2) -> s2.age - s1.age);
        System.out.println("Students sorted by age in reverse: " + Arrays.toString(students));
    }
}

Output:

Students sorted by age: [Pooja (19), Rohan (20), Amit (22)]
Students sorted by name: [Amit (22), Pooja (19), Rohan (20)]
Students sorted by age in reverse: [Amit (22), Rohan (20), Pooja (19)]

Explanation:

1. Sorting by age: We are using a lambda expression (s1, s2) -> s1.age – s2.age to compare the age of two student objects. If the result is negative, s1 is younger than s2, if positive s1 is older, and if zero, they are of the same age.

2. Sorting by name: The lambda (s1, s2) -> s1.name.compareTo(s2.name) leverages the compareTo method from the String class to compare the names alphabetically.

3. Sorting by age in descending order: The lambda (s1, s2) -> s2.age – s1.age sorts the students by age in descending order. Notice how the order of subtraction is reversed compared to the ascending order comparator.

Lambda expressions provide a concise and readable way to implement Comparator logic without the need for anonymous inner classes.

Reversing Order using Java’s Comparator Interface

import java.util.Arrays;
import java.util.Comparator;

class Student {
    String name;
    int age;

    Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

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

public class ReverseOrderComparatorDemo {
    public static void main(String[] args) {
        Student[] students = {
            new Student("Rohan", 20),
            new Student("Amit", 22),
            new Student("Pooja", 19)
        };

        // 1. Reversing natural order using Comparator
        Arrays.sort(students, Comparator.reverseOrder());
        System.out.println("Students in reverse natural order: " + Arrays.toString(students));

        // 2. Reversing custom order (by age) using Comparator
        Comparator<Student> ageComparator = Comparator.comparingInt(Student::getAge);
        Arrays.sort(students, ageComparator.reversed());
        System.out.println("Students sorted by age in reverse: " + Arrays.toString(students));
    }
}

Output:

Students in reverse natural order: [Rohan (20), Pooja (19), Amit (22)]
Students sorted by age in reverse: [Amit (22), Rohan (20), Pooja (19)]

Explanation:

1. Reversing natural order: The method Comparator.reverseOrder() returns a comparator that imposes the reverse of the natural ordering on a collection of objects that implement the Comparable interface. Here, since Student doesn't implement Comparable, the output is unpredictable.

2. Reversing custom order: First, we create a custom comparator ageComparator to sort students by their age. We then use the reversed() method on this comparator to get a new comparator that sorts students in descending order of age.

Using Comparator, we can easily reverse the sorting order, either by leveraging the natural ordering of elements or by applying custom sorting logic.

Sorting Custom Objects using Java’s Comparator Interface

import java.util.Arrays;
import java.util.Comparator;

// Define a custom object: Student
class Student {
    String name;
    int marks;

    Student(String name, int marks) {
        this.name = name;
        this.marks = marks;
    }

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

public class CustomObjectSorting {
    public static void main(String[] args) {
        Student[] students = {
            new Student("Rohan", 85),
            new Student("Amit", 90),
            new Student("Pooja", 88)
        };

        // 1. Sorting by name using Comparator
        Arrays.sort(students, Comparator.comparing(Student::getName));
        System.out.println("Students sorted by name: " + Arrays.toString(students));

        // 2. Sorting by marks using Comparator
        Arrays.sort(students, Comparator.comparingInt(Student::getMarks));
        System.out.println("Students sorted by marks: " + Arrays.toString(students));

        // 3. Sorting by marks in descending order using Comparator
        Arrays.sort(students, Comparator.comparingInt(Student::getMarks).reversed());
        System.out.println("Students sorted by marks in descending order: " + Arrays.toString(students));
    }
}

Output:

Students sorted by name: [Amit (90), Pooja (88), Rohan (85)]
Students sorted by marks: [Rohan (85), Pooja (88), Amit (90)]
Students sorted by marks in descending order: [Amit (90), Pooja (88), Rohan (85)]

Explanation:

1. Sorting by name: The method Comparator.comparing(Student::getName) provides a comparator that sorts the Student objects based on their names alphabetically.

2. Sorting by marks: The method Comparator.comparingInt(Student::getMarks) provides a comparator that sorts Student objects based on their marks in ascending order.

3. Sorting by marks in descending order: By chaining the reversed() method to the above comparator, we achieve sorting in descending order of marks.

Using Comparator, we can define custom sorting logic for our objects without the need for them to implement the Comparable interface.

Sorting Custom Objects using Java’s Comparator Interface with Streams

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

// Define a custom object: Student
class Student {
    String name;
    int marks;

    Student(String name, int marks) {
        this.name = name;
        this.marks = marks;
    }

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

public class ComparatorWithStreams {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
            new Student("Rohan", 85),
            new Student("Amit", 90),
            new Student("Pooja", 88)
        );

        // 1. Sorting by name using Streams and Comparator
        List<Student> sortedByName = students.stream()
            .sorted(Comparator.comparing(Student::getName))
            .collect(Collectors.toList());
        System.out.println("Students sorted by name: " + sortedByName);

        // 2. Sorting by marks using Streams and Comparator
        List<Student> sortedByMarks = students.stream()
            .sorted(Comparator.comparingInt(Student::getMarks))
            .collect(Collectors.toList());
        System.out.println("Students sorted by marks: " + sortedByMarks);

        // 3. Sorting by marks in descending order using Streams and Comparator
        List<Student> sortedByMarksDesc = students.stream()
            .sorted(Comparator.comparingInt(Student::getMarks).reversed())
            .collect(Collectors.toList());
        System.out.println("Students sorted by marks in descending order: " + sortedByMarksDesc);
    }
}

Output:

Students sorted by name: [Amit (90), Pooja (88), Rohan (85)]
Students sorted by marks: [Rohan (85), Pooja (88), Amit (90)]
Students sorted by marks in descending order: [Amit (90), Pooja (88), Rohan (85)]

Explanation:

1. Sorting by name with Streams: We use the stream() method to convert the list to a stream. The sorted() method is then used with Comparator.comparing(Student::getName) to sort students based on their names. Finally, collect() converts the stream back to a list.

2. Sorting by marks with Streams: Similar to the first example, we use the Comparator.comparingInt(Student::getMarks) to sort students based on their marks in ascending order.

3. Sorting by marks in descending order with Streams: By chaining the reversed() method to the marks comparator inside the sorted() method, we achieve sorting in descending order of marks.

Using Comparator with streams, we can perform sorting operations seamlessly and enjoy the functional programming style that Java streams offer.

Case-Insensitive String Sorting using Java’s Comparator Interface

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class CaseInsensitiveSorting {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Rohan", "amit", "POOJA", "aMiT", "RohaN");

        // Sorting in case-insensitive order using Comparator
        List<String> sortedNames = names.stream()
            .sorted(String.CASE_INSENSITIVE_ORDER)
            .collect(Collectors.toList());

        System.out.println("Names sorted in case-insensitive order: " + sortedNames);
    }
}

Output:

Names sorted in case-insensitive order: [amit, aMiT, POOJA, Rohan, RohaN]

Explanation:

1. String.CASE_INSENSITIVE_ORDER: Java provides a built-in Comparator for case-insensitive string sorting. This comparator uses String.compareToIgnoreCase internally to compare strings without considering their case.

2. We utilize the Java streams to sort the list of names. The stream() method is used to convert the list into a stream. The sorted() method, with String.CASE_INSENSITIVE_ORDER as an argument, sorts the strings without considering their case. Finally, the collect() method aggregates the sorted stream back into a list.

By using the built-in CASE_INSENSITIVE_ORDER comparator, we can easily achieve case-insensitive sorting for strings in Java.