使用字节数组作为Map键


76

使用字节数组作为Map键,您是否看到任何问题?我也可以这样做new String(byte[])String但使用起来更简单byte[]

Answers:


65

问题是,byte[]将对象标识用于equalshashCode,因此

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

不会与匹配HashMap。我看到三个选择:

  1. 包装为String,但随后必须注意编码问题(您需要确保字节->字符串->字节为您提供相同的字节)。
  2. 使用List<Byte>(在内存中可能会很昂贵)。
  3. 做自己的包装类,编写hashCodeequals使用字节数组的内容。

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

(实际数字无关紧要;它们不同的事实很重要。)

假设您实际上想要平等,我建议您创建自己的包装器,其中包含abyte[]并适当地实现平等和哈希码生成:

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(etc)中的键之后更改字节数组中的值,则将再次查找该键时会遇到问题...如果需要,可以在ByteArrayWrapper构造函数中复制数据,但是如果您知道不会更改字节数组的内容,那显然会浪费性能。

编辑:如评论中所述,您也可以ByteBuffer为此使用(特别是其ByteBuffer#wrap(byte[])方法)。考虑到ByteBuffer您不需要的所有额外功能,我不知道这是否真的正确,但这是一个选择。


@dfa:“ instanceof”测试处理空值。
乔恩·斯基特

4
您可以添加到包装器实现中的其他几件事情:1.在构造上获取byte []的副本,因此确保该对象是不可变的,这意味着密钥的哈希码不会随时间而改变。2.预先计算并存储一次哈希码(假设速度比存储开销更重要)。
亚当斯基2009年

2
@Adamski:我在答案的结尾提到了复制的可能性。在某些情况下,这是正确的做法,但在其他情况下则不然。我可能希望使其成为一个选项(可能是静态方法而不是构造函数-copyOf和wrapperAround)。请注意,在不进行复制的情况下,可以更改基础数组,直到您首先获取哈希并检查是否相等为止,这在某些情况下可能很有用。
乔恩·斯基特

糟糕-抱歉,乔恩;我想念您的那部分答复。
亚当斯基

3
只是想指出,java.nio.ByteBuffer类实际上完成了包装程序的所有工作,尽管有同样的告诫,只有在字节数组的内容不变的情况下才应使用它。您可能需要修改答案以提及它。
Ed Anuff

46

我们可以为此使用ByteBuffer(这基本上是带有比较器的byte []包装器)

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为最轻量级的字节数组包装器(我认为...)
Nicholas

7
可以使用ByteBuffer.wrap()正常工作,但是要小心,如果已使用几个put()调用创建了一个复合密钥字节数组来创建了ByteBuffer的内容。在这种情况下,最后一个put()调用必须后面跟一个rewind()调用-否则,即使底层字节数组包含不同的数据,equals()也会返回true。
RenniePet 2015年

这将是一个不错的解决方案,但是如果您要序列化地图(例如我的情况),则不能使用此方法。
501-未实施

请注意:“由于缓冲区哈希码是与内容相关的,因此不建议将缓冲区用作哈希映射或类似数据结构中的键,除非已知缓冲区的内容不会改变。”(docs.oracle.com/javase/7 / docs / api / java / nio /…
LMD,

您应该ByteBuffer.wrap(k1.clone())对阵列进行防御性复制。如果没有,如果有人改变阵列,将会发生不好的事情。与字符串相比,在调试器中查看ByteBuffer具有许多内部状态,因此就内存开销而言,这似乎并不是一个轻量级的解决方案。
simbo1905

12

您可以使用java.math.BigInteger。它有一个BigInteger(byte[] val)构造函数。这是一种引用类型,因此可以用作哈希表的键。并且.equals().hashCode()被定义为相应的整数,这意味着BigInteger具有与byte []数组相同的相等语义。


16
听起来很吸引人,但这是错误的,因为仅在前导零元素(例如{0,100}{100})不同的两个数组将给出相同的BigInteger
leonbloy 2013年

好点@leonbloy。可能有一种解决方法:通过向其添加一个固定的非null前导字节常量,但这将需要在BigInteger构造函数周围编写包装,并使我们返回Jon的响应。
Artem Oboturov

@vinchan的响应将更合适,因为不会出现零前导字节问题。
Artem Oboturov

5

我很惊讶答案没有指出最简单的选择。

是的,不可能使用HashMap,但是没有人阻止您使用SortedMap作为替代。唯一的事情就是编写一个需要比较数组的比较器。它不像HashMap那样高效,但是如果您想要一个简单的替代方法,请执行以下操作(如果要隐藏实现,可以用Map替换SortedMap):

 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键。


我想我的措词有点偏离。我正在考虑使用SAME字节数组同时插入哈希图和从哈希图检索的情况。在这种情况下,“两个”字节数组都是相同的,并且共享相同的哈希码。
亚当·佩恩特

1

您应该使用创建诸如ByteArrKey和重载哈希码之类的类以及相等的方法,记住它们之间的约定。

这将为您提供更大的灵活性,因为您可以跳过字节数组末尾附加的0个条目,特别是如果您仅从另一个字节缓冲区中复制一部分。

这样,您将决定两个对象应如何相等。


0

我看到了问题,因为您应该使用Arrays.equals和Array.hashCode代替默认的数组实现


以及如何使HashMap使用这些?
Michael Borgwardt 2009年

请参见乔恩·斯基特(Jon Skeet)的答案(字节数组包装器)
dfa


0

您还可以使用Base32或Base64将byte []转换为“安全”字符串,例如:

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

当然,上面有很多变体,例如:

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

0

这是使用TreeMap,Comparator接口和Java方法java.util.Arrays.equals(byte [],byte []);的解决方案。

注意:地图中的排序与此方法无关

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

这样做是因为aString是字符代码点的序列,并且从a进行的任何转换byte[]都基于某种字节编码。在上述情况下,平台默认编码碰巧将许多负字节映射到同一字符。另一个事实String是,它总是获取并提供其内部状态的副本。如果原始字节来自String复制的,则将其包装为aString以将其用作映射的键​​将获取第二个副本。这可能会产生很多可以避免的垃圾。

这里有一个很好的建议,建议使用java.nio.ByteBufferwith 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)));
    }

然后,您不必担心如何正确获取equals和hashcode逻辑,因为Lombok在此处提供了正确的方法,并Arrays.deepEqualshttps://projectlombok.org/features/EqualsAndHashCode中进行了说明。注意,lombok不仅是运行时依赖项一个编译时依赖性,您可以将一个开源插件安装到您的IDE中,以便您的IDE“看到”所有生成的样板方法。

使用此实现,您仍然需要担心字节的可变性。如果有人通过了您byte[]可能会被突变的,您应该使用进行防御性复制clone()

kvs.put(ByteSequence.of(key.clone()), value);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.