Ви бачите проблему з використанням байтового масиву як ключа мапи? Я також міг би робити new String(byte[])
і хеш, String
але це простіше у використанні byte[]
.
Відповіді:
Проблема полягає в тому, що byte[]
використовується ідентифікація об’єкта для equals
і hashCode
, так що
byte[] b1 = {1, 2, 3}
byte[] b2 = {1, 2, 3}
не збігатиметься в a HashMap
. Я бачу три варіанти:
String
, але тоді ви повинні бути обережними щодо питань кодування (потрібно переконатися, що байт -> String -> byte дає вам однакові байти).List<Byte>
(може бути дорогим в пам’яті).hashCode
та equals
використання вмісту байтового масиву.Це нормально, якщо ви хочете лише посилальну рівність для вашого ключа - масиви не реалізують "рівність значень" так, як ви, мабуть, хотіли б. Наприклад:
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
які вам не потрібні, але це варіант.
Для цього ми можемо використовувати 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
ByteBuffer.wrap(k1.clone())
зробити захисну копію масиву. Якщо ні, якщо хтось змінить масив, траплятимуться погані речі. Дивлячись у налагоджувачі, ByteBuffer має багато внутрішнього стану в порівнянні зі рядком, тому здається, що це насправді не є легким рішенням з точки зору витрат пам'яті.
Ви могли б використовувати java.math.BigInteger
. Він має BigInteger(byte[] val)
конструктор. Це посилальний тип, тому його можна використовувати як ключ для хеш-таблиці. А .equals()
та .hashCode()
визначаються як для відповідних цілих чисел, що означає BigInteger має послідовну дорівнює семантику, Byte [] масиву.
{0,100}
і {100}
), дадуть одне і те ж BigInteger
Я дуже здивований, що відповіді не вказують на найпростішу альтернативу.
Так, використовувати 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 і що у вас є детермістичний порядок
Я вважаю, що масиви в Java не обов'язково реалізовують hashCode()
і equals(Object)
методи інтуїтивно. Тобто два однакові байтові масиви не обов’язково мають однаковий хеш-код, і вони не обов’язково претендуватимуть на рівність. Без цих двох рис ваша HashMap буде поводитися несподівано.
Тому я рекомендую не використовувати byte[]
як ключі в HashMap.
Вам слід скористатися створенням такого класу, як ByteArrKey, і перевантажити хеш-код і рівні методи, пам'ятати про контракт між ними.
Це дасть вам більшу гнучкість, оскільки ви можете пропустити 0 записів, що додаються в кінці байтового масиву, особливо якщо ви копіюєте лише частину з іншого байтового буфера.
Таким чином ви вирішите, як обидва об’єкти ПОВИННІ бути рівними.
Я бачу проблеми, оскільки вам слід використовувати Arrays.equals та Array.hashCode замість стандартних реалізацій масиву
Arrays.toString (байти)
Ви також можете перетворити байт [] у "безпечний" рядок, використовуючи Base32 або Base64, наприклад:
byte[] keyValue = new byte[] {…};
String key = javax.xml.bind.DatatypeConverter.printBase64Binary(keyValue);
Звичайно, є багато варіантів вищезазначеного, наприклад:
String key = org.apache.commons.codec.binary.Base64.encodeBase64(keyValue);
Ось рішення із використанням 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;
}
}
Крім того, ми можемо створити власний власний 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;
}
}
}
Інші відповіді не вказували на те, що не всі 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);