Как выполнить сортировку на основе компаратора, сохраняя исходную сортировку в Java

Я рассматривал примеры реализации интерфейса Comparable vs Comparator.

Но я застрял на одном этапе его реализации:

Предположим, у меня есть простой класс: Сотрудник, у которого есть механизм сортировки по умолчанию, основанный на имени сотрудника.

public class Employee implements Comparable<Employee> {

   private int empSalary;
   private String empName;

   @Override
   public int compareTo(Employee e) {
        return this.empName.compareTo(e.empName);
   }

}

Но, допустим, мне нужно сначала отсортировать по имени сотрудника, а затем, если у двух сотрудников одинаковое имя, я должен отсортировать их по их зарплате.

Итак, я написал собственный компаратор для сортировки по зарплате, как показано ниже.

public class SalaryComparator implements Comparator<Employee> {

      @Override
      public int compare(Employee e1, Employee e2)  {
        return e1.empSalary - e2.empSalary;
      }

}

Но когда я запустил свой тестовый класс для сортировки сначала по имени, а затем по зарплате, результат оказался не таким, как ожидалось.

Collections.sort(employeeList, new SalaryComparator());

Порядок ввода:

Name : Kumar, Salary : 40
Name : Sanket, Salary : 10
Name : Kumar, Salary : 20

Ожидаемый результат:

Name : Kumar, Salary : 20
Name : Kumar, Salary : 40
Name : Sanket, Salary : 10

Фактический результат:

Name : Sanket, Salary : 10 // incorrect order
Name : Kumar, Salary : 20
Name : Kumar, Salary : 40

person Saurabh Gokhale    schedule 24.03.2014    source источник


Ответы (4)


Это не потому, что ваш класс Employee уже имеет порядок по умолчанию, что использование Collections.sort с пользовательским компаратором введет новый уровень порядка.

Например, предположим, что ваши Employees по умолчанию упорядочиваются по их зарплате в порядке возрастания. Теперь предположим, что вы хотите отсортировать их по зарплате в порядке убывания.

По вашей логике как это будет себя вести?

Collections.sort(employees, new SalaryDescendingComparator());

Дело в том, что когда вы предоставляете кастомный компаратор для Collections.sort, он будет использовать только этот, а не тот механизм сортировки, который вы реализовали в своем классе Employee.

Как doc гласит:

Сортирует указанный список в соответствии с порядком, заданным указанным компаратором.

Так как SalaryComparator сравнивает сотрудников только по их зарплате, вот почему вы получаете этот вывод.

Если вы хотите сначала отсортировать по имени, а затем по зарплате, вам придется сделать это за один раз, то есть:

public class Employee implements Comparable<Employee> {

   private int empSalary;
   private String empName;

   @Override
   public int compareTo(Employee e) {
       int cmp = this.empName.compareTo(e.empName);
       return cmp != 0 ? cmp : Integer.compare(empSalary, e.empSalary);
   }

}
person Alexis C.    schedule 24.03.2014

Если вы хотите создать пользовательский компаратор для разделения связей имен на основе заработной платы, пусть компаратор сначала попытается сравнить двух сотрудников, а затем, если это связь, используйте зарплату:

public class SalaryComparator implements Comparator<Employee> {

    @Override
    public int compare(Employee e1, Employee e2)  {
        if (e1.compareTo(e2) == 0)
            return e1.empSalary - e2.empSalary;
        return e1.compareTo(e2);
    }
}
person Bohemian♦    schedule 24.03.2014

Обратите внимание, что вы можете сделать это следующим образом в Java 8:

public class Employee {
    private int empSalary;
    private String empName;

    public int getEmpSalary() {
        return empSalary;
    }

    public String getEmpName() {
        return empName;
    }
}

Обратите внимание, что я добавил публичный геттер, а затем для простого сравнения:

List<Employee> employeeList = new ArrayList<>();
// Populate it
employeeList.sort(Comparator.comparingInt(Employee::getEmpSalary));

Это создаст Comparator<Employee>, который сравнивает целочисленные значения, возвращаемые Employee.getEmpSalary(), я использую там ссылку на метод, что является причудливым способом написания employee -> employee.getEmpSalary(), который представляет собой лямбда-функцию, отображающую от Employee до int.

Для обратного вы можете использовать:

employeeList.sort(Comparator.comparingInt(Employee::getEmpSalary).reversed());

Теперь, если вы хотите сначала сравнить имя и зарплату, вы можете использовать:

employeeList.sort(
        Comparator.comparing(Employee::getEmpName)
        .thenComparingInt(Employee::getEmpSalary)
person skiwi    schedule 24.03.2014

Вместо того, чтобы создавать новый компаратор со всеми вашими условиями, как предлагали другие, вы также можете сортировать несколько раз.

Итак, если вы замените свою часть сортировки на это:

Collections.sort(employeeList, new SalaryComparator());
Collections.sort(employeeList);

Тогда вы получите ожидаемый результат.

Это может быть менее эффективно, чем сортировка всего одновременно с помощью комбинированного компаратора, но это очень удобно, когда вам нужно сортировать по нескольким свойствам.

А если вам действительно нужен мощный способ сортировки по разным свойствам (как в таблице), то стоит попробовать создать цепочку компараторов.
Вот пример такого шаблона:
http://commons.apache.org/proper/commons-collections/javadocs/api-2.1.1/org/apache/commons/collections/comparators/ComparatorChain.html
(это старый способ сделать то, что предлагается в ответе с использованием Java8 :)

person Guillaume    schedule 24.03.2014