Learnitweb

Multi-Level Sorting using Comparator in Java

1. Introduction

Sorting is one of the most common operations in programming, especially when working with collections of objects such as lists of employees, products, or students.

In Java, sorting objects based on multiple fields (or multi-level sorting) is often required when one attribute alone is not sufficient to determine order.

For example:

  • Sort employees first by department name, then by salary, and finally by name if salaries are equal.
  • Sort students first by grade, then by marks, and then by name.

This kind of hierarchical sorting is called multi-level sorting or chained comparison, and it can be efficiently implemented using the Comparator interface in Java.


2. Background: Comparator in Java

Before we dive into multi-level sorting, let’s review what a Comparator is.

The Comparator interface in Java is used to define custom sorting logic for objects that do not implement Comparable, or when you need sorting logic different from the natural order.

The interface looks like this:

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}
  • It returns a negative integer if o1 should come before o2.
  • It returns zero if o1 and o2 are equal.
  • It returns a positive integer if o1 should come after o2.

3. Single-Level Sorting Example

Let’s begin with a simple example. Suppose we have an Employee class.

class Employee {
    private String name;
    private String department;
    private double salary;

    public Employee(String name, String department, double salary) {
        this.name = name;
        this.department = department;
        this.salary = salary;
    }

    public String getName() { return name; }
    public String getDepartment() { return department; }
    public double getSalary() { return salary; }

    @Override
    public String toString() {
        return name + " | " + department + " | " + salary;
    }
}

Now, suppose you want to sort a list of employees by salary:

List<Employee> employees = Arrays.asList(
    new Employee("Alice", "HR", 50000),
    new Employee("Bob", "Finance", 70000),
    new Employee("Charlie", "HR", 40000)
);

// Sort by salary
employees.sort(Comparator.comparing(Employee::getSalary));

employees.forEach(System.out::println);

Output:

Charlie | HR | 40000.0
Alice | HR | 50000.0
Bob | Finance | 70000.0

Here, the list is sorted in ascending order of salary.


4. Multi-Level Sorting: Concept and Need

Now imagine two employees have the same salary. You might want to sort them by name in that case.

For this, we perform multi-level sorting, meaning:

  1. Sort primarily by one field (e.g., department),
  2. If equal, sort by another (e.g., salary),
  3. If still equal, sort by yet another (e.g., name).

This can be done by chaining multiple comparators together.


5. Multi-Level Sorting using Chained Comparators

Approach 1: Using Comparator.thenComparing()

The Java 8+ Comparator API makes this extremely easy with method chaining.

Comparator<Employee> multiLevelComparator =
    Comparator.comparing(Employee::getDepartment)
              .thenComparing(Employee::getSalary)
              .thenComparing(Employee::getName);

employees.sort(multiLevelComparator);

Explanation:

  1. First, employees are sorted by department (primary key).
  2. If two employees have the same department, they are then sorted by salary (secondary key).
  3. If both department and salary are the same, they are sorted by name (tertiary key).

Example Data:

List<Employee> employees = Arrays.asList(
    new Employee("Alice", "HR", 50000),
    new Employee("Bob", "Finance", 70000),
    new Employee("Charlie", "HR", 50000),
    new Employee("David", "Finance", 70000),
    new Employee("Eve", "Finance", 90000)
);

Code:

employees.sort(
    Comparator.comparing(Employee::getDepartment)
              .thenComparing(Employee::getSalary)
              .thenComparing(Employee::getName)
);

employees.forEach(System.out::println);

Output:

Bob | Finance | 70000.0
David | Finance | 70000.0
Eve | Finance | 90000.0
Alice | HR | 50000.0
Charlie | HR | 50000.0

You can see:

  • Employees are first grouped by department (Finance before HR),
  • Within each department, sorted by salary,
  • Within the same salary group, sorted by name.

Approach 2: Reversing Order for Specific Fields

By default, sorting is in ascending order.
You can reverse order for specific levels using Comparator.reversed() or Comparator.comparing(..., Comparator.reverseOrder()).

Example:
Sort by:

  1. Department (ascending),
  2. Salary (descending),
  3. Name (ascending).
employees.sort(
    Comparator.comparing(Employee::getDepartment)
              .thenComparing(Employee::getSalary, Comparator.reverseOrder())
              .thenComparing(Employee::getName)
);

Output:

Eve | Finance | 90000.0
Bob | Finance | 70000.0
David | Finance | 70000.0
Alice | HR | 50000.0
Charlie | HR | 50000.0

Notice that for the Finance department, higher salaries appear first (because we used reverseOrder() for salary).


Approach 3: Custom Comparator Logic (Pre-Java 8 Style)

Before Java 8, multi-level sorting was done by explicitly writing the logic in a single compare() method.

Comparator<Employee> multiComparator = new Comparator<Employee>() {
    @Override
    public int compare(Employee e1, Employee e2) {
        int deptCompare = e1.getDepartment().compareTo(e2.getDepartment());
        if (deptCompare != 0)
            return deptCompare;

        int salaryCompare = Double.compare(e1.getSalary(), e2.getSalary());
        if (salaryCompare != 0)
            return salaryCompare;

        return e1.getName().compareTo(e2.getName());
    }
};

You can then sort:

Collections.sort(employees, multiComparator);

This approach is more verbose but demonstrates the manual control you have over the sorting logic.


6. Handling Null Values in Multi-Level Sorting

If any of the fields might be null, use Comparator.nullsFirst() or Comparator.nullsLast() to handle them gracefully.

Example:

Comparator<Employee> comparator = 
    Comparator.comparing(Employee::getDepartment, Comparator.nullsFirst(String::compareTo))
              .thenComparing(Employee::getSalary)
              .thenComparing(Employee::getName);

This ensures:

  • Null departments appear first.
  • Sorting continues by salary and name as usual.

7. Example with Mixed Data Types

Suppose your sorting criteria mix types — for example:

  1. Sort by department (String),
  2. Then by salary (double),
  3. Then by joiningDate (LocalDate).

You can still chain comparators easily:

Comparator<Employee> mixedComparator = 
    Comparator.comparing(Employee::getDepartment)
              .thenComparing(Employee::getSalary)
              .thenComparing(Employee::getJoiningDate);

Java handles type-safe comparison using each field’s natural order or a custom comparator.


8. Advantages of Using Comparator for Multi-Level Sorting

  1. Flexibility:
    • You can define multiple sorting levels dynamically.
    • You can easily switch between ascending and descending orders.
  2. Readability:
    • Method chaining (thenComparing()) makes code clean and expressive.
  3. Immutability:
    • Comparators are stateless and reusable.
  4. Customizability:
    • You can provide your own custom comparison logic for complex fields (e.g., nested objects).
  5. Java 8 Stream Integration:
    • Works seamlessly with stream().sorted(comparator) for sorting streams of data.

9. Disadvantages or Limitations

  1. Performance Overhead:
    • Sorting using multiple comparators can be slower for large datasets compared to single-field sorting.
  2. Complex Logic for Deep Nesting:
    • When objects have nested fields (like employee.getAddress().getCity()), the comparator chain can become verbose.
  3. Potential Null Issues:
    • If fields can be null and are not handled, sorting may throw NullPointerException.
  4. Comparator Complexity:
    • Multi-level comparators can become hard to maintain if too many conditions are added.