Эта статья приведет нас к изучению того, как использовать массив байтов в качестве ключа в HashMap. Механизм HashMap не позволяет нам сделать это напрямую. Давайте рассмотрим, почему это происходит, и несколько возможных решений для этой ситуации.
Как работает HashMap
HashMap — это структура данных, использующая механизм хеширования для хранения и извлечения значений. Использование хеш-кодов для хранения и извлечения значений может значительно улучшить производительность HashMap, поскольку может поддерживать временную сложность поиска пар ключ-значение на уровне O (1) O(1)О ( 1 ) уровень. Конечно, это также требует от насhashCode()
как можно более равномерного распределения хеш-кодов при реализации метода, чтобы избежать конфликтов хэшей и повлиять на эффективность HashMap.
Когда мы вызываем put(key, value)
метод, HashMap будет hashCode()
вычислять хеш-код по методу ключа. Этот хэш-код используется для определения корзины, в которой в конечном итоге хранится значение:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
Когда метод используется get(key)
для извлечения значения, он проходит ряд этапов обработки: сначала вычисляется хеш-код из ключа, а затем находится хэш-сегмент. Затем используйте equals()
этот метод, чтобы проверить, соответствует ли ключу каждая запись в ведре. Наконец, возвращается значение соответствующей записи:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
equals
и hashCode
метод
В программировании на Java equals
методы и hashCode
методы имеют правила, которым следует следовать. В структуре данных HashMap особенно важен один аспект: equals
объекты с одним и тем же результатом сравнения методов должны возвращать одно и то же хеш-значение. Однако обратное не обязательно верно, то есть объекты с одинаковым значением хеш-функции не обязательно имеют одинаковый equals
результат сравнения методов. Это также причина, по которой мы можем хранить несколько объектов в одном сегменте HashMap.
При использовании HashMap рекомендуется не менять хеш-значение ключа. Хотя это не является обязательным, настоятельно рекомендуется определять ключи как неизменяемые объекты. Если объект неизменяем, hashCode
его хэш-значение не изменится независимо от реализации метода.
По умолчанию хеш-значение вычисляется на основе всех полей объекта. Если нам нужно использовать изменяемые ключи, нам нужно переопределить hashCode
метод, чтобы гарантировать, что его вычисления не включают изменяемые поля. Чтобы сохранить это правило, нам также необходимо изменить метод equals.
Использовать массив байтов в качестве ключа
Чтобы можно было успешно получить значение из карты, равенство должно быть значимым. Это основная причина, по которой использование массивов байтов на самом деле не вариант. В Java массивы используют идентификатор объекта для определения равенства. Если мы создадим HashMap, используя массив байтов в качестве ключа, то для извлечения значения можно будет использовать только тот же самый объект массива.
Давайте создадим простой пример, используя байтовые массивы в качестве ключей:
byte[] key1 = {
1, 2, 3};
byte[] key2 = {
1, 2, 3};
Map<byte[], String> map = new HashMap<>();
map.put(key1, "value1");
map.put(key2, "value2");
System.out.println(map.get(key1));
System.out.println(map.get(key2));
System.out.println(map.get(new byte[]{
1, 2, 3}));
У нас есть два одинаковых ключа, но мы не можем ничего получить, используя только что созданный массив с тем же значением, и результат следующий:
value1
value2
null
Решение
использоватьString
String
Равенство основано на содержимом массива символов:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
Строки также неизменяемы, и создать строку из массива байтов очень просто. Мы можем легко кодировать и декодировать строки с помощью Base64, а затем создать HashMap, который использует строки в качестве ключей вместо байтовых массивов:
String key1 = Base64.getEncoder().encodeToString(new byte[]{
1, 2, 3});
String key2 = Base64.getEncoder().encodeToString(new byte[]{
1, 2, 3});
Map<String, String> map = new HashMap<>();
map.put(key1, "value1");
map.put(key2, "value2");
System.out.println(map.get(key1));
System.out.println(map.get(key2));
System.out.println(map.get(Base64.getEncoder().encodeToString(new byte[]{
1, 2, 3})));
Результат операции следующий:
value2
value2
value2
Примечание.String
При преобразовании массива байтов в формат . Поэтому это решение не .
использоватьList
Подобно String
, List#equals
метод будет проверять каждый из своих элементов на равенство:
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof List))
return false;
ListIterator<E> e1 = listIterator();
ListIterator<?> e2 = ((List<?>) o).listIterator();
while (e1.hasNext() && e2.hasNext()) {
E o1 = e1.next();
Object o2 = e2.next();
if (!(o1==null ? o2==null : o1.equals(o2)))
return false;
}
return !(e1.hasNext() || e2.hasNext());
}
Список будет правильно работать как ключ HashMap, если эти элементы имеют разумный метод equals() и являются неизменяемыми. Нам просто нужно убедиться, что используется неизменяемая реализация List:
List<Byte> key1 = ImmutableList.of((byte) 1, (byte) 2, (byte) 3);
List<Byte> key2 = ImmutableList.of((byte) 1, (byte) 2, (byte) 3);
Map<List<Byte>, String> map = new HashMap<>();
map.put(key1, "value1");
map.put(key2, "value2");
System.out.println(map.get(key1));
System.out.println(map.get(key2));
System.out.println(map.get(ImmutableList.of((byte) 1, (byte) 2, (byte) 3)));
Результат операции следующий:
value2
value2
value2
Примечание. Список объектов Byte займет больше памяти, чем массив байтов. Поэтому это решение не .
пользовательский класс ( 推荐使用
)
Мы также можем определить собственный класс, чтобы полностью контролировать вычисление хэш-кода и равенство. Таким образом, мы можем гарантировать, что решение будет быстрым и не потребует большого объема памяти.
Давайте создадим класс только final
с одним закрытым полем массива байтов. У него не будет методов установки, только методы получения, чтобы обеспечить полную неизменность.
Затем реализуйте свои собственные equals
и hashCode
методы. Для методов мы можем использовать Arrays
классы для выполнения этих двух задач.Окончательный код выглядит следующим образом:
public class BytesKey {
private final byte[] array;
public BytesKey(byte[] array) {
this.array = array;
}
public byte[] getArray() {
return array.clone();
}
@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});
Map<BytesKey, String> map = new HashMap<>();
map.put(key1, "value1");
map.put(key2, "value2");
System.out.println(map.get(key1));
System.out.println(map.get(key2));
System.out.println(map.get(new BytesKey(new byte[]{
1, 2, 3})));
Результат операции следующий:
value2
value2
value2
Примечание. Пользовательский класс не имеет ни String
потери производительности, ни использования памяти списка объектов Byte. Поэтому рекомендуется это решение .
Подведем итог
В этой статье будут рассмотрены проблемы и решения, возникающие при использовании массива байтов в качестве ключа при использовании HashMap.
Во-первых, мы рассмотрим, почему вы не можете напрямую использовать массивы в качестве ключей. При использовании HashMap нам необходимо обеспечить уникальность каждого ключа, а использование массива в качестве ключа может вызвать конфликты. Это связано с тем, что значение хэш-кода массива вычисляется на основе его адреса в памяти, поэтому даже если содержимое двух массивов совершенно одинаково, их расположение в памяти различно, и их хэш-код будет другим. Поэтому использование массива напрямую в качестве ключа может привести к некорректному доступу к значениям или неожиданным перезаписям.
Далее мы представим метод использования двух структур данных, String и List, в качестве временного решения. Они являются сопоставимыми и хешируемыми структурами данных, которые могут гарантировать уникальность. Но этот метод не является идеальным решением, потому что использование String или List в качестве ключа приведет к снижению производительности или ненужному объему памяти.
Наконец, мы отлично решим эту проблему, настроив класс. Этот пользовательский класс содержит поле массива байтов, а также переопределения hashCode
и equals
методы для обеспечения уникальности и правильности. Таким образом, мы можем избежать проблем с производительностью и использованием памяти при использовании String или List и можем достичь более высокой эффективности, гарантируя правильность.