Переопределение hashCode с переопределенным равенством с использованием equalsIgnoreCase для проверки равенства

В настоящее время у меня есть переопределенный equals(Object), который выглядит так:

@Override
public boolean equals(Object o) {
    if (o == this) return true;
    if (! (o instanceof Player)) return false;
    Player p = (Player) o;
    return getFirstName().equalsIgnoreCase(p.getFirstName()) && 
            getLastName().equalsIgnoreCase(p.getLastName());
}

Мой hashCode() в настоящее время выглядит так:

@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + getFirstName().toLowerCase().hashCode();
    result = 31 * result + getLastName().toLowerCase().hashCode();
    return result;
}

Мой вопрос касается моего переопределенного метода hashCode(). Я знаю, что мне нужно, чтобы hashCode() возвращал одно и то же значение для двух объектов, если они считаются равными методом equals(Object). Моя интуиция подсказывает мне, что есть какой-то случай, когда этот hashCode() нарушит контракт.

Есть ли приемлемый способ использования метода equalsIgnoreCase(String) в переопределенном методе equals(Object) и генерации хэш-кода, не нарушающего контракт?


person Jazzer    schedule 26.03.2013    source источник
comment
В hashCode() результат = 31... должен быть результат *= 31... чтобы вы не потеряли уже существующее значение.   -  person Patashu    schedule 26.03.2013
comment
У него есть результат в уравнении, 31 * результат + (прочие вещи). Так что не теряется. Просто мои 2 цента, но я думаю, что вы идете по правильному пути. Ваш метод equals выглядит хорошо для меня.   -  person Kyle    schedule 26.03.2013
comment
Почему ваш код нарушает контракт? Ваше нутро должно быть нервничает, не слушайте его ;)   -  person ddmps    schedule 26.03.2013
comment
Возможно, я просто немного излишне осторожен, но я не совсем уверен, как методы equalsIgnoreCase() и toLowerCase() работают со специальными символами и разными локалями. Я не думаю, что это применимо к этому приложению, но я пытаюсь сделать все как можно более пуленепробиваемым, чтобы выработать эту привычку.   -  person Jazzer    schedule 26.03.2013
comment
Принято считать, что вы никогда не должны полагаться на Locale по умолчанию, но всегда должны использовать String.toLowerCase(Locale) с явным Locale. В противном случае вы столкнетесь с печально известной ошибкой турецкой локали.   -  person Robert Tupelo-Schneck    schedule 08.05.2013


Ответы (4)


@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + characterwiseCaseNormalize(getFirstName()).hashCode();
    result = 31 * result + characterwiseCaseNormalize(getLastName()).hashCode();
    return result;
}

private static String characterwiseCaseNormalize(String s) {
    StringBuilder sb = new StringBuilder(s);
    for(int i = 0; i < sb.length(); i++) {
        sb.setCharAt(i,Character.toLowerCase(Character.toUpperCase(sb.charAt(i))));
    }
    return sb.toString();
}

Это hashCode будет соответствовать equals, определенному с помощью equalsIgnoreCase. В принципе, согласно договору equalsIgnoreCase, это, по-видимому, зависит от того, что

Character.toLowerCase(Character.toUpperCase(c1))==Character.toLowerCase(Character.toUpperCase(c2))

в любое время

Character.toLowerCase(c1)==Character.toLowerCase(c2).  

У меня нет доказательств того, что это правда, но реализация equalsIgnoreCase в OpenJDK фактически делает это последовательно с этим методом; он проверяет, равны ли соответствующие символы, затем равны ли их версии в верхнем регистре, затем равны ли версии в нижнем регистре версий в верхнем регистре.

person Robert Tupelo-Schneck    schedule 07.05.2013
comment
И String.compareToIgnoreCase явно использует этот метод. - person Robert Tupelo-Schneck; 07.05.2013
comment
Я +1 за новый подход, но вы должны быть очень осторожны. Javadocs даже предупреждает вас: In general, String.toLowerCase() should be used to map characters to lowercase. String case mapping methods have several benefits over Character case mapping methods. String case mapping methods can perform locale-sensitive mappings, context-sensitive mappings, and 1:M character mappings, whereas the Character case mapping methods cannot. Кроме того, такое поведение не гарантируется спецификацией, поэтому оно может отличаться от вашего другого. Осторожность! - person Steven Schlansker; 07.05.2013
comment
Верно... Я бы сказал, что String.equalsIgnoreCase()String.compareToIgnoreCase()), основанные на методах отображения регистра Character, должны сопровождаться той же оговоркой. С точки зрения написания hashCode(), совместимого с equals(), вы должны либо использовать сопоставление регистра на основе Character в обоих случаях, либо сопоставление регистра на основе String в обоих случаях. На самом деле, первоначальный задавший вопрос может действительно захотеть сохранить свой метод hashCode() и изменить свой метод equals(), чтобы использовать s1.toLowerCase().equals(s2.toLowerCase()) вместо equalsIgnoreCase(). - person Robert Tupelo-Schneck; 08.05.2013

