JavaRush /Java блогы /Random-KK /Equals және hashCode келісім-шарттары немесе кез келген н...
Aleksandr Zimin
Деңгей
Санкт-Петербург

Equals және hashCode келісім-шарттары немесе кез келген нәрсе

Топта жарияланған
Java-бағдарламашыларының басым көпшілігі, әрине, әдістер бір-бірімен тығыз байланысты екенін біледі equalsжәне hashCodeосы екі әдісті де өз сабақтарында дәйекті түрде ауыстырған жөн. Неліктен бұлай екенін және бұл ереже бұзылған жағдайда қандай қайғылы салдары болуы мүмкін екенін сәл азырақ сан біледі. Мен осы әдістердің тұжырымдамасын қарастыруды, олардың мақсатын қайталауды және олардың неге соншалықты байланысты екенін түсінуді ұсынамын. Мен бұл мақаланы, сыныптарды жүктеу туралы алдыңғы мақала сияқты, мәселенің барлық егжей-тегжейлерін ақырында ашып көрсету және енді үшінші тарап көздеріне оралмау үшін өзім үшін жаздым. Сондықтан мен сындарлы сынға қуаныштымын, өйткені бір жерде олқылықтар болса, оны жою керек. Мақала, өкінішке орай, өте ұзақ болып шықты.

қайта анықтау ережелеріне тең

Java тілінде бір шығу тегі екі нысанның логикалық теңequals() екендігін растайтын немесе жоққа шығаратын әдіс қажет . Яғни, екі нысанды салыстыру кезінде бағдарламашы олардың маңызды өрістерінің эквивалентті екенін түсінуі керек . Барлық өрістердің бірдей болуы міндетті емес, өйткені әдіс логикалық теңдікті білдіреді . Бірақ кейде бұл әдісті қолданудың ерекше қажеттілігі болмайды. Олар айтқандай, белгілі бір механизмді пайдалану проблемаларын болдырмаудың ең оңай жолы - оны қолданбау. Сондай-ақ, сіз келісім-шартты бұзған кезде, сіз басқа нысандар мен құрылымдардың сіздің an objectімен қалай әрекеттесетінін түсінуді бақылауды жоғалтатыныңызды атап өткен жөн . Ал кейіннен қатенің себебін табу өте қиын болады. equals()equals

Бұл әдісті қайта анықтауға болмайды

  • Сыныптың әрбір данасы бірегей болғанда.
  • Көбінесе бұл деректермен жұмыс істеуге арналған емес, нақты әрекетті қамтамасыз ететін сыныптарға қатысты. Мысалы, сынып сияқты Thread. Олар үшін equalsсынып ұсынған әдісті жүзеге асыру Objectжеткілікті. Тағы бір мысал - enum сыныптары ( Enum).
  • Шындығында сыныпқа оның даналарының баламалылығын анықтау талап етілмейді.
  • Мысалы, сынып үшін java.util.Randomолардың кездейсоқ сандардың бірдей тізбегін қайтара алатындығын анықтай отырып, сынып даналарын бір-бірімен салыстырудың қажеті жоқ. Бұл сыныптың табиғаты мұндай мінез-құлықты білдірмейді.
  • Сіз кеңейтіп жатқан сыныпта әдістің өзіндік іске асырылуы болған кезде equalsжәне осы іске асыру әрекеті сізге сәйкес келеді.
  • Мысалы, , сыныптары үшін Setіске асыру сәйкесінше және ішінде Listболады . MapequalsAbstractSetAbstractListAbstractMap
  • equalsАқырында, сіздің сыныпыңыздың ауқымы болған кезде қайта анықтаудың қажеті жоқ privateнемесе package-privateжәне сіз бұл әдіс ешқашан шақырылмайтынына сенімдісіз.

шартқа тең

