踩坑:在Java中使用 byte 数组作为 Map 的 key

2023-09-01 16:56:14 浏览数 (2)

本文将引领我们探索:如何将byte数组作为HashMap中键。HashMap的机制使我们无法直接这样做。让我们研究一下,为何出现此状况,以及针对这种情况,几种可供选择的解决方案。

HashMap的工作原理

HashMap是一种使用哈希机制来存储和检索值的数据结构。使用哈希码来存储和检索值可以大大提高HashMap的性能,因为它可以使查找键值对的时间复杂度保持在O(1)的级别。当然,这也要求我们在实现hashCode()方法时尽可能地让哈希码分布均匀,以免造成哈希冲突,从而影响HashMap的效率。

当我们调用put(key, value)方法时,HashMap会通过键的hashCode()方法计算哈希码。这个哈希码用于确定最终存储值的桶:

代码语言:javascript复制
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

在使用get(key)方法检索值时,需要经过一系列处理步骤:首先,会通过键计算哈希码,然后找到哈希桶。接下来,使用equals()方法检查桶中的每个条目是否与键相等。最终,返回匹配条目的值:

代码语言:javascript复制
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

`equals`和`hashCode`方法

在Java编程中,equals方法和hashCode方法都有应该遵守的规则。在HashMap这个数据结构中,有一个方面尤其重要:具有相同equals方法比较结果的对象,必须返回相同的哈希值。然而,反之则不一定成立,也就是说,具有相同哈希值的对象,并不一定具有相同的equals方法比较结果。这也是为什么我们可以将多个对象存储在HashMap的同一个桶中的原因。

在使用HashMap时,建议不要更改key的哈希值。虽然这不是强制性规定,但强烈建议将键定义为不可变对象。如果对象是不可变的,无论hashCode方法的实现如何,它的哈希值都不会被更改。

在默认情况下,哈希值是基于对象的所有字段进行计算的。如果我们需要使用可变的键,我们需要重写hashCode方法,以确保它的计算不涉及可变字段。为了维护这一个规则,我们还需要修改equals方法。

使用 byte 数组作为key

为了能够从映射中成功地检索值,相等性必须是有意义的。这就是使用byte数组并不是一个真正的选择的主要原因。在Java中,数组使用对象标识来确定相等性。如果我们使用byte数组作为key创建HashMap,那么只有使用完全相同的数组对象才能检索值。

让我们使用byte数组作为key创建一个简单的例子:

代码语言:javascript复制
byte[] key1 = {1, 2, 3};
byte[] key2 = {1, 2, 3};
Map<byte[], String> map = new HashMap<>();
map.put(key1, "value1");
map.put(key2, "value2");

System.out.println(map.get(key1));
System.out.println(map.get(key2));
System.out.println(map.get(new byte[]{1, 2, 3}));

我们虽然有两个相同的键,但是我们无法使用具有相同值的新创建的数组检索到任何内容,运行结果如下:

代码语言:javascript复制
value1
value2
null

解决方法

使用`String`

String的相等性基于字符数组的内容:

代码语言:javascript复制
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i  ;
            }
            return true;
        }
    }
    return false;
}

字符串也是不可变的,并且基于byte数组创建一个字符串非常简单。我们可以使用Base64轻松编码和解码字符串,然后创建一个使用字符串作为key而不是byte数组的HashMap:

代码语言:javascript复制
String key1 = Base64.getEncoder().encodeToString(new byte[]{1, 2, 3});
String key2 = Base64.getEncoder().encodeToString(new byte[]{1, 2, 3});
Map<String, String> map = new HashMap<>();
map.put(key1, "value1");
map.put(key2, "value2");

System.out.println(map.get(key1));
System.out.println(map.get(key2));
System.out.println(map.get(Base64.getEncoder().encodeToString(new byte[]{1, 2, 3})));

运行结果如下:

代码语言:javascript复制
value2
value2
value2

注意: 在byte数组转化为String时会有性能损耗。因此,在大多数情况下,该解决方案并不推荐

使用`List`

