1.简介
在本教程中,我们将学习如何在HashMap
使用字节数组作为键。由于HashMap
工作方式,不幸的是,我们无法直接做到这一点。我们将调查为什么会这样,并探讨解决该问题的几种方法。
2.为HashMap
设计一个好的Key
2.1。 HashMap
如何工作
HashMap
使用散列机制从自身存储和检索值。当我们调用put(key, value)
方法时, HashMap
根据键的hashCode()
方法计算哈希码。此哈希用于标识最终将值存储在其中的存储桶:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
当我们使用get(key)
方法检索值时,将涉及类似的过程。该密钥用于计算哈希码,然后找到存储桶。然后,使用equals()
方法检查存储桶中的每个条目是否相等。最后,返回匹配条目的值:
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
2.2。 equals
()和hashCode
()之间的契约
equals
和hashCode
方法都具有应遵守的约定。在HashMaps
的上下文中,一方面特别重要:彼此相等的对象必须返回相同的hashCode
。但是,返回相同hashCode
对像不必彼此相等。这就是为什么我们可以将多个值存储在一个存储桶中的原因。
2.3。不变性
HashMap
中的键的hashCode
不应更改。尽管它不是强制性的,但强烈建议键是不可变的。如果对象是不可变的,则无论hashCode
方法的实现如何,其hashCode
都没有机会进行更改。
默认情况下,哈希是根据对象的所有字段计算的。如果我们想要一个可变键,则需要重写hashCode
方法,以确保在其计算中不使用可变字段。为了维持合同,我们还需要更改equals
方法。
2.4。有意义的equal
为了能够从地图成功检索值,相等性必须有意义。在大多数情况下,我们需要能够创建一个新的键对象,该对象将等于地图中的某些现有键。因此,对象标识在这种情况下不是很有用。
这也是使用原始字节数组并不是真正的选择的主要原因。 Java中的数组使用对象标识来确定相等性。如果我们使用字节数组作为键来创建HashMap
,我们将只能使用完全相同的数组对象来检索值。
让我们创建一个简单的实现,将字节数组作为键:
byte[] key1 = {1, 2, 3};
byte[] key2 = {1, 2, 3};
Map<byte[], String> map = new HashMap<>();
map.put(key1, "value1");
map.put(key2, "value2");
我们不仅有两个条目具有几乎相同的键,而且我们不能使用新创建的具有相同值的数组来检索任何内容:
String retrievedValue1 = map.get(key1);
String retrievedValue2 = map.get(key2);
String retrievedValue3 = map.get(new byte[]{1, 2, 3});
assertThat(retrievedValue1).isEqualTo("value1");
assertThat(retrievedValue2).isEqualTo("value2");
assertThat(retrievedValue3).isNull();
3.使用现有容器
代替字节数组,我们可以使用现有的类,其相等性实现是基于内容而不是对象标识的。
3.1。 String
String
相等性基于字符数组的内容:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = count;
if (n == anotherString.count) {
char v1[] = value;
char v2[] = anotherString.value;
int i = offset;
int j = anotherString.offset;
while (n-- != 0) {
if (v1[i++] != v2[j++])
return false;
}
return true;
}
}
return false;
}
String
也是不可变的,并且基于字节数组创建String
非常简单。我们可以使用Base64
方案轻松地对String
进行编码和解码:
String key1 = Base64.getEncoder().encodeToString(new byte[]{1, 2, 3});
String key2 = Base64.getEncoder().encodeToString(new byte[]{1, 2, 3});
现在,我们可以创建一个以String
为键而不是字节数组的HashMap
。我们将以类似于上一个示例的方式将值放入Map
中:
Map<String, String> map = new HashMap<>();
map.put(key1, "value1");
map.put(key2, "value2");
然后,我们可以从地图中检索一个值。对于这两个键,我们将获得相同的第二个值。我们还可以检查两个键是否真正相等:
String retrievedValue1 = map.get(key1);
String retrievedValue2 = map.get(key2);
assertThat(key1).isEqualTo(key2);
assertThat(retrievedValue1).isEqualTo("value2");
assertThat(retrievedValue2).isEqualTo("value2");
3.2。Lists
与String
相似, List#equals
方法将检查其每个元素的相等性。如果这些元素具有明智的equals()
方法并且是不可变的,则List
将作为HashMap
键正确工作。我们只需要确保我们使用的是不可变的List
实现:
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");
assertThat(map.get(key1)).isEqualTo(map.get(key2));
请注意, Byte
对象List
将比byte
基元数组占用更多的内存。因此,该解决方案虽然方便,但在大多数情况下并不可行。
4.实施自定义容器
我们还可以实现自己的包装器,以完全控制哈希码的计算和相等性。这样,我们可以确保解决方案是快速的,并且不会占用大量内存。
让我们创建一个带有最后一个私有byte
数组字段的类。它没有二传手,它的getter会进行防御性复制以确保完全不变:
public final class BytesKey {
private final byte[] array;
public BytesKey(byte[] array) {
this.array = array;
}
public byte[] getArray() {
return array.clone();
}
}
我们还需要实现自己的equals
和hashCode
方法。幸运的是,我们可以将Arrays
实用程序类用于以下两个任务:
@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
的键:
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");
然后,我们可以使用已声明的键之一检索第二个值,也可以使用动态创建的键:
String retrievedValue1 = map.get(key1);
String retrievedValue2 = map.get(key2);
String retrievedValue3 = map.get(new BytesKey(new byte[]{1, 2, 3}));
assertThat(retrievedValue1).isEqualTo("value2");
assertThat(retrievedValue2).isEqualTo("value2");
assertThat(retrievedValue3).isEqualTo("value2");
5.结论
在本教程中,我们研究了在HashMap
使用byte
数组作为键的不同问题和解决方案。首先,我们研究了为什么不能使用数组作为键。然后,我们使用了一些内置容器来缓解该问题,最后实现了自己的包装器。
0 评论