【问题标题】:Using a byte array as Map key使用字节数组作为 Map 键
【发布时间】:2010-11-06 16:29:28
【问题描述】:

您认为使用字节数组作为 Map 键有什么问题吗?我也可以使用new String(byte[]) 并通过String 进行散列,但使用byte[] 更直接。

【问题讨论】:

    标签: java hashmap bytearray


    【解决方案1】:

    其他答案并未指出并非所有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

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

    这里有一个很好的答案,建议使用java.nio.ByteBufferByteBuffer.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 不是运行时依赖项,只是一个编译-time 依赖项,您可以将开源插件安装到您的 IDE,以便您的 IDE“看到”所有生成的样板方法。

    使用此实现,您仍然需要担心字节的可变性。如果有人向您传递了可能已变异的byte[],您应该使用clone() 进行防御性副本:

    kvs.put(ByteSequence.of(key.clone()), value);
    

    【讨论】:

      【解决方案2】:

      另外,我们可以像这样创建自己的自定义 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;
              }
      
          }
      }
      

      【讨论】:

        【解决方案3】:

        这里是一个使用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;
            }
        }
        

        【讨论】:

          【解决方案4】:

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

          是的,不可能使用 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!
          【解决方案5】:

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

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

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

          【讨论】:

            【解决方案6】:

            您还可以使用 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);
            

            【讨论】:

              【解决方案7】:

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

              【讨论】:

              • 听起来很吸引人,但这是错误的,因为两个仅在前导零元素上不同的数组(例如,{0,100}{100})将给出相同的 BigInteger
              • 好点@leonbloy。可能有一个解决方法:通过向它添加一些固定的非空前导字节常量,但它需要在 BigInteger 构造函数周围编写一个包装器,并将我们返回给 Jon 的响应。
              • @vinchan 的回复会更合适,因为不会出现零前导字节问题。
              【解决方案8】:

              我们可以为此使用 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
              

              【讨论】:

              • +1 表示最轻量级的字节数组包装器(我认为...)
              • 这适用于 ByteBuffer.wrap(),但如果 ByteBuffer 的内容是使用几个 put() 调用创建复合键字节数组创建的,请小心。在这种情况下,最后一次 put() 调用必须跟在 rewind() 调用之后——否则即使底层字节数组包含不同的数据,equals() 也会返回 true。
              • 这将是一个不错的解决方案,但如果您想序列化地图(如我的情况),则不能使用此方法。
              • 请注意:“因为缓冲区哈希码是依赖于内容的,所以不建议使用缓冲区作为哈希映射或类似数据结构中的键,除非知道它们的内容不会改变。”(@ 987654321@)
              • 您应该ByteBuffer.wrap(k1.clone()) 获取阵列的防御性副本。如果没有,如果有人确实更改了数组,就会发生坏事。在调试器中查看 ByteBuffer 与 String 相比具有很多内部状态,因此看起来这在内存开销方面并不是一个真正的轻量级解决方案。
              【解决方案9】:

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

              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
              

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

              假设您实际上想要相等,我建议您创建自己的包装器,其中包含 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 构造函数中的数据,但如果您知道 不会 更改字节数组的内容,那显然会浪费性能。

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

              【讨论】:

              • @dfa:“instanceof”测试处理空情况。
              • 您可以添加到包装器实现中的其他几件事: 1. 在构造时获取 byte[] 的副本,从而保证对象是不可变的,这意味着您的密钥的哈希码不会有危险随着时间的推移而变化。 2. 预先计算并存储一次哈希码(假设速度比存储开销更重要)。
              • @Adamski:我在答案末尾提到了复制的可能性。在某些情况下,这是正确的做法,但在其他情况下则不然。我可能想让它成为一个选项(可能是静态方法而不是构造函数 - copyOf 和 wrapperAround)。请注意,无需复制,您可以更改底层数组,直到您首先获取哈希并检查是否相等,这在某些情况下可能很有用。
              • 哎呀-对不起乔恩;我错过了你回复的那部分。
              • 只是想指出 java.nio.ByteBuffer 类基本上完成了你的包装器所做的一切,尽管同样需要注意的是,你应该只在字节数组的内容不是的情况下使用它变化。您可能需要修改您的答案以提及它。
              【解决方案10】:

              Arrays.toString(字节)

              【讨论】:

              • 可以使用,但效率不高。如果您想采用这种方式,您可能需要改用 base64 编码。
              【解决方案11】:

              问题在于byte[] 使用equalshashCode 的对象标识,所以

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

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

              1. 包装在String 中,但是您必须小心编码问题(您需要确保字节 -> 字符串 -> 字节为您提供相同的字节)。
              2. 使用List&lt;Byte&gt;(内存开销可能很大)。
              3. 编写自己的包装类,编写 hashCodeequals 以使用字节数组的内容。

              【讨论】:

              • 我通过使用十六进制编码解决了字符串换行问题。您也可以使用 base64 编码。
              • 包装/处理类选项很简单,应该非常易读。
              【解决方案12】:

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

              【讨论】:

              • 你会如何让 HashMap 使用这些?
              • 查看 Jon Skeet 的回答(字节数组包装器)
              【解决方案13】:

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

              因此,我建议反对在 HashMap 中使用 byte[] 作为键。

              【讨论】:

              • 我想我的措辞有点不对劲。我正在考虑使用相同字节数组插入哈希映射和从哈希映射检索的情况。在这种情况下,“两个”字节数组是相同的并且共享相同的哈希码。
              猜你喜欢
              • 2016-01-07
              • 1970-01-01
              • 2010-11-28
              • 2014-01-07
              • 1970-01-01
              • 1970-01-01
              • 2020-02-14
              • 2012-08-25
              相关资源
              最近更新 更多