String类似,List#equals方法将检查其每个元素的相等性:

代码语言:javascript复制
public boolean equals(Object o) {
    if (o == this)
        return true;
    if (!(o instanceof List))
        return false;

    ListIterator<E> e1 = listIterator();
    ListIterator<?> e2 = ((List<?>) o).listIterator();
    while (e1.hasNext() && e2.hasNext()) {
        E o1 = e1.next();
        Object o2 = e2.next();
        if (!(o1==null ? o2==null : o1.equals(o2)))
            return false;
    }
    return !(e1.hasNext() || e2.hasNext());
}

如果这些元素具有合理的equals()方法并且是不可变的,则List将作为HashMap键正确工作。我们只需要确保使用不可变的List实现:

代码语言:javascript复制
List<Byte> key1 = ImmutableList.of((byte) 1, (byte) 2, (byte) 3);
List<Byte> key2 = ImmutableList.of((byte) 1, (byte) 2, (byte) 3);
Map<List<Byte>, String> map = new HashMap<>();
map.put(key1, "value1");
map.put(key2, "value2");

System.out.println(map.get(key1));
System.out.println(map.get(key2));
System.out.println(map.get(ImmutableList.of((byte) 1, (byte) 2, (byte) 3)));

运行结果如下:

代码语言:javascript复制
value2
value2
value2

注意: Byte对象的列表将占用比byte数组更多的内存。因此,在大多数情况下,该解决方案并不推荐

自定义类(`推荐使用`)

我们还可以自己的定义一个类,用来完全控制哈希码计算和相等性。这样,我们可以确保解决方案快速,并且没有太大的内存占用。

让我们创建一个只有一个final私有byte数组字段的类。它将没有setter方法,只用getter方法,用来确保完全不可变性。

然后在实现自己的equalshashCode方法。为了方法,我们可以使用Arrays类来完成这两项任务,最终代码如下:

代码语言:javascript复制
public class BytesKey {
    private final byte[] array;

    public BytesKey(byte[] array) {
        this.array = array;
    }

    public byte[] getArray() {
        return array.clone();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()){
            return false;
        }
        BytesKey bytesKey = (BytesKey) o;
        return Arrays.equals(array, bytesKey.array);
    }

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

最后,我们使用我们自定义的类作为HashMap的key:

代码语言:javascript复制
BytesKey key1 = new BytesKey(new byte[]{1, 2, 3});
BytesKey key2 = new BytesKey(new byte[]{1, 2, 3});
Map<BytesKey, String> map = new HashMap<>();
map.put(key1, "value1");
map.put(key2, "value2");

System.out.println(map.get(key1));
System.out.println(map.get(key2));
System.out.println(map.get(new BytesKey(new byte[]{1, 2, 3})));

运行结果如下:

代码语言:javascript复制
value2
value2
value2

注意: 自定义的类既没有转化为String的性能损耗,也没有Byte对象列表的内存占用。因此,该解决方案推荐使用

总结

本文将讨论在使用HashMap时,当byte数组作为key时所遇到的问题及其解决方案。

首先,我们将研究为什么不能直接使用数组作为键。在使用HashMap时,我们需要保证每个键的唯一性,而使用数组作为键可能会出现冲突。这是因为数组的hashCode值是基于其在内存中的地址计算得出的,因此即使两个数组内容完全相同,它们在内存中的位置不同,它们的hashCode也会不同。因此,直接使用数组作为键可能会导致无法正确获取值或者出现意外的覆盖。

接着,我们会介绍使用String和List这两种数据结构作为临时解决方案的方法。它们都是具有可比性和可哈希性的数据结构,能够保证唯一性。但这种方法并不是完美的解决方案,因为使用String或List作为键会带来一些性能上的开销,或者占用不必要的内存空间。

最后,我们将通过自定义类的方式完美解决这个问题。这个自定义类包含了一个byte数组字段,并重写hashCodeequals方法,以确保唯一性和正确性。通过这种方式,我们可以避免使用String或List时的性能和内存占用问题,并且能够在保证正确性的同时获得更高的效率。

0 人点赞