Використання байтового масиву як ключа мапи


76

Ви бачите проблему з використанням байтового масиву як ключа мапи? Я також міг би робити new String(byte[])і хеш, Stringале це простіше у використанні byte[].

Відповіді:


65

Проблема полягає в тому, що byte[]використовується ідентифікація об’єкта для equalsі hashCode, так що

byte[] b1 = {1, 2, 3}
byte[] b2 = {1, 2, 3}

не збігатиметься в a HashMap. Я бачу три варіанти:

  1. Обертаючи a String, але тоді ви повинні бути обережними щодо питань кодування (потрібно переконатися, що байт -> String -> byte дає вам однакові байти).
  2. Використання List<Byte>(може бути дорогим в пам’яті).
  3. Виконайте власний клас обтікання, запису hashCodeта equalsвикористання вмісту байтового масиву.

3
Я вирішив проблему переносу рядків за допомогою шістнадцяткового кодування. Ви можете використати кодування base64.
metadaddy

1
Параметр класу обгортання / обробки є простим і повинен бути дуже читабельним.
ZX9

79

Це нормально, якщо ви хочете лише посилальну рівність для вашого ключа - масиви не реалізують "рівність значень" так, як ви, мабуть, хотіли б. Наприклад:

byte[] array1 = new byte[1];
byte[] array2 = new byte[1];

System.out.println(array1.equals(array2));
System.out.println(array1.hashCode());
System.out.println(array2.hashCode());

друкує щось на зразок:

false
1671711
11394033

(Фактичні цифри не мають значення; важливий той факт, що вони різні.)

Припускаючи, що ви насправді хочете рівності, я пропоную вам створити свою власну обгортку, яка містить a byte[]та реалізує рівність та генерування хеш-коду належним чином:

public final class ByteArrayWrapper
{
    private final byte[] data;

    public ByteArrayWrapper(byte[] data)
    {
        if (data == null)
        {
            throw new NullPointerException();
        }
        this.data = data;
    }

    @Override
    public boolean equals(Object other)
    {
        if (!(other instanceof ByteArrayWrapper))
        {
            return false;
        }
        return Arrays.equals(data, ((ByteArrayWrapper)other).data);
    }

    @Override
    public int hashCode()
    {
        return Arrays.hashCode(data);
    }
}

Зверніть увагу, що якщо ви зміните значення в масиві байтів після використання ByteArrayWrapper, як ключа в HashMap(і т.д.), у вас будуть проблеми з пошуком ключа знову ... ви можете зробити копію даних у ByteArrayWrapperконструкторі, якщо хочете , але, очевидно, це буде марною продуктивністю, якщо ви знаєте , що не будете змінювати вміст байтового масиву.

