Проблема с компаратором и ТreeSet

Проблема с компаратором и Т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 a set (whether or not an explicit comparator is provided) must be consistent with equals if it is to correctly implement the Set interface. (See Comparable or Comparator for a precise definition of consistent with equals.) This is so because the Set interface is defined in terms of the equals operation, but a TreeSet instance performs all element comparisons using its compareTo (or compare) 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 the Set interface.

То есть, для сортированного множества упорядочение должно соответствовать методу equals, как описано в документации интерфейса Comparator:

The ordering imposed by a comparator c on a set of elements S is said to be consistent with equals if and only if c.compare(e1, e2)==0 has the same boolean value as e1.equals(e2) for every e1 and e2 in S.

и интерфейса Comparable:

The natural ordering for a class C is said to be consistent with equals if and only if e1.compareTo(e2) == 0 has the same boolean value as e1.equals(e2) for every e1 and e2 of class C...

В представленном коде это требование соответствия нарушено, так как переопределённый компаратор сначала сравнивает два объекта Human по полному имени o1.getFullName().compareTo(o2.getFullName()), и в случае совпадения имён сравнивает объекты при помощи метода compareTo из реализации метода Comparable, в которой опять же выполняется сравнение только по полному имени, игнорируя другие свойства, которые используются в hashCode и equals для соответствующих классов (age для Humanfaculty для 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
}




Report Page