Сразу же скажу, что статья во многом опирается базовые понятия алгебры, которые к великому счастью легко и быстро осознаются при помощи всего-лишь метода внимательного разглядывания. Поехали.

В Java так устроено, что любой класс, который вы определяете, наследуется от класса Object. Таким образом класс Object является суперклассом любого класса в любой программе.

Это означает, что абсолютно любой класс содержит методы, которые определены в классе Object. Методы .equals() и .hashcode() - одни из них.

Прежде всего я должен описать главные правила для любых реализаций этих двух методов, которые нужно обязательно соблюдать, запомнить как аксиому:


1). Если x.equals(y) == true, то обязательно hashcode(x) == hashcode(y)

2) Если hashcode(x) == hashcode(y), то не обязательно x.equals(y) == true


Метод .equals()

Отношение эквивалентности (алгебра)

Прежде чем поговорить о методе .equals, я бы хотел рассказать, что такое отношение эквивалентности с точки зрения алгебры (пока-что забудьте про программирование).

Отношение эквивалентности - это бинарное (бинарное - значит между двумя) отношение, которое является:

  • симметричным (для любых x, y выполняется: если x = y, то y = x)

  • рефлексивным (для любого x выполняется: x = x)

  • транзитивным (для любых x, y, z выполняется: если x = y и y = z, то x = z)

Таким образом, если на множестве определено отношение эквивалентности, множество можно разделить на подмножества - классы эквивалентности.

Каждый класс эквивалентности содержит внутри себя только те элементы, которые эквиваленты (более формально - находятся в отношении эквивалентности) между собой.

Реализация .equals() по умолчанию

Метод .equals() в классе Object реализован примерно следующим образом:

public boolean equals(Object x) {
  return(this == y)
}

Фактически он делает следующее: Он принимает в качестве аргумента ссылочную переменную и проверяет, ссылается ли они на тот же объект (ту же область памяти, если быть точнее), что и объект, к которому мы применили метод .equals().

Таким образом, стандартная реализация .equals() выстраивает отношение эквивалентности, которое можно описать так: две ссылки эквивалентны, если они ссылаются на одну и ту же область памяти.

Такая реализация не противоречит математической идеологии, описанной выше. Однако на практике метод .equals() часто переопределяют в подклассах.

Как и зачем переопределяют метод .equals()?

Очевидно, гораздо более применимой будет возможность сравнивать объекты по какому-нибудь другому критерию. Часто метод .equals() переопределяют так, чтобы он сравнивал объекты по значениям их полей.

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

Конкретную кодовую реализацию я приводить не буду, потому что она не так важна, как сама идея

Это и другие возможные переопределения метода .equals() мало того, что расширяют круг наших возможностей, так ещё и не лишают старых, ведь мы по прежнему имеем возможность проверять, ссылаются ли две ссылки на одну область памяти, используя операнд ==, вместо прежнего .equals()

return(ob1 == ob2);

Метод .hashcode()

Сюръекция (алгебра)

Сюръекция - сопоставление элементам множества X элементов второго множества Y, при котором для любого элемента из Y есть хотя-бы один сопоставленный элемент из X.

Если немного более подробно разобрать это определение, то мы увидим следующее:

  • Даже несколько элементов из X могут быть сопоставлены одному и тому же элементу из Y (это называется коллизией).

  • Возможно есть такое элемент из X, и даже возможно не один, что он не сопоставлен никакому элементу из Y. (см. рисунок, всё интуитивно)

C -> Z, D -> Z - коллизия
E элемент, которому ничего не сопоставлено
C -> Z, D -> Z - коллизия E элемент, которому ничего не сопоставлено

Что происходит в java?

Метод .hashcode() как-раз осуществляет сюръекцию. Множеством X выступает множество всевозможных объектов которые мы можем создать, множеством Y выступает область значений типа данных int. Метод .hashcode() вычисляет каким-то скрытым от нас способом целое число, опираясь на объект, к которому применяется.

Единственное отличие метода .hashcode() от сюръекции в том, что любой объект может быть обработан методом .hashcode()

Здесь нет элементов по типу E из пред. рисунка
Здесь нет элементов по типу E из пред. рисунка

Реализация .hashcode() по умолчанию?

Насколько я понял, точно так никто в этом и не разобрался. Есть много версий:

  • Значение .hashcode() - это область памяти, где лежит объект

  • Значение .hashcode() - это число, создаваемое генератором случайных чисел в какой-то момент

  • Сама функция написана не на Java а вообще на C.

И многие другие. В общем каким-то образом она всё же устроена, но самое главное в том, что стандартная реализация .hashcode() со стандартной реализацией .equals() подчиняются правилу, приведённому в самом начале статьи

Как и зачем переопределяют метод .hashcode()?

Основной причиной для изменения метода .hashcode() является то, что желают изменить .equals(), однако смена стандартной реализации .equals() приводит к нарушению правила из начала статьи