РЕДАГУВАТИ: Як згадувалося в коментарях, ви також можете використовувати ByteBufferдля цього (зокрема, його ByteBuffer#wrap(byte[])метод). Я не знаю, чи це дійсно правильно, враховуючи всі додаткові здібності, ByteBufferякі вам не потрібні, але це варіант.


@dfa: Тест "instanceof" обробляє нульовий регістр.
Джон Скіт,

4
Ще кілька речей, які ви можете додати до реалізації обгортки: 1. Візьміть копію байта [] на конструкції, отже гарантуючи, що об’єкт незмінний, тобто немає небезпеки, що хеш-код вашого ключа зміниться з часом. 2. Попередньо обчисліть і збережіть хеш-код один раз (припускаючи, що швидкість важливіша, ніж накладні витрати на зберігання).
Адамскі

2
@Adamski: Я згадую про можливість копіювання в кінці відповіді. У деяких випадках це правильно, але в інших - не. Можливо, я хотів би зробити це опцією (можливо, статичні методи замість конструкторів - copyOf та wrapperAround). Зауважте, що без копіювання ви можете змінювати базовий масив, поки спочатку не візьмете хеш і не перевірите рівність, що може бути корисним у деяких ситуаціях.
Джон Скіт,

На жаль - Вибачте Джона; Я пропустив цю частину Вашої відповіді.
Адамскі

3
Просто хотів зазначити, що клас java.nio.ByteBuffer по суті робить все, що робить ваша обгортка, хоча з тим самим застереженням, що ви повинні використовувати його лише в тому випадку, якщо вміст байтового масиву не зміниться. Можливо, ви захочете змінити свою відповідь, щоб згадати про неї.
Ед Ануфф,

46

Для цього ми можемо використовувати ByteBuffer (це в основному обгортка байтів [] із компаратором)

HashMap<ByteBuffer, byte[]> kvs = new HashMap<ByteBuffer, byte[]>();
byte[] k1 = new byte[]{1,2 ,3};
byte[] k2 = new byte[]{1,2 ,3};
byte[] val = new byte[]{12,23,43,4};

kvs.put(ByteBuffer.wrap(k1), val);
System.out.println(kvs.containsKey(ByteBuffer.wrap(k2)));

надрукує

true

2
+1 для найлегшої обгортки масивів байтів (я думаю ...)
Ніколас

7
Це працює нормально з ByteBuffer.wrap (), але будьте обережні, якщо вміст ByteBuffer було створено за допомогою пари викликів put () для створення складеного масиву байтів ключів. У цьому випадку за останнім викликом put () повинен слідувати виклик rewind () - інакше equals () повертає true, навіть якщо базові масиви байтів містять різні дані.
RenniePet

Це було б непоганим рішенням, але якщо ви хочете серіалізувати карту (як у моєму випадку), ви не можете використовувати цей підхід.
501 - не реалізовано

Зверніть увагу: "Оскільки хеш-коди буфера залежать від вмісту, недоцільно використовувати буфери як ключі в хеш-картах або подібних структурах даних, якщо не відомо, що їх вміст не зміниться." ( Docs.oracle.com/javase/7 / docs / api / java / nio /… )
LMD

Вам слід ByteBuffer.wrap(k1.clone())зробити захисну копію масиву. Якщо ні, якщо хтось змінить масив, траплятимуться погані речі. Дивлячись у налагоджувачі, ByteBuffer має багато внутрішнього стану в порівнянні зі рядком, тому здається, що це насправді не є легким рішенням з точки зору витрат пам'яті.
simbo1905

11

Ви могли б використовувати java.math.BigInteger. Він має BigInteger(byte[] val)конструктор. Це посилальний тип, тому його можна використовувати як ключ для хеш-таблиці. А .equals()та .hashCode()визначаються як для відповідних цілих чисел, що означає BigInteger має послідовну дорівнює семантику, Byte [] масиву.


17
Звучить привабливо, але це неправильно, оскільки два масиви, що відрізняються лише початковими нульовими елементами (скажімо, {0,100}і {100}), дадуть одне і те ж BigInteger
leonbloy

Хороший момент @leonbloy. Тут може бути обхідний шлях: додавши до нього деяку фіксовану не нульову провідну байтову константу, але для цього потрібно буде написати обгортку навколо конструктора BigInteger і повернеться до відповіді Джона.
Артем Оботуров

Відповідь @ vinchan була б більш доречною, оскільки не було б проблем із байтами, що ведуть нуль.
Артем Оботуров

5

Я дуже здивований, що відповіді не вказують на найпростішу альтернативу.

Так, використовувати HashMap не можна, але ніхто не заважає вам використовувати SortedMap як альтернативу. Єдине - написати компаратор, який повинен порівнювати масиви. Це не така продуктивність, як HashMap, але якщо ви хочете просту альтернативу, ось вам (ви можете замінити SortedMap на Map, якщо хочете приховати реалізацію):

 private SortedMap<int[], String>  testMap = new TreeMap<>(new ArrayComparator());

 private class ArrayComparator implements Comparator<int[]> {
    @Override
    public int compare(int[] o1, int[] o2) {
      int result = 0;
      int maxLength = Math.max(o1.length, o2.length);
      for (int index = 0; index < maxLength; index++) {
        int o1Value = index < o1.length ? o1[index] : 0;
        int o2Value = index < o2.length ? o2[index] : 0;
        int cmp     = Integer.compare(o1Value, o2Value);
        if (cmp != 0) {
          result = cmp;
          break;
        }
      }
      return result;
    }
  }

Цю реалізацію можна налаштувати для інших масивів, єдине, про що ви повинні знати, це те, що рівні масиви (= однакова довжина з однаковими членами) повинні повертати 0 і що у вас є детермістичний порядок


Приємне рішення з величезною перевагою, якщо не створювати додаткові об'єкти. Дуже маленька помилка, якщо масиви не однакової довжини, але найдовші мають лише 0 після коротшої довжини. Крім того, управління замовленням, ймовірно, допомагає пришвидшити обхід дерева. +1!
jmspaggi

1

Я вважаю, що масиви в Java не обов'язково реалізовують hashCode()і equals(Object)методи інтуїтивно. Тобто два однакові байтові масиви не обов’язково мають однаковий хеш-код, і вони не обов’язково претендуватимуть на рівність. Без цих двох рис ваша HashMap буде поводитися несподівано.

Тому я рекомендую не використовувати byte[]як ключі в HashMap.


Я припускаю, що моє формулювання було трохи відхиленим. Я враховував ситуацію, коли ТИЙ самий байтовий масив використовується як для вставки в хеш-карту, так і для отримання з хеш-карти. У цьому випадку байтові масиви "обидва" ідентичні І мають однаковий хеш-код.
Адам Пейнтер,

1

Вам слід скористатися створенням такого класу, як ByteArrKey, і перевантажити хеш-код і рівні методи, пам'ятати про контракт між ними.

Це дасть вам більшу гнучкість, оскільки ви можете пропустити 0 записів, що додаються в кінці байтового масиву, особливо якщо ви копіюєте лише частину з іншого байтового буфера.

Таким чином ви вирішите, як обидва об’єкти ПОВИННІ бути рівними.


0

Я бачу проблеми, оскільки вам слід використовувати Arrays.equals та Array.hashCode замість стандартних реалізацій масиву


І як би ви змусили HashMap використовувати їх?
Майкл Борґвардт

див. відповідь Джона Скіта (обгортка байтового масиву)
dfa

0

Arrays.toString (байти)


1
Можна використовувати, але не дуже ефективно. Якщо ви хочете піти цим шляхом, ви можете замість цього використовувати кодування base64.
Maarten Bodewes

0

Ви також можете перетворити байт [] у "безпечний" рядок, використовуючи Base32 або Base64, наприклад:

byte[] keyValue = new byte[] {…};
String key = javax.xml.bind.DatatypeConverter.printBase64Binary(keyValue);

Звичайно, є багато варіантів вищезазначеного, наприклад:

String key = org.apache.commons.codec.binary.Base64.encodeBase64(keyValue);

0

Ось рішення із використанням TreeMap, інтерфейсу компаратора та Java-методу java.util.Arrays.equals (байт [], байт []);

ПРИМІТКА. Упорядкування на карті не актуально для цього методу

SortedMap<byte[], String> testMap = new TreeMap<>(new ArrayComparator());

static class ArrayComparator implements Comparator<byte[]> {
    @Override
    public int compare(byte[] byteArray1, byte[] byteArray2) {

        int result = 0;

        boolean areEquals = Arrays.equals(byteArray1, byteArray2);

        if (!areEquals) {
            result = -1;
        }

        return result;
    }
}

0

Крім того, ми можемо створити власний власний ByteHashMap, як це,

ByteHashMap byteMap = new ByteHashMap();
byteMap.put(keybyteArray,valueByteArray);

Ось повна реалізація

public class ByteHashMap implements Map<byte[], byte[]>, Cloneable,
        Serializable {

    private Map<ByteArrayWrapper, byte[]> internalMap = new HashMap<ByteArrayWrapper, byte[]>();

    public void clear() {
        internalMap.clear();
    }

    public boolean containsKey(Object key) {
        if (key instanceof byte[])
            return internalMap.containsKey(new ByteArrayWrapper((byte[]) key));
        return internalMap.containsKey(key);
    }

    public boolean containsValue(Object value) {
        return internalMap.containsValue(value);
    }

    public Set<java.util.Map.Entry<byte[], byte[]>> entrySet() {
        Iterator<java.util.Map.Entry<ByteArrayWrapper, byte[]>> iterator = internalMap
                .entrySet().iterator();
        HashSet<Entry<byte[], byte[]>> hashSet = new HashSet<java.util.Map.Entry<byte[], byte[]>>();
        while (iterator.hasNext()) {
            Entry<ByteArrayWrapper, byte[]> entry = iterator.next();
            hashSet.add(new ByteEntry(entry.getKey().data, entry
                    .getValue()));
        }
        return hashSet;
    }

    public byte[] get(Object key) {
        if (key instanceof byte[])
            return internalMap.get(new ByteArrayWrapper((byte[]) key));
        return internalMap.get(key);
    }

    public boolean isEmpty() {
        return internalMap.isEmpty();
    }

    public Set<byte[]> keySet() {
        Set<byte[]> keySet = new HashSet<byte[]>();
        Iterator<ByteArrayWrapper> iterator = internalMap.keySet().iterator();
        while (iterator.hasNext()) {
            keySet.add(iterator.next().data);
        }
        return keySet;
    }

    public byte[] put(byte[] key, byte[] value) {
        return internalMap.put(new ByteArrayWrapper(key), value);
    }

    @SuppressWarnings("unchecked")
    public void putAll(Map<? extends byte[], ? extends byte[]> m) {
        Iterator<?> iterator = m.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry<? extends byte[], ? extends byte[]> next = (Entry<? extends byte[], ? extends byte[]>) iterator
                    .next();
            internalMap.put(new ByteArrayWrapper(next.getKey()), next
                    .getValue());
        }
    }

    public byte[] remove(Object key) {
        if (key instanceof byte[])
            return internalMap.remove(new ByteArrayWrapper((byte[]) key));
        return internalMap.remove(key);
    }

    public int size() {
        return internalMap.size();
    }

    public Collection<byte[]> values() {
        return internalMap.values();
    }

    private final class ByteArrayWrapper {
        private final byte[] data;

        public ByteArrayWrapper(byte[] data) {
            if (data == null) {
                throw new NullPointerException();
            }
            this.data = data;
        }

        public boolean equals(Object other) {
            if (!(other instanceof ByteArrayWrapper)) {
                return false;
            }
            return Arrays.equals(data, ((ByteArrayWrapper) other).data);
        }

        public int hashCode() {
            return Arrays.hashCode(data);
        }
    }

    private final class ByteEntry implements Entry<byte[], byte[]> {
        private byte[] value;
        private byte[] key;

        public ByteEntry(byte[] key, byte[] value) {
            this.key = key;
            this.value = value;
        }

        public byte[] getKey() {
            return this.key;
        }

        public byte[] getValue() {
            return this.value;
        }

        public byte[] setValue(byte[] value) {
            this.value = value;
            return value;
        }

    }
}

