81

您认为使用字节数组作为 Map 键有什么问题吗?我也可以做new String(byte[])和散列,String但使用起来更简单byte[]

4

13 回答 13

84

只要您只希望键的引用相等就可以了 - 数组不会以您可能想要的方式实现“值相等”。例如:

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(等)中的键后更改字节数组中的值,您将无法再次查找该键...ByteArrayWrapper如果您愿意,可以在构造函数中获取数据的副本,但如果您知道不会更改字节数组的内容,显然这将浪费性能。

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

于 2009-06-29T13:06:12.780 回答
65

问题是对andbyte[]使用对象标识,因此equalshashCode

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

不会在 a 中匹配HashMap。我看到三个选项:

  1. 包装在 a 中String,但是你必须小心编码问题(你需要确保 byte -> String -> byte 给你相同的字节)。
  2. 使用List<Byte>(在内存中可能很昂贵)。
  3. 做你自己的包装类,写入hashCodeequals使用字节数组的内容。
于 2009-06-29T13:10:22.680 回答
47

我们可以为此使用 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
于 2012-12-30T00:32:15.420 回答
11

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

于 2011-11-12T00:11:52.270 回答
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 并且您具有确定性顺序

于 2016-07-24T13:44:08.250 回答
1

我相信Java中的数组不一定能直观地实现hashCode()和方法。equals(Object)也就是说,两个相同的字节数组不一定共享相同的哈希码,也不一定声称是相等的。如果没有这两个特征,您的 HashMap 将表现出意外。

因此,我建议不要byte[]在 HashMap中使用as 作为键。

于 2009-06-29T13:05:44.000 回答
1

您应该使用创建一个类似 ByteArrKey 的类并重载 hashcode 和 equal 方法,记住它们之间的约定。

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

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

于 2015-10-22T11:58:07.017 回答
1

这是使用 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;
    }
}
于 2018-01-30T15:59:28.373 回答
0

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

于 2009-06-29T13:06:48.410 回答
0

Arrays.toString(字节)

于 2009-11-17T03:15:07.983 回答
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);
于 2014-03-07T12:35:21.783 回答
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;
        }

    }
}
于 2019-08-30T06:29:11.707 回答
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作为副本的 a,则将其包装为 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)));
    }

然后,您不必担心得到正确的等号和哈希码逻辑,因为它由 Lombok 提供,它在https://projectlombok.org/features/EqualsAndHashCodeArrays.deepEquals中记录了它的文档请注意,lombok 不仅仅是运行时依赖项编译时依赖项,您可以将开源插件安装到您的 IDE,以便您的 IDE“看到”所有生成的样板方法。

有了这个实现,你仍然需要担心字节的可变性。如果有人传给您一个byte[]可能已变异的,您应该使用以下方法进行防御性副本clone()

kvs.put(ByteSequence.of(key.clone()), value);
于 2019-11-18T22:01:23.153 回答