Замяна на 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 цента, но мисля, че го правиш по правилния начин. Вашият метод равенства ми изглежда добър.   -  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).  

Нямам доказателство, че това е вярно, но Реализацията на OpenJDK на equalsIgnoreCase всъщност го прави последователно с този метод; той проверява дали съответните символи са равни, след това дали техните версии с главни букви са равни, след това дали версиите с малки букви на версиите с главни букви са равни.

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", но няма да имат същото представяне, когато се преобразуват в малки букви!

Така че вашият подход тук е повреден. За съжаление, не съм сигурен, че ще можете да проектирате hashCode, който адекватно да изразява вашите нужди тук.

person Steven Schlansker    schedule 26.03.2013
comment
Така че използвайки символа ß като пример, ако имаме играч с първо/фамилно име ßilly ßob, сравняването му с друг играч на име SSilly SSob ще ги направи равни в очите на equalsIgnoreCase, но след това ще генерира два различни хеш-кода (проблемът ). Ако приемем, че това е „добре“ за моето приложение, можем ли да генерираме hashCode, който е равен, когато се считат за равни от 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