Әдісті қайта анықтау кезінде equalsәзірлеуші ​​Java тілінің спецификациясында анықталған негізгі ережелерді сақтауы керек.
  • Рефлексивтілік
  • кез келген берілген мән үшін xөрнек x.equals(x)қайтарылуы керек true.
    Берілген – осылай дегенді білдіредіx != null
  • Симметрия
  • кез келген берілген мәндер үшін xжәне y, қайтарылса ғана x.equals(y)қайтарылуы керек . truey.equals(x)true
  • Өтпелілік
  • кез келген берілген мәндер үшін және x, егер қайтарса және қайтарса , мәнді қайтаруы керек . yzx.equals(y)truey.equals(z)truex.equals(z)true
  • Жүйелілік
  • кез келген берілген мәндер үшін xжәне yқайталанатын шақыру x.equals(y)екі нысанды салыстыру үшін пайдаланылатын өрістер қоңыраулар арасында өзгермеген жағдайда, алдыңғы шақырудың мәнін осы әдіске қайтарады.
  • Салыстыру null
  • кез келген берілген мән үшін xқоңырау x.equals(null)қайтарылуы керек false.

шартты бұзуға тең

Java Collections Framework сияқты көптеген сыныптар әдісті іске асыруға байланысты equals(), сондықтан оны елемеуге болмайды, себебі Бұл әдістің шартын бұзу өтінімнің қисынсыз жұмыс істеуіне әкелуі мүмкін және бұл жағдайда оның себебін табу өте қиын болады. Рефлексивтілік принципі бойынша әрбір an object өзіне эквивалентті болуы керек. Егер бұл принцип бұзылса, коллекцияға an object қосып, содан кейін оны әдіс арқылы іздегенде, contains()біз жинаққа жаңа қосқан нысанды таба алмаймыз. Симметрия шарты кез келген екі an object салыстыру ретіне қарамастан тең болуы керектігін айтады. equalsМысалы, егер сізде жол түрінің тек бір өрісі бар сынып болса, бұл өрісті әдістегі жолмен салыстыру дұрыс емес болады . Өйткені кері салыстыру жағдайында әдіс әрқашан мәнді қайтарады false.
// Нарушение симметричности
public class SomeStringify {
    private String s;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o instanceof SomeStringify) {
            return s.equals(((SomeStringify) o).s);
        }
        // нарушение симметричности, классы разного происхождения
        if (o instanceof String) {
            return s.equals(o);
        }
        return false;
    }
}
//Правильное определение метода equals
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    return o instanceof SomeStringify &&
            ((SomeStringify) o).s.equals(s);
}
Өтпелілік шартынан шығатыны, егер үш an objectінің кез келген екеуі тең болса, онда бұл жағдайда үшеуі де тең болуы керек. Бұл принцип оңай бұзылуы мүмкін, қашан қажет кеңейту белгілі бір базалық класс қосу арқылы оған мағыналы құрамдас . PointМысалы, координаттары бар сыныпқа xжәне yоны кеңейту арқылы нүктенің түсін қосу керек. ColorPointМұны істеу үшін сізге сәйкес өрісі бар сыныпты жариялау қажет color. Осылайша, егер кеңейтілген сыныпта біз equalsата-аналық әдіс деп атасақ, ал ата-аналықта тек координаттар xмен салыстырылады деп есептесек y, онда әртүрлі түсті, бірақ координаталары бірдей екі нүкте тең деп есептеледі, бұл дұрыс емес. Бұл жағдайда туынды сыныпты түстерді ажыратуға үйрету керек. Мұны істеу үшін сіз екі әдісті пайдалана аласыз. Бірақ біреуі симметрия ережесін бұзады , ал екіншісі - транзитивтілік .
// Первый способ, нарушая симметричность
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof ColorPoint)) return false;
    return super.equals(o) && ((ColorPoint) o).color == color;
}
Бұл жағдайда қоңырау point.equals(colorPoint)мәнді қайтарады , ал trueсалыстыру , өйткені «өз» класының an objectісін күтеді. Осылайша симметрия ережесі бұзылады. Екінші әдіс нүктенің түсі туралы деректер болмаған жағдайда «соқыр» тексеруді қамтиды, яғни бізде сынып бар . Немесе түс туралы ақпарат бар болса, оны тексеріңіз, яғни сыныптың нысанын салыстырыңыз . colorPoint.equals(point)falsePointColorPoint
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof Point)) return false;

    // Слепая проверка
    if (!(o instanceof ColorPoint))
        return super.equals(o);

    // Полная проверка, включая цвет точки
    return super.equals(o) && ((ColorPoint) o).color == color;
}
Бұл жерде транзитивтілік принципі келесідей бұзылады. Келесі an objectілердің анықтамасы бар делік:
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
p1.equals(p2)Осылайша, және теңдігі орындалса да p2.equals(p3), p1.equals(p3)ол мәнді қайтарады false. Сонымен қатар, екінші әдіс, менің ойымша, аз тартымды көрінеді, өйткені Кейбір жағдайларда алгоритм соқыр болуы және салыстыруды толық орындамауы мүмкін және сіз бұл туралы білмеуіңіз мүмкін. Біраз поэзия Жалпы, менің түсінуімше, бұл мәселенің нақты шешімі жоқ. Кей Хорстманн есімді бір беделді автордың пікірі бар, сіз операторды пайдалануды нысанның сыныбын қайтаратын instanceofәдіс шақыруымен ауыстыра аласыз getClass()және an objectілердің өздерін салыстыруды бастамас бұрын олардың бір типті екеніне көз жеткізіңіз. , және олардың ортақ шығу фактісіне назар аудармаңыз. Осылайша, симметрия және транзитивтілік ережелері қанағаттандырылады. Бірақ сонымен бірге, баррикаданың арғы жағында, бұл тәсіл Барбара Лисковтың ауыстыру принципін бұзады деп санайтын, кең ортада одан кем емес тағы бір автор Джошуа Блох тұр. Бұл принцип «шақыру codeы базалық классты оның ішкі сыныптары сияқты білмей-ақ қарауы керек » деп көрсетеді . Ал Хорстман ұсынған шешімде бұл принцип анық бұзылған, өйткені ол іске асыруға байланысты. Қысқасы, істің күңгірт екені анық. Сондай-ақ, Хорстманның өз көзқарасын қолдану ережесін түсіндіретінін және сабақтарды құрастыру кезінде стратегия туралы шешім қабылдау керек екенін қарапайым ағылшын тілінде жазатынын атап өткен жөн, ал егер теңдік тестілеуді тек суперкласс жүргізетін болса, мұны орындау арқылы жасауға болады. операция instanceof. Әйтпесе, тексерудің семантикасы туынды сыныпқа байланысты өзгергенде және әдісті жүзеге асыру иерархия бойынша төмен жылжыту қажет болғанда, әдісті пайдалану керек getClass(). Джошуа Блох, өз кезегінде, мұрадан бас тартуды және сыныпқа ColorPointсыныпты қосу және нүкте туралы арнайы ақпаратты алу үшін Pointкіру әдісін қамтамасыз ету арқылы an object құрамын пайдалануды ұсынады. asPoint()Бұл барлық ережелерді бұзудан аулақ болады, бірақ менің ойымша, бұл codeты түсінуді қиындатады. Үшінші нұсқа IDE көмегімен теңдік әдісін автоматты түрде жасауды пайдалану болып табылады. Айтпақшы, идея суперкласста немесе оның ұрпақтарында әдісті енгізу стратегиясын таңдауға мүмкіндік беретін Хорстман ұрпақтарын шығарады. Соңында, келесі бірізділік ережесі нысандар өзгермесе де x, yоларды қайта шақыру x.equals(y)бұрынғыдай мәнді қайтару керек екенін айтады. Соңғы ереже - ешбір нысан -ге тең болмауы керек null. Мұнда бәрі түсінікті null– бұл белгісіздік, an object белгісіздікке тең бе? Бұл анық емес, яғни false.

