Проблема с компаратором и ТreeSet
Имеется класс Human и производный от него класс Student:
public class Human implements Comparable<Human> {
private String name;
private String surname;
private String patronymic;
private int age;
public Human(String surname, String name, String patronymic, int age) {
this.name = name;
this.surname = surname;
this.patronymic = patronymic;
this.age = age;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getSurname() { return surname; }
public void setSurname(String surname) { this.surname = surname; }
public String getPatronymic() { return patronymic; }
public void setPatronymic(String patronymic) { this.patronymic = patronymic; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Human human = (Human) o;
return age == human.age && Objects.equals(name, human.name) && Objects.equals(surname, human.surname) && Objects.equals(patronymic, human.patronymic);
}
@Override
public int hashCode() {
return Objects.hash(name, surname, patronymic, age);
}
@Override
public String toString() {
return "Human{" +
", surname='" + surname + '\'' +
"name='" + name + '\'' +
", patronymic='" + patronymic + '\'' +
", age=" + age +
'}';
}
public String getFullName() {
return surname + " " + name + " " + patronymic;
}
@Override
public int compareTo(Human o2) {
return this.getFullName().compareTo(o2.getFullName());
}
}
//Student
public class Student extends Human {
String faculty;
public Student(String surname, String name, String patronymic, int age, String faculty) {
super(surname, name, patronymic, age);
this.faculty = faculty;
}
public String getFaculty() { return faculty; }
public void setFaculty(String faculty) { this.faculty = faculty; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
Student student = (Student) o;
return Objects.equals(faculty, student.faculty);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), faculty);
}
@Override
public String toString() {
return "Student{" +
"faculty='" + faculty + '\'' +
'}';
}
}
Мне нужно написать метод, который будет сортировать элементы входного множества объектов, расширяющих Human по неубыванию ФИО без явного использования сортировки (задача на изучение JCF).
Я использую TreeSet и определяю компаратор внутри метода:
public static List<Human> buildSortedlist(Set<? extends Human> humans) {
Set<Human> sortedSet = new TreeSet<>(new Comparator<Human>() {
@Override
public int compare(Human o1, Human o2) {
int compareNames = o1.getFullName().compareTo(o2.getFullName());
if (compareNames != 0) {
return compareNames;
} else {
return o1.compareTo(o2);
}
}
});
sortedSet.addAll(humans);
return new ArrayList<>(sortedSet);
}
Однако, если в сете есть люди с полностью одинаковым ФИО, например, "Иванов Иван Иванович 10 лет" и "Иванов Иван Иванович 60 лет", то данный метод оставит в списке только одного из них.
Не понимаю, в чем может быть проблема, подскажите, пожалуйста.
Об этом собственно написано в документации TreeSet: в данной реализации для сравнения элементов используются методы compareTo / compare, а не стандартная пара методов hashCode / equals как в обычном множестве (Set)
Note that the ordering maintained by aset(whether or not an explicit comparator is provided) must be consistent with equals if it is to correctly implement theSetinterface. (SeeComparableorComparatorfor a precise definition of consistent with equals.) This is so because theSetinterface is defined in terms of theequalsoperation, but aTreeSetinstance performs all element comparisons using itscompareTo(orcompare) method, so two elements that are deemed equal by this method are, from the standpoint of the set, equal. The behavior of a set is well-defined even if its ordering is inconsistent with equals; it just fails to obey the general contract of theSetinterface.
То есть, для сортированного множества упорядочение должно соответствовать методу equals, как описано в документации интерфейса Comparator:
The ordering imposed by a comparatorcon a set of elementsSis said to be consistent with equals if and only ifc.compare(e1, e2)==0has the samebooleanvalue ase1.equals(e2)for everye1ande2inS.
The natural ordering for a classCis said to be consistent with equals if and only ife1.compareTo(e2) == 0has the samebooleanvalue ase1.equals(e2)for everye1ande2of classC...
В представленном коде это требование соответствия нарушено, так как переопределённый компаратор сначала сравнивает два объекта Human по полному имени o1.getFullName().compareTo(o2.getFullName()), и в случае совпадения имён сравнивает объекты при помощи метода compareTo из реализации метода Comparable, в которой опять же выполняется сравнение только по полному имени, игнорируя другие свойства, которые используются в hashCode и equals для соответствующих классов (age для Human, faculty для Student).
Следовательно, придётся либо переделывать метод Human::compareTo, чтобы он учитывал поле age, либо переопределять компаратор с той же целью.
Переопределить компаратор можно гораздо лаконичнее с помощью фабричных методов Сomparator.comparing, Сomparator.thenComparing и др.
public static List<Human> buildSortedlist(Set<? extends Human> humans) {
Set<Human> sortedSet = new TreeSet<>(Comparator
.comparing(Human::getFullName)
.thenComparing(Human::getAge)
);
sortedSet.addAll(humans);
return new ArrayList<>(sortedSet);
}
Правда, и такой вариант не идеален, так как теперь могут теряться экземпляры "студентов" -- полных тёзок и сверстников с разных факультетов, чего можно избежать, "наколхозив" дополнительные сравнения:
public static List<Human> buildSortedlist(Set<? extends Human> humans) {
Set<Human> sortedSet = new TreeSet<>(Comparator
.comparing(Human::getFullName)
.thenComparing(Human::getAge)
.thenComparing(h -> h.getClass().getName())
.thenComparing(h -> ((Student) h).getFaculty())
);
sortedSet.addAll(humans);
return new ArrayList<>(sortedSet);
}
Также следует упомянуть о возможном решении данной задачи при помощи Stream API -- фактически, достаточно отсортировать стрим элементов исходного множества и преобразовать его в список:
public static <H extends Human> List<H> buildSortedlist(Set<H> humans) {
return humans.stream()
.sorted(Comparator
.comparing(Human::getFullName)
.thenComparing(Human::getAge)
.thenComparing(h -> h.getClass().getName())
.thenComparing(h -> ((Student) h).getFaculty())
)
.toList(); // или collect(Collectors.toList()) для Java 8..15
}