Автор оригинала: Michał Dąbrowski.
1. введение
В этом уроке мы узнаем, как использовать массив байтов в качестве ключа в HashMap . Из-за того, как работает HashMap , мы, к сожалению, не можем сделать это напрямую. Мы выясним, почему это так, и рассмотрим несколько способов решения этой проблемы.
2. Разработка хорошего ключа для HashMap
2.1. Как работает HashMap
HashMap использует механизм хеширования для хранения и извлечения значений из самого себя. Когда мы вызываем метод put(ключ, значение) , HashMap вычисляет хэш-код на основе метода hashCode() ключа. Этот хэш используется для идентификации корзины, в которой в конечном итоге хранится значение:
public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }
Когда мы извлекаем значение с помощью метода get(key) , происходит аналогичный процесс. Ключ используется для вычисления хэш-кода, а затем для поиска корзины. Затем каждая запись в корзине проверяется на равенство с помощью метода equals () . Наконец, возвращается значение соответствующей записи:
public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; }
2.2. Контракт между equals() и хэш-кодом()
Оба метода equals и hashCode имеют контракты , которые следует соблюдать. В контексте HashMaps особенно важен один аспект: объекты , равные друг другу, должны возвращать один и тот же хэш-код . Однако объекты, возвращающие один и тот же хэш-код , не обязательно должны быть равны друг другу. Вот почему мы можем хранить несколько значений в одном ведре.
2.3. Неизменность
Хэш-код ключа в HashMap не должен изменяться. Хотя это не обязательно, настоятельно рекомендуется, чтобы ключи были неизменяемыми. Если объект является неизменяемым, его хэш-код не будет иметь возможности измениться, независимо от реализации метода hashCode .
По умолчанию хэш вычисляется на основе всех полей объекта. Если мы хотим иметь изменяемый ключ, нам нужно переопределить метод hashCode , чтобы гарантировать, что изменяемые поля не используются при его вычислении. Чтобы сохранить контракт, нам также нужно будет изменить метод equals .
2.4. Значимое равенство
Чтобы иметь возможность успешно извлекать значения из карты, равенство должно быть значимым. В большинстве случаев нам нужно иметь возможность создать новый ключевой объект, который будет равен некоторому существующему ключу на карте. По этой причине идентификация объекта не очень полезна в этом контексте.
Это также основная причина, по которой использование примитивного массива байтов на самом деле не является вариантом. Массивы в Java используют идентификатор объекта для определения равенства. Если мы создадим HashMap с массивом байтов в качестве ключа, мы сможем получить значение только с использованием точно такого же объекта массива.
Давайте создадим наивную реализацию с массивом байтов в качестве ключа:
byte[] key1 = {1, 2, 3}; byte[] key2 = {1, 2, 3}; Mapmap = new HashMap<>(); map.put(key1, "value1"); map.put(key2, "value2");
Мало того, что у нас есть две записи с практически одинаковым ключом, но также мы не можем ничего извлечь, используя вновь созданный массив с одинаковыми значениями:
String retrievedValue1 = map.get(key1); String retrievedValue2 = map.get(key2); String retrievedValue3 = map.get(new byte[]{1, 2, 3}); assertThat(retrievedValue1).isEqualTo("value1"); assertThat(retrievedValue2).isEqualTo("value2"); assertThat(retrievedValue3).isNull();
3. Использование Существующих Контейнеров
Вместо массива байтов мы можем использовать существующие классы, реализация равенства которых основана на содержании, а не на идентичности объекта.
3.1. Строка
Строка равенство основано на содержимом массива символов:
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = count; if (n == anotherString.count) { char v1[] = value; char v2[] = anotherString.value; int i = offset; int j = anotherString.offset; while (n-- != 0) { if (v1[i++] != v2[j++]) return false; } return true; } } return false; }
String s также неизменяемы, и создание String на основе массива байтов довольно просто. Мы можем легко кодировать и декодировать Строку , используя схему Base64 :
String key1 = Base64.getEncoder().encodeToString(new byte[]{1, 2, 3}); String key2 = Base64.getEncoder().encodeToString(new byte[]{1, 2, 3});
Теперь мы можем создать HashMap с String в качестве ключей вместо массивов байтов. Мы поместим значения в Map аналогично предыдущему примеру:
Mapmap = new HashMap<>(); map.put(key1, "value1"); map.put(key2, "value2");
Затем мы можем извлечь значение из карты. Для обоих ключей мы получим одно и то же второе значение. Мы также можем проверить, действительно ли ключи равны друг другу:
String retrievedValue1 = map.get(key1); String retrievedValue2 = map.get(key2); assertThat(key1).isEqualTo(key2); assertThat(retrievedValue1).isEqualTo("value2"); assertThat(retrievedValue2).isEqualTo("value2");
3.2. Списки
Аналогично String , метод List#equals будет проверять равенство каждого из его элементов. Если эти элементы имеют разумный метод equals() и являются неизменяемыми, List будет корректно работать в качестве ключа HashMap . Нам нужно только убедиться, что мы используем неизменяемую Список реализацию :
Listkey1 = ImmutableList.of((byte)1, (byte)2, (byte)3); List key2 = ImmutableList.of((byte)1, (byte)2, (byte)3); Map , String> map = new HashMap<>(); map.put(key1, "value1"); map.put(key2, "value2"); assertThat(map.get(key1)).isEqualTo(map.get(key2));
Имейте в виду, что Список объекта Byte займет намного больше памяти, чем массив примитивов byte . Таким образом, это решение, хотя и удобное, не является жизнеспособным для большинства сценариев.
4. Реализация Пользовательского Контейнера
Мы также можем реализовать нашу собственную оболочку, чтобы полностью контролировать вычисление хэш-кода и равенство. Таким образом, мы можем убедиться, что решение быстрое и не требует большого объема памяти.
Давайте создадим класс с одним последним закрытым полем byte array. У него не будет сеттера, и его геттер сделает защитную копию, чтобы обеспечить полную неизменность:
public final class BytesKey { private final byte[] array; public BytesKey(byte[] array) { this.array = array; } public byte[] getArray() { return array.clone(); } }
Нам также необходимо реализовать наши собственные методы equals и hashCode . К счастью, мы можем использовать класс утилиты Arrays для обеих этих задач:
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; BytesKey bytesKey = (BytesKey) o; return Arrays.equals(array, bytesKey.array); } @Override public int hashCode() { return Arrays.hashCode(array); }
Наконец, мы можем использовать нашу оболочку в качестве ключа в HashMap :
BytesKey key1 = new BytesKey(new byte[]{1, 2, 3}); BytesKey key2 = new BytesKey(new byte[]{1, 2, 3}); Mapmap = new HashMap<>(); map.put(key1, "value1"); map.put(key2, "value2");
Затем мы можем получить второе значение, используя любой из объявленных ключей, или мы можем использовать один, созданный на лету:
String retrievedValue1 = map.get(key1); String retrievedValue2 = map.get(key2); String retrievedValue3 = map.get(new BytesKey(new byte[]{1, 2, 3})); assertThat(retrievedValue1).isEqualTo("value2"); assertThat(retrievedValue2).isEqualTo("value2"); assertThat(retrievedValue3).isEqualTo("value2");
5. Заключение
В этом уроке мы рассмотрели различные проблемы и решения для использования массива byte в качестве ключа в HashMap . Во-первых, мы исследовали, почему мы не можем использовать массивы в качестве ключей. Затем мы использовали некоторые встроенные контейнеры, чтобы смягчить эту проблему, и, наконец, реализовали нашу собственную оболочку.
Как обычно, исходный код этого учебника можно найти на GitHub .