Теңдіктерді анықтаудың жалпы алгоритмі

  1. thisНысан сілтемелерінің және әдіс параметрлерінің теңдігін тексеріңіз o.
    if (this == o) return true;
  2. Сілтеменің анықталғанын o, яғни анықталғанын тексеріңіз null.
    Егер болашақта нысан түрлерін салыстыру кезінде оператор пайдаланылса instanceof, бұл элементті өткізіп жіберуге болады, өйткені бұл параметр falseосы жағдайда қайтарылады null instanceof Object.
  3. Жоғарыдағы сипаттаманы және жеке интуицияны басшылыққа ала отырып, оператор немесе әдісті thisпайдаланып нысан түрлерін салыстырыңыз .oinstanceofgetClass()
  4. Егер әдіс equalsішкі сыныпта қайта анықталса, қоңырау шалуды ұмытпаңызsuper.equals(o)
  5. Параметр түрін oқажетті классқа түрлендіріңіз.
  6. Барлық маңызды нысан өрістерін салыстыруды орындаңыз:
    • қарапайым түрлер үшін ( floatжәне қоспағанда double) операторды пайдалана отырып==
    • анықтамалық өрістер үшін олардың әдісін шақыру керекequals
    • массивтер үшін циклдік итерацияны немесе әдісті пайдалануға боладыArrays.equals()
    • түрлері үшін floatжәне doubleсәйкес орауыш сыныптарының салыстыру әдістерін қолдану қажет Float.compare()жәнеDouble.compare()
  7. Соңында үш сұраққа жауап беріңіз: енгізілген әдіс симметриялы ма ? Транзитивті ? Келісілді ме ? Қалған екі принцип ( рефлексивтілік және сенімділік ) әдетте автоматты түрде орындалады.