Ты прав. Мы можем просмотреть все односимвольные строки и найти пары s1,s2 и s1.equalsIgnoreCase(s2) && !s1.toLowerCase().equals(s2.toLowerCase()). Есть довольно много пар. Например

s1=0049   'LATIN CAPITAL LETTER I'
s2=0131   'LATIN SMALL LETTER DOTLESS I'

s1.lowercase = 0069   'LATIN SMALL LETTER I'
s2.lowercase = 0131   itself

Это также зависит от локали: для s1 турецкий и азербайджанский используют U+0131 для нижнего регистра (см. http://www.fileformat.info/info/unicode/char/0049/index.htm )

person ZhongYu    schedule 26.03.2013

Вы правы, что беспокоитесь. Прочитайте договор для equalsIgnoreCase.

Два символа c1 и c2 считаются одним и тем же без учета регистра, если верно хотя бы одно из следующего:

  • Два символа одинаковы (по сравнению с оператором ==)
  • Применение метода Character.toUpperCase(char) к каждому символу дает тот же результат.
  • Применение метода Character.toLowerCase(char) к каждому символу дает тот же результат.

Итак, если есть символ, который равен при преобразовании в верхний регистр, но не наоборот, у вас будут проблемы.

Возьмем в качестве примера немецкий символ ß, который превращается в последовательность из двух символов SS при преобразовании в верхний регистр. Это означает, что строки «ß» и «SS» являются «equalsIgnoreCase», но не будут иметь такого же представления при преобразовании в нижний регистр!

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

person Steven Schlansker    schedule 26.03.2013
comment
Таким образом, используя символ ß в качестве примера, если бы у нас был игрок с именем/фамилией ßilly ßob, сравнение его с другим игроком по имени SSilly SSob сделало бы их равными в глазах equalsIgnoreCase, но затем сгенерировало бы два разных хэш-кода (проблема ). Предполагая, что это «хорошо» для моего приложения, можем ли мы сгенерировать хэш-код, который равен, когда они считаются равными equalsIgnoreCase, используя toUpperCase, где я использовал toLowerCase? - person Jazzer; 26.03.2013
comment
Я уверен, что вы могли бы найти и противоположный пример. - person Steven Schlansker; 27.03.2013
comment
@Jazzer: Определяет ли equalsIgnoreCase отношение эквивалентности, то есть невозможно ли иметь три строки x, y и z, такие, что x.equalsIgnoreCase(y) и y.equalsIgnoreCase(z), но не x.equalsIgnoreCase(z)? Судя по всему, ß.equalsIgnoreCase(SS) будет истинным, а ss.equalsIgnoreCase(SS) будет истинным, но ß.equalsIgnoreCase(ss) будет ложным. Переопределение equals функцией, которая не реализует отношение эквивалентности, будет нарушено, даже если hashCode всегда возвращает совпадающие значения для совпадающих строк. - person supercat; 27.04.2013
comment
"ß".equalsIgnoreCase("SS") ложно, потому что equalsIgnoreCase использует Character.toUpperCase и Character.toLowerCase вместо String.toUpperCase и String.toLowerCase. Это дает надежду на то, что hashCode будет соответствовать equalsIgnoreCase; см. мой ответ. - person Robert Tupelo-Schneck; 07.05.2013

С точки зрения написания hashCode(), совместимого с equals(), вы должны либо использовать сопоставление регистра на основе Character в обоих случаях, либо сопоставление регистра на основе String в обоих случаях. В моем другом ответе я показал, как написать hashCode(), используя сопоставление регистров на основе Character; но есть и другое решение, которое состоит в том, чтобы изменить equals() вместо использования отображения регистра на основе String. (Обратите внимание, что String.equalsIgnoreCase() использует сопоставление регистра на основе Character.)

@Override
public boolean equals(Object o) {
    if (o == this) return true;
    if (! (o instanceof Player)) return false;
    Player p = (Player) o;
    return getFirstName().toLowerCase().equals(p.getFirstName().toLowerCase()) && 
        getLastName().toLowerCase().equals(p.getLastName().toLowerCase());
}
person Robert Tupelo-Schneck    schedule 08.05.2013
comment
Фактически, в некоторых обстоятельствах вы действительно хотите использовать какую-то причудливую нормализацию Unicode для ваших строк, а также свертывание регистра. См. userguide.icu-project.org/transforms/normalization . - person Robert Tupelo-Schneck; 08.05.2013