Второстепенной причиной для изменения метода .hashcode() является то, что желают изменить вероятность коллизии (эта причина встречается реже)

Конец :)

Комментарии (18)


  1. csl
    27.10.2021 17:49
    +1

    Об ООП: не существует способа расширить класс, порождающий экземпляры, и добавить к нему новый аспект, сохранив при этом соглашения для метода equals. (с)


    1. vsb
      27.10.2021 18:23

      Можете раскрыть мысль подробней? Не очень понятно, в чём именно проблема и какое именно соглашение вызывает проблемы.


      1. csl
        27.10.2021 18:30

        Спасибо вам за вопрос. Метод реализует отношение эквивалентности. Соглашения включают рефлексивность, симметричность, транзитивность, консистентность и отличие от нуля. Проблема: при добавлении аспекта нарушается хотя бы одно из этих соглашений.

        Многие классы, включая классы коллекций, полагаются на то, что объекты, передаваемые им, подчиняются соглашениям (контракту) для этого метода.


    1. mayorovp
      27.10.2021 18:35

      …если только в методе базового класса нет сравнения типов. Но такое сравнение будет работать небыстро.


      1. csl
        27.10.2021 18:36

        О, приветствую, ждал вас в этом треде, вы совершенно правы, удачное замечание.


      1. MFilonen2
        27.10.2021 20:33

        А почему не быстро? Возможность downcast вроде бы означает хранение типа в рантайме… Так сложно два указателя на тип сравнить?


        1. mayorovp
          27.10.2021 20:59

          Вроде и не сложно, но какие-то заморочки были, какие — не помню.


          1. csl
            27.10.2021 21:07

            Присаживайтесь поудобнее, начнём. Вопрос в том, используется == или вызов метода equals. В последнем случае накладные расходы выше. Пример более быстрого сравнения, начиная с Java 5 - сравнение членов перечисления Enum.


  1. sshikov
    27.10.2021 18:03
    +2

    Во-первых, hashСode, и имена в Java case sensitive. Это все, что нужно знать о качестве данной статьи. Ну и во-вторых, в этом месте практически ничего не менялось с выпуска версии 1.0, т.е. с января 1996 года. То есть, статья опоздала примерно лет на 25.


    1. csl
      27.10.2021 22:16

      После версии 1.0 добавились, например, Arrays.hashCode(), Objects.hashCode(), Objects.hash() .


      1. sshikov
        27.10.2021 22:19

        Ну, вообще-то как раз про них тут и ни слова.


  1. sshikov
    27.10.2021 18:10
    +5

    Реализация .hashcode() по умолчанию?
    Насколько я понял, точно так никто в этом и не разобрался.


    Ага, ага. Только ответ ищется за две минуты, и содержит ссылку на код. А для тех кто читал Хабр, прежде чем начал писать, несложно и на хабре найти парочку статей, гораздо глубже этой.



  1. Throwable
    27.10.2021 19:22
    +1

    Легко проследить, что такое определение не противоречит математической идеологии.

    Легко заметить, что противоречит:

    симметричным (для любых xy выполняется: если x = y, то y = x)

    String a = "Test";
    String b = null;
    a.equals(b)	// false
    b.equals(a)	// NPE

    Строгую математическую семантику соблюдает Objects.equals(). И именно его нужно было использовать с самого начала для оператора "==". Однако создателям Java казалось очень важным каждый обращать внимание пользователей на то, как они реализовали equals(), и что сравнивать строки при помощи "==" в их языке хоть и можно, но будет неправильно. Даром что ссылочное сравнение используется в одном случае из ста. Но уж теперь ничего не поделаешь.


    1. urvanov
      27.10.2021 21:29

      Иногда ещё и equals переопределяют так, что при сравнении типов сравнивают их неправильно через instanceof, а не через getClass. И тогда вполне может быть, что

      a.equals(b) // true
      b.equals(a) // false


      1. csl
        27.10.2021 21:40

        Сравнивая через getClass, вы подразумеваете экземпляр строго одного типа (ведь через instanceof можете более гибко сравнить подтипы)?


        1. urvanov
          29.10.2021 00:46
          +1

          Да, именно это имею в виду.

          Если один класс наследуется от другого, то при реализации equals через instanceof это правило симметрии нарушается.


  1. Naf2000
    31.10.2021 08:59

    Пару замечаний чисто про математику:

    Возможно есть такое элемент из X, и даже возможно не один, что он не сопоставлен никакому элементу из Y

    тогда эта функция не определена на всём X. Другое дело что не всем Y могут найтись из X в hashCode, а это запросто. Тогда это не сюрьекция.

    И второе: считаю уместным отметить, что любому отображению соответствует отношение эквивалентности по правилу: два элемента эквивалентны если и только если их образы отображений совпадают. Это даёт наглядный пример, как можно устроить equals через hashCode. Ни в коем случае к этому не призываю.

    Верно и обратное: если есть отношение эквивалентности, то ему соответствует отображение исходного множества в его фактор-множество по этой эквивалентности. Но это так ;-)