0

Інші відповіді не вказували на те, що не всі byte[]перетворюються на унікальні String. Я потрапив у цю пастку, роблячи new String(byteArray)як ключі до карти лише для того, щоб виявити, що багато негативні байти відображаються в одному рядку. Ось тест, який демонструє цю проблему:

    @Test
    public void testByteAsStringMap() throws Exception {
        HashMap<String, byte[]> kvs = new HashMap<>();
        IntStream.range(Byte.MIN_VALUE, Byte.MAX_VALUE).forEach(b->{
            byte[] key = {(byte)b};
            byte[] value = {(byte)b};
            kvs.put(new String(key), value);
        });
        Assert.assertEquals(255, kvs.size());
    }

Це кине:

java.lang.AssertionError: Очікується: 255 Фактичний: 128

Це робиться тому, що a String- це послідовність символьних точок коду, і будь-яке перетворення з a byte[]базується на якомусь байтовому кодуванні. У наведеному вище випадку кодування за замовчуванням на платформі відображає багато негативних байтів до одного і того ж символу. Іншим фактом Stringє те, що він завжди бере і дає копію свого внутрішнього стану. Якщо оригінальні байти надійшли з Stringкопії, тоді оберніть його якString щоб використовувати його як ключ до карти, потрібно друга копія. Це може створити багато сміття, якого можна уникнути.

