Метод hashCode в Java. Его переопределение

Search Icon

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

Если у любого объекта вызвать метод hashCode, то его реализация по умолчанию вернет нам число.

Это случайное уникальное число, которое генерируется этим методом по умолчанию.

Переопределять его нужно если мы собираемся запихивать в HashSet или HashMap не простые элементы типа char, int, String и т.д., а объекты.

То есть, например, так – set.add(new MyClass(1, 34));.

Вот например мы только что записали в set объект new MyClass(1, 34) и у нас в классе MyClass пока не переопределен hashCode и если мы теперь запишем в этот же set такой же объект new MyClass(1, 34) еще раз вот так – set.add(new MyClass(1, 34));, то в set уже будет ДВА элемента.

А это не должно быть так! Так как мы помним, что ни в HashSet, ни в HashMap одинаковые ключи храниться не должны.

Почему же если мы записываем в hashset идентичные объекты, как ключи, то hashset рассматривает их как разные ключи?

Реализация метода hashCode по умолчанию генерирует разные ключи всем объектам ДАЖЕ ЕСЛИ ОНИ ИДЕНТИЧНЫ по своему содержанию.

То есть если мы создаем объект new MyClass(1, 34) в первый раз, то у него будет свой hashCode, когда мы создаем new MyClass(1, 34) второй раз, у него уже будет другой hashCode.

Каждый объект имеет свой hashCode. И hashset добавляет объекты в себя по этому hashCode.

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

Как же нам переопределить hashCode, чтобы идентичные объекты всегда записывались в один и тот же linkedlist?

Нам нужно переопределить hashCode так, чтобы он возвращал данные объекта представленные одним восьмибитным числом.

То есть все значения полей объекта нам нужно каким-то образом скомпановать в одно восьмибитное число, которое будет возвращать hashCode.

И теперь hashCode всех идентичных объектов, например, new MyClass(1, 34) всегда будет возвращать один и тот же hashCode, так как он является скомпонованными полями объекта, а поля у идентичных объектов new MyClass(1, 34) одинаковые – 1 и 34.

Но на этом еще не всё. Если объекты одинаковы по hashCode, это только значит, что они попадут в один и тот же самый linkedlist, это еще не обязательно значит, что они одинаковы полностью.

HashSet еще будет сравнивать добавляемый в него объект со всеми уже присутствующими в hashset элементами методом equals и если он НЕ найдет там методом equals идентичный элемент, но при этом объект с таким hashCode уже там присутствует, то в hashset всё равно добавиться этот добавляемый объект и в итоге в нем будет два элемента с одинаковыми hashCode.

Search Icon

Поэтому, чтобы в HashSet не было идентичных объектов, обязательно вместе с hashCode должен быть переопределен и equals.


Переопределение HashCode

Пример программы:

import java.util.*; class MyClass implements Cloneable { int myA; SomeClass myB; MyClass(int myA, SomeClass myB){ this.myA = myA; this.myB = myB; } // hashCode – данные класса представлены одним восьмибитным числом. // По умолчанию, если не переопределять, это случайное уникальное число. @Override public int hashCode() { System.out.println(“HashCode is called: ” + this); // Это стандартное переопределение. Не заморачивайтесь почему 31, // почему умножение. нам лишь важно в result // добавить все поля класса, так как result это и есть // это самое восьмибитное число, состоящее из полей класса. final int prime = 31; int result = 1; // Числовые переменные просто добавляем. result = prime * result + myA; // У полей объектов вызываем hashCode. // Метод hashCode в классе этих объектов // тоже должен быть определен таким же образом. result = prime * result + myB.hashCode(); return result; } // equals обязательно тоже переопределяем, как уже было сказано @Override public boolean equals(Object obj) { System.out.println(“Equals is called:” + this + ” : ” + obj); if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; MyClass other = (MyClass) obj; if (myA != other.myA) return false; if (!myB.equals(other.myB)) return false; return true; } @Override public String toString() { return “MyClass{” + “myA=” + myA + “, myB='” + myB + ‘\” + ‘}’; } @Override public SomeClass clone() throws CloneNotSupportedException { Object obj = super.clone(); SomeClass someClass = (SomeClass) obj; return someClass; } } class SomeClass implements Cloneable { int someVar; SomeClass(int someVar){ this.someVar = someVar; } // Здесь тоже переопределены hashCode и equals @Override public int hashCode() { System.out.println(“HashCode is called:” + this); final int prime = 31; int result = 1; result = prime * result + someVar; return result; } @Override public boolean equals(Object obj) { System.out.println(“Equals is called:” + this + ” : ” + obj); if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; SomeClass other = (SomeClass) obj; if (someVar != other.someVar) return false; return true; } @Override public SomeClass clone() throws CloneNotSupportedException { Object obj = super.clone(); SomeClass someclass = (SomeClass)obj; return someclass; } public String toString(){ return “SomeClass{” + “someVar=” + someVar + ‘}’; } } public class hashCodeLesson { public static void main(String[] args) throws CloneNotSupportedException { Set< MyClass > set = new HashSet<>(); set.add(new MyClass(1, new SomeClass(34))); set.add(new MyClass(1, new SomeClass(36))); set.add(new MyClass(1, new SomeClass(34))); set.add(new MyClass(2, new SomeClass(26))); set.add(new MyClass(3, new SomeClass(75))); System.out.println(“SIZE:” + set.size()); // По результатам можно увидеть, что размер коллекции 4, // так как для 1 и 3 элементов хеш-коды одинаковые, // и equals сравнил поля, которые оказались одинаковыми. // Если бы hashCode не был переопределен, элементов было бы 5. } }

Вывод:


Последовательность добавления элементов в HashSet

Также стоит упомянуть некоторые детали последовательности добавления элементов в HashSet.

При добавлении ключа в HashSet и расчета его hashCode происходит сравнение этого hashCode с hashCode каждого элемента в HashSet, и если hashCode очередного добавляемого объекта отличается от всех остальных уже присутствующих в коллекции, то ключ добавляется СРАЗУ, без сравнения по equals.

Если же такой же hashCode нашелся, то происходит сравнение по equals, и если этим методом не найдет такого же элемента, то произойдет добавление.

В этом уроке был приведен пример стандартного переопределения hashCode.

Search Icon

Таким вот образом его нужно переопределять почти всегда когда вы работаете с hash коллекциями.

Коллекции. Интерфейс List

Разберитесь с интерфейсом List в Java: ArrayList, LinkedList и Vector. Узнайте, как работать с динамическими массивами и их реализациями.
Time to read: 10

Iterator в коллекциях Java

Изучите Iterator в Java: универсальный способ перебора любых коллекций без знания их внутренней структуры. Примеры кода и объяснение работы.
Time to read: 8

Интерфейс Queue в Java

Освойте PriorityQueue и принцип FIFO: как работает приоритетная очередь, сортировка элементов и удаление по приоритету. Подробные примеры на Java.
Time to read: 9