Шаг в яму: используйте массив байтов в качестве ключа Map в Java

Эта статья приведет нас к изучению того, как использовать массив байтов в качестве ключа в 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 и можем достичь более высокой эффективности, гарантируя правильность.

Guess you like

Origin blog.csdn.net/heihaozi/article/details/130398856