Java, почему подразумевается, что объекты равны, если compareTo () возвращает 0? - PullRequest
19 голосов
/ 29 августа 2011

Давайте иметь класс Person. У человека есть имя и рост.

Равен, а hashCode () учитывает только имя. Человек сопоставим (или мы реализуем для него компаратор, не важно какой). Люди сравниваются по росту.

Кажется разумным ожидать ситуации, когда два разных человека могут иметь одинаковый рост, но, например, TreeSet ведет себя как comapareTo () == 0 означает равный, а не просто одинаковый размер.

Чтобы избежать этого, сравнение может вторично взглянуть на что-то другое, если размер одинаков, но затем его нельзя использовать для обнаружения объектов одинакового размера.

Пример:

import java.util.Comparator;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;

public class Person implements Comparable<Person> {

private final String name;
private int height;

public Person(String name,
        int height) {
    this.name = name;
    this.height = height;
}

public int getHeight() {
    return height;
}

public void setHeight(int height) {
    this.height = height;
}

public String getName() {
    return name;
}

@Override
public int compareTo(Person o) {
    return Integer.compare(height, o.height);
}

public boolean equals(Object obj) {
    if (obj == null) {
        return false;
    }
    if (getClass() != obj.getClass()) {
        return false;
    }
    final Person other = (Person) obj;
    if (!Objects.equals(this.name, other.name)) {
        return false;
    }
    return true;
}

public int hashCode() {
    int hash = 5;
    hash = 13 * hash + Objects.hashCode(this.name);
    return hash;
}

public String toString() {
    return "Person{" + name + ", height = " + height + '}';
}

public static class PComparator1 implements Comparator<Person> {

    @Override
    public int compare(Person o1,
            Person o2) {
        return o1.compareTo(o2);
    }
}

public static class PComparator2 implements Comparator<Person> {

    @Override
    public int compare(Person o1,
            Person o2) {
        int r = Integer.compare(o1.height, o2.height);
        return r == 0 ? o1.name.compareTo(o2.name) : r;
    }
}

public static void test(Set<Person> ps) {
    ps.add(new Person("Ann", 150));
    ps.add(new Person("Jane", 150));
    ps.add(new Person("John", 180));
    System.out.println(ps.getClass().getName());
    for (Person p : ps) {
        System.out.println(" " + p);
    }
}

public static void main(String[] args) {
    test(new HashSet<Person>());
    test(new TreeSet<Person>());
    test(new TreeSet<>(new PComparator1()));
    test(new TreeSet<>(new PComparator2()));
}
}

результат:

java.util.HashSet
 Person{Ann, height = 150}
 Person{John, height = 180}
 Person{Jane, height = 150}

java.util.TreeSet
 Person{Ann, height = 150}
 Person{John, height = 180}

java.util.TreeSet
 Person{Ann, height = 150}
 Person{John, height = 180}

java.util.TreeSet
 Person{Ann, height = 150}
 Person{Jane, height = 150}
 Person{John, height = 180}

У вас есть представление, почему это так?

Ответы [ 6 ]

16 голосов
/ 29 августа 2011

Извлечение из java.util.SortedSet javadoc:

Обратите внимание, что упорядоченность, поддерживаемая отсортированным набором (независимо от того, предоставляется ли явный компаратор), должна соответствовать значениям equals, если отсортированный набор равенправильно реализовать интерфейс Set.(См. Интерфейс Comparable или интерфейс Comparator для точного определения соответствия с equals.) Это так, потому что интерфейс Set определен в терминах операции equals, но отсортированный набор выполняет все сравнения элементов, используя его метод CompareTo (или сравнение)Таким образом, два элемента, которые считаются равными этим методом, с точки зрения отсортированного набора равны.Поведение отсортированного набора четко определено, даже если его порядок не совпадает с равенством;он просто не соблюдает общий контракт интерфейса Set.

Следовательно, другими словами, SortedSet нарушает (или «расширяет») общие контракты для Object.equals() и Comparable.compareTo.См. Контракт на compareTo:

Настоятельно рекомендуется, но не обязательно, чтобы (x.compareTo (y) == 0) == (x.equals (y)).Вообще говоря, любой класс, который реализует интерфейс Comparable и нарушает это условие, должен четко указывать на этот факт.Рекомендуемый язык: «Примечание: этот класс имеет естественный порядок, не совместимый с равным».

10 голосов
/ 29 августа 2011

Рекомендуется, чтобы compareTo возвращал только 0, если вызов equals для тех же объектов вернет true:

Естественный порядок для класса C называется равным в том и только том случае, если e1.compareTo (e2) == 0 имеет то же логическое значение, что и e1.equals (e2) для всех e1 и e2 класса C Обратите внимание, что null не является экземпляром какого-либо класса, и e.compareTo (null) должен выдать исключение NullPointerException, даже если e.equals (null) возвращает false.

(из JDK 1.6 Javadocs )

6 голосов
/ 29 августа 2011

TreeSet не работает с использованием хеш-кодов и равенства - он работает только на основе предоставленного вами компаратора.Обратите внимание, что Javadoc заявляет:

Обратите внимание, что порядок, поддерживаемый набором (независимо от того, предоставляется ли явный компаратор), должен соответствовать равным, если он должен правильно реализовыватьсяЗадать интерфейс.(См. Comparable или Comparator для точного определения соответствия с equals.) Это так, потому что интерфейс Set определен в терминах операции equals, но экземпляр TreeSet выполняет все сравнения элементов, используя свой метод CompareTo (или сравнение), поэтому дваэлементы, которые считаются равными этим методом, с точки зрения множества равны.Поведение множества корректно определено, даже если его порядок не совпадает с равенством;он просто не соблюдает общий контракт интерфейса Set.

В вашем случае ваше сравнение * не соответствует equals, поэтому ваш набор не подчиняется общему контракту Set.

Почему бы просто не добавить больше аспектов к сравнению, чтобы только равные элементы сравнивались с результатом 0?

1 голос
/ 29 августа 2011

Настоятельно рекомендуется, но не строго требует, чтобы (x.compareTo(y)==0) == (x.equals(y)) [ 1 ]

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

Однако было бы лучше, если бы вы сравнивали по тем же критериям и, если необходимо, предоставили специальный компаратор, который работает по новым критериям (высота в случае человека)

1 голос
/ 29 августа 2011

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

@Override
public int compareTo(Person o) {
    if(height == o.height)return name.compareTo(o.name);

    return Integer.compare(height, o.height);
}

, поскольку имена уникальны, возвращается только 0, если this.equals(o)

0 голосов
/ 29 августа 2011

Когда вы даете Person Comparator, который сравнивает экземпляры по атрибуту высоты Person, это действительно означает, что два экземпляра Person одинаковы, если они имеют одинаковую высоту. Вам нужно будет создать компаратор, специфичный для класса Person.

...