HashCode ережелерін қайта анықтау

Хэш - белгілі бір уақытта оның күйін сипаттайтын нысаннан жасалған сан. Бұл сан Java тілінде негізінен хэш-кестелерде пайдаланылады, мысалы HashMap. Бұл жағдайда нысанға негізделген санды алудың хэш функциясы хэш кестесі бойынша элементтердің салыстырмалы түрде біркелкі таралуын қамтамасыз ететіндей жүзеге асырылуы керек. Сондай-ақ, функция әртүрлі пернелер үшін бірдей мәнді қайтарған кезде соқтығысу ықтималдығын азайту үшін.

Келісімшарт хэшcodeы

Хэш функциясын жүзеге асыру үшін тіл спецификациясы келесі ережелерді анықтайды:
  • бір нысанда әдісті hashCodeбір немесе бірнеше рет шақыру мәнді есептеуге қатысатын нысан өрістері өзгермеген жағдайда, бірдей хэш мәнін қайтаруы керек.
  • екі нысандағы әдісті шақыру әрқашан бірдей санды қайтаруы керек, егер нысандар тең болса ( осы нысандардағы hashCodeәдісті шақыру қайтарады ).equalstrue
  • hashCodeекі тең емес нысанда әдісті шақыру әртүрлі хэш мәндерін қайтаруы керек. Бұл талап міндетті емес болса да, оның орындалуы хэш-кестелердің жұмысына оң әсер ететінін ескеру керек.

тең және hashCode әдістерін бірге қайта анықтау керек

