Анализатор выявил случай, когда метод 'compareTo' перегружает одноименный метод родительского класса, который реализует интерфейс 'Comparable<T>'. Скорее всего, перегрузка метода родительского класса не то, что подразумевалось разработчиком.
Рассмотрим пример класса, который вместо переопределения метода осуществляет перегрузку:
public class Human implements Comparable<Human> { private String mName; private int mAge; .... Human(String name, int age) { mName = name; mAge = age; } .... public int compareTo(Human human) { int result = this.mName.compareTo(human.mName); if (result == 0) { result = Integer.compare(this.mAge, human.mAge); } return result; } } public class Employee extends Human { int mSalary; .... public Employee(String name, int age, int salary) { super(name, age); mSalary = salary; } .... public int compareTo(Employee employee) { return Integer.compare(this.mSalary, employee.mSalary); } }
Итак, у нас есть два класса: базовый 'Human' и дочерний 'Employee'. 'Human' реализует интерфейс 'Comparable<Human>' и определяет метод 'compareTo'. Далее дочерний класс 'Employee' расширяет базовый класс и перегружает метод 'compareTo'. Результат метода сравнения таков:
Из этого может выйти следующий эффект:
1) Если мы представим объекты 'Employee' через ссылку на базовый класс, то вызвав метод 'compareTo' мы не достигнем нужного сравнения объектов:
Human emp1 = new Employee("Andrew", 25, 33000); Human emp2 = new Employee("Madeline", 29, 31000); System.out.println(emp1.compareTo(emp2));
На экран будет выведено -12, и получается, что объект emp1 логически меньше emp2. Ведь это не так. Скорее всего, программистом подразумевалось, что эти объекты будут сравниваться на основе поля 'mSalary', и в таком случае, результат был бы противоположным. Это произошло из-за того, что программист перегрузил метод сравнения, а не переопределил. И когда произошел вызов метода, то вызвался метод из класса 'Human'.
2) Известно, что списки элементов, которые реализуют интерфейс 'Comparable<T>', могут автоматически быть отсортированы с помощью 'Collections.sort'/'Arrays.sort', а также такие элементы могут быть использованы в качестве ключей отсортированных коллекций, без явного указания компоратора. И в случае такой перегрузки сортировка будет осуществляться не так, как задумывалась, судя по реализации сравнения в дочернем классе. Опасность в том, что в таких случаях происходит неявный вызов метода сравнения, и сразу ошибку можно не найти.
Выполним следующий код:
List<Human> listEmployees = new ArrayList<>(); listEmployees.add(new Employee("Andrew", 25, 33000)); listEmployees.add(new Employee("Madeline", 29, 31000)); listEmployees.add(new Employee("Hailey", 45, 55000)); System.out.println("Before: "); listEmployees.forEach(System.out::println); Collections.sort(listEmployees); System.out.println("After: "); listEmployees.forEach(System.out::println);
На экран будет выведен следующий текст:
Before: Name: Andrew; Age: 25; Salary: 33000 Name: Madeline; Age: 29; Salary: 31000 Name: Hailey; Age: 45; Salary: 55000 After: Name: Andrew; Age: 25; Salary: 33000 Name: Hailey; Age: 45; Salary: 55000 Name: Madeline; Age: 29; Salary: 31000
Как видно из вывода, сортировка была осуществлена не по полю 'mSalary'. И все происходит по той же самой причине.
Решение этой проблемы сводится к тому, что метод сравнения должен быть переопределен, а не перегружен. Например:
public class Employee extends Human { .... public int compareTo(Human employee) { if (employee instanceof Employee) { return Integer.compare(this.mSalary, ((Employee)employee).mSalary); } return -1; } .... }
И тогда будет все работать так, как задумывалось изначально.
В первом случае вывод будет 1 (emp1 логически больше emp2).
Во втором случае вывод будет следующим:
Name: Andrew; Age: 25; Salary: 33000 Name: Madeline; Age: 29; Salary: 31000 Name: Hailey; Age: 45; Salary: 55000 After: Name: Madeline; Age: 29; Salary: 31000 Name: Andrew; Age: 25; Salary: 33000 Name: Hailey; Age: 45; Salary: 55000