Можно ли использовать объект в качестве ключа для HashMap в Java? Это действительно очень популярный вопрос на собеседовании. Его задают сразу после «как работает HashMap?». Давайте порассуждаем вокруг определяемого пользователем класса в качестве ключа в hashmap в Java.
1. Ключ должен соблюдать контракт между hashCode() и equals()
Основная потребность в разработке хорошего ключа заключается в том, что «мы должны иметь возможность извлекать объект-значение обратно из карты без сбоев», в противном случае, какую бы сложную структуру данных вы ни построили, она будет бесполезна.
Чтобы решить, что мы создали хороший ключ, мы ДОЛЖНЫ знать « как работает HashMap? ». Я оставлю часть того, как работает hashmap, вам для прочтения из связанного поста, но вкратце, он работает по принципу хеширования.
В HashMap hashcode() ключа используется в основном в сочетании с методом equals() для помещения ключа в карту и последующего его получения из карты. Поэтому наша единственная точка фокусировки — эти два метода.
- equals() – проверяет равенство двух объектов, ключей в нашем случае. Переопределить для предоставления логики для сравнения двух ключей.
- hashcode() – возвращает уникальное целочисленное значение для ключа во время выполнения. Это значение используется для определения местоположения контейнера на карте.
Переопределение hashCode() обычно необходимо всякий раз, когда переопределяется equals(), чтобы сохранить общий контракт для метода hashCode(), который гласит, что равные объекты должны иметь равные хеш-коды.
2. Что делать, если разрешено изменение хэш-кода ключа?
Как указано выше, хэш-код помогает вычислить позицию контейнера для хранения пары ключ-значение в Map. Разные значения хэш-кода могут ссылаться на разные местоположения контейнеров.
Если случайно хэш-код объекта ключа изменится после того, как мы поместили пару ключ-значение в карту, то будет практически невозможно извлечь объект значения обратно из карты, поскольку мы не знаем, в какой контейнер мы поместили ключ-значение в прошлом. Старая пара ключ-значение недоступна, и поэтому это случай утечки памяти.
Во время выполнения JVM вычисляет хэш-код для каждого объекта и предоставляет его по требованию. Когда мы изменяем состояние объекта, JVM устанавливает флаг, что объект изменен и хэш-код должен быть СНОВА вычислен. Поэтому в следующий раз, когда вы вызываете метод hashCode() объекта, JVM пересчитывает хэш-код для этого объекта.
3. Мы должны сделать ключ HashMap неизменяемым.
Для вышеприведенных основных рассуждений предполагается, что ключевые объекты являются неизменяемыми. Неизменяемость гарантирует, что мы будем получать один и тот же хэш-код каждый раз для ключевого объекта. Так что это фактически решает почти все проблемы за один раз. Но, опять же, такой класс должен соблюдать контракт методов hashCode() и equals().
Это главная причина, по которой неизменяемые классы, такие как String, Integer или другие классы-оболочки, являются хорошими кандидатами на роль ключевых объектов. И это ответ на вопрос, почему string является популярным ключом хэш-карты в Java?
Но помните, что неизменяемость рекомендуется, а не обязательна. Если вы хотите сделать изменяемый объект ключом в хэш-карте, то вы должны убедиться, что изменение состояния для ключевого объекта не изменит хэш-код объекта. Это можно сделать, переопределив метод hashCode(). Но вы должны убедиться, что вы также соблюдаете контракт с equals().
4. Пример пользовательского ключа HashMap
Пример всегда лучше для демонстрации, не так ли? Тогда давайте его приведем.
В этом примере я создал класс Account только с двумя полями для простоты. Я переопределил хэш-код и метод equals таким образом, чтобы он использовал только номер счета для проверки уникальности объекта Account. Все остальные возможные атрибуты класса Account можно изменить во время выполнения.
public class Account{private int accountNumber;private String holderName;public Account(int accountNumber) {this.accountNumber = accountNumber;}public String getHolderName() {return holderName;}public void setHolderName(String holderName) {this.holderName = holderName;}public int getAccountNumber() {return accountNumber;}//Depends only on account number@Overridepublic int hashCode() {final int prime = 31;int result = 1;result = prime * result + accountNumber;return result;}//Compare only account numbers@Overridepublic boolean equals(Object obj) {if(this == obj)return true;if(obj == null)return false;if(getClass() != obj.getClass())return false;Account other =(Account) obj;if(accountNumber != other.accountNumber)return false;return true;}}
Вызовет ли это какое-либо нежелательное поведение???
НЕТ, не будет. Причина в том, что реализация класса Account соблюдает контракт, что «Равные объекты должны создавать одинаковый хэш-код, пока они равны, однако неравные объекты не обязаны создавать разные хэш-коды». т.е.
- Если a.equals(b) имеет значение true, то a.hashCode() должен быть таким же, как b.hashCode().
- Если a.equals(b) имеет значение false, то a.hashCode() может совпадать с b.hashCode(), а может и не совпадать.
5. Демонстрация
Давайте проверим наш класс Account для вышеприведенного анализа.
//Create a HashMap with mutable keyHashMap<Account, String> map = new HashMap<Account, String>();//Create key 1Account a1 = new Account(1);a1.setHolderName("A_ONE");//Create key 2Account a2 = new Account(2);a2.setHolderName("A_TWO");//Put mutable key and value in mapmap.put(a1, a1.getHolderName());map.put(a2, a2.getHolderName());//Change the keys state so hash map should be calculated againa1.setHolderName("Defaulter");a2.setHolderName("Bankrupt");//Success !! We are able to get back the valuesSystem.out.println(map.get(a1)); //Prints A_ONESystem.out.println(map.get(a2)); //Prints A_TWO//Try with newly created key with same account numberAccount a3 = new Account(1);a3.setHolderName("A_THREE");//Success !! We are still able to get back the value for account number 1System.out.println(map.get(a3)); //Prints A_ONE
Вывод программы.
A_ONEA_TWOA_ONE
6. Заключение
В этом уроке мы научились разрабатывать класс, который можно использовать в качестве ключа в экземплярах карты для хранения пар ключ-значение.
В качестве наилучшей практики:
- ключевой класс следует сделать неизменяемым.
- В большинстве ситуаций методы hashCode() и equals() по умолчанию вполне хороши, но если мы переопределяем один метод, то нам следует переопределить и другой метод, чтобы убедиться, что они следуют контракту между ними.
Вот как я понимаю разработку пользовательского объекта ключа для HashMap.