Тут є хороша відповідь, яка пропонує використовувати java.nio.ByteBufferз ByteBuffer.wrap(b). Проблема полягає в тому, що byte[]вона змінюється, і вона не робить копію, тому ви повинні бути обережними, щоб зробити захисну копію будь-яких масивів, переданих вам, ByteBuffer.wrap(b.clone())інакше ключі вашої карти будуть пошкоджені. Якщо ви подивитесь на результат карти з ByteBufferключами у налагоджувачі, то побачите, що буфери мають багато внутрішніх посилань, призначених для відстеження читання та запису з кожного буфера. Тож предмети набагато важчі, ніж загортання в прості String. Нарешті, навіть рядок містить більше стану, ніж потрібно. Дивлячись на це у моєму налагоджувачі, він зберігає символи як двобайтовий масив UTF16, а також зберігає чотирибайтовий хеш-код.

Я віддаю перевагу тому, щоб Lombok генерував під час компіляції шаблонний шаблон, щоб зробити легку обгортку байтового масиву, яка не зберігає додатковий стан:

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

@ToString
@EqualsAndHashCode
@Data(staticConstructor="of")
class ByteSequence {
    final byte[] bytes;
}

Потім це проходить тест, який перевіряє, чи всі можливі байти відображаються в унікальний рядок:

    byte[] bytes(int b){
        return new byte[]{(byte)b};
    }

    @Test
    public void testByteSequenceAsMapKey() {
        HashMap<ByteSequence, byte[]> kvs = new HashMap<>();
        IntStream.range(Byte.MIN_VALUE, Byte.MAX_VALUE).forEach(b->{
            byte[] key = {(byte)b};
            byte[] value = {(byte)b};
            kvs.put(ByteSequence.of(key), value);
        });
        Assert.assertEquals(255, kvs.size());
        byte[] empty = {};
        kvs.put(ByteSequence.of(empty), bytes(1));
        Assert.assertArrayEquals(bytes(1), kvs.get(ByteSequence.of(empty)));
    }

Тоді вам не доведеться турбуватися про те, щоб виправити логіку рівності та хеш-коду, оскільки вона надається Lombok, де вона це робить, Arrays.deepEqualsщо задокументовано на https://projectlombok.org/features/EqualsAndHashCode. Зверніть увагу, що lombok - це не лише залежність від часу виконання залежність від часу компіляції, і ви можете встановити плагін відкритого джерела в свою IDE, щоб ваша IDE "бачила" всі згенеровані шаблонні методи.

З цією реалізацією вам все одно доведеться турбуватися про перемінливість байту. Якщо хтось передає вам, byte[]що може бути мутованим, ви повинні зробити захисну копію, використовуючи clone():

kvs.put(ByteSequence.of(key.clone()), value);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.