Жоғарыда сипатталған келісім-шарттарға сүйене отырып, codeыңыздағы әдісті қайта анықтау кезінде equalsсіз әрқашан әдісті қайта анықтауыңыз керек hashCode. Шын мәнінде сыныптың екі данасы әртүрлі болғандықтан, олар әртүрлі жад аймақтарында болғандықтан, оларды кейбір логикалық критерийлерге сәйкес салыстыру керек. Сәйкесінше, екі логикалық баламалы нысан бірдей хэш мәнін қайтаруы керек. Осы әдістердің біреуі ғана қайта анықталса не болады?
  1. equalsИә hashCodeЖоқ

    Біз equalsсыныбымызда әдісті дұрыс анықтадық делік және hashCodeәдісті сыныптағыдай қалдыруды ұйғардық Object. Сонда әдіс тұрғысынан equalsекі нысан логикалық тең болады, ал әдіс тұрғысынан hashCodeолардың ортақ ештеңесі болмайды. Осылайша, нысанды хэш-кестеге орналастыру арқылы біз оны кілт арқылы қайтарып алмау қаупін тудырамыз.
    Мысалы, келесідей:

    Map<Point, String> m = new HashMap<>();
    m.put(new Point(1, 1),Point A);
    // pointName == null
    String pointName = m.get(new Point(1, 1));

    Орналастырылатын an object пен ізделетін an object логикалық жағынан тең болғанымен, екі түрлі нысан екені анық. Бірақ, өйткені оларда әртүрлі хэш мәндері бар, өйткені біз келісім-шартты бұздық, біз хэш кестесінің бір жерінде an objectімізді жоғалттық деп айта аламыз.

  2. hashCodeИә equalsЖоқ.

    Егер әдісті қайта анықтасақ hashCodeжәне equalsәдісті жүзеге асыруды сыныптан мұра етсек не болады Object. Өздеріңіз білетіндей, equalsәдепкі әдіс көрсеткіштерді an objectілермен салыстырады, олардың бір нысанға сілтеме жасайтынын анықтайды. hashCodeБіз әдісті барлық канондарға сәйкес жаздык деп есептейік , атап айтқанда, оны IDE көмегімен құрдық және ол логикалық бірдей нысандар үшін бірдей хэш мәндерін қайтарады. Әлбетте, осылайша біз екі нысанды салыстырудың кейбір механизмін анықтадық.

    Сондықтан алдыңғы абзацтағы мысалды теориялық түрде орындау керек. Бірақ біз әлі де хэш кестесінде нысанды таба алмаймыз. Біз бұған жақын боламыз, өйткені кем дегенде біз an object жататын хэш үстелінің себетін табамыз.

    Хэш кестесіндегі нысанды сәтті іздеу үшін кілттің хэш мәндерін салыстырудан басқа, ізделетін нысанмен кілттің логикалық теңдігін анықтау да қолданылады. Яғни, equalsәдісті жоққа шығармай, амал жоқ.

hashCode анықтаудың жалпы алгоритмі

Бұл жерде, менің ойымша, сіз тым көп уайымдамауыңыз керек және әдісті сүйікті IDE-де жасауыңыз керек. Өйткені алтын қатынасты іздеу үшін барлық осы биттердің оңға және солға жылжулары, яғни қалыпты бөлу - бұл мүлдем қыңыр жігіттерге арналған. Өз басым, мен бір идеядан жақсырақ және тезірек істей алатыныма күмәнданамын.

Қорытындының орнына

Осылайша, әдістердің Java тілінде нақты анықталған рөл equalsатқаратынын және екі an objectінің логикалық теңдік сипаттамасын алуға арналғанын көреміз . hashCodeӘдіс жағдайында equalsбұл an objectілерді салыстыруға тікелей қатысы бар, hashCodeжанама жағдайда, қажет болғанда, айталық, хэш кестелерінде немесе ұқсас деректер құрылымдарында нысанның шамамен орналасуын анықтау үшін an objectіні іздеу жылдамдығын арттыру. Келісімшарттардан басқа , an objectілерді салыстыруға қатысты тағы бір талап бар equals. Бұл интерфейс әдісінің hashCodeсәйкестігі . Бұл талап әзірлеушіні әрқашан қашан қайтаруға міндеттейді . Яғни, біз екі an objectіні логикалық салыстыру қосымшаның ешбір жерінде қайшы келмеуі және әрқашан сәйкес болуы керек екенін көреміз. compareToComparableequalsx.equals(y) == truex.compareTo(y) == 0

Дереккөздер

Тиімді Java, екінші басылым. Джошуа Блох. Өте жақсы кітаптың тегін аудармасы. Java, кәсіби кітапхана. 1-том. Негіздер. Кей Хорстман. Біраз азырақ теория және көп тәжірибе. Бірақ бәрі Блох сияқты егжей-тегжейлі талданбайды. Бірдей тең() бойынша көрініс бар болса да. Суреттердегі деректер құрылымдары. HashMap Java тіліндегі HashMap құрылғысындағы өте пайдалы мақала. Дереккөздерді қараудың орнына.
Пікірлер
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION