您认为使用字节数组作为Map键存在问题吗?我也可以使用new String(byte[])
并通过String
进行哈希,但使用byte[]
更加直接。
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
构造函数中复制数据,但显然这将浪费性能。
编辑:如评论中所述,您也可以使用ByteBuffer
(特别是其ByteBuffer#wrap(byte[])
方法)。我不知道是否真的是正确的做法,因为ByteBuffer
具有您不需要的所有额外功能,但这是一种选择。
byte[]
使用对象标识符来进行equals
和hashCode
比较。byte[] b1 = {1, 2, 3}
byte[] b2 = {1, 2, 3}
在 HashMap
中无法匹配。我看到三个选项:
String
中,但是你必须小心编码问题(你需要确保字节 -> 字符串 -> 字节给出相同的字节)。List<Byte>
(可能会占用大量内存)。hashCode
和 equals
方法来使用字节数组的内容。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
ByteBuffer.wrap(k1.clone())
来获取数组的防御性副本。否则,如果有人更改了数组,将会发生糟糕的事情。在调试器中查看,与字符串相比,ByteBuffer具有许多内部状态,因此从内存开销的角度来看,这似乎并不是一种轻量级的解决方案。 - simbo1905您可以使用 java.math.BigInteger
。它具有BigInteger(byte[] val)
构造函数。它是一个引用类型,因此可以用作hashtable的键。而且,.equals()
和.hashCode()
的定义与相应的整数数字相同,这意味着BigInteger具有与byte[]数组一致的equals语义。
{0,100}
和 {100}
)将会给出相同的 BigInteger。 - leonbloy我很惊讶答案没有指出最简单的替代方案。
是的,无法使用HashMap,但你可以使用SortedMap作为替代方案。唯一需要做的是编写一个比较器来比较数组。它不像HashMap那样高效,但如果你需要一个简单的替代方案,这里有一个(如果你想隐藏实现,可以将SortedMap替换为Map):
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,并且有一个确定的顺序。
这里有一个使用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;
}
}
我认为在Java中,数组并不一定直观地实现了hashCode()
和equals(Object)
方法。也就是说,两个相同的字节数组不一定共享相同的哈希码,并且它们也不一定声称相等。如果没有这两个特性,你的HashMap将会表现出意外的行为。
因此,我建议不要在HashMap中使用byte[]
作为键。
你也可以将 byte[] 转换为“安全”的字符串,例如 Base32 或 Base64:
byte[] keyValue = new byte[] {…};
String key = javax.xml.bind.DatatypeConverter.printBase64Binary(keyValue);
当然,以上还有许多变体,例如:
String key = org.apache.commons.codec.binary.Base64.encodeBase64(keyValue);
其他答案没有指出并非所有的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: Expected :255 Actual :128
这是因为一个 String
是一系列字符代码点,而从 byte[]
进行的任何转换都基于某些字节编码。在上面的情况下,平台默认编码恰好将许多负字节映射到相同的字符。关于 String
的另一个事实是它始终获取并给出其内部状态的副本。如果原始字节来自被复制的 String
,那么将其包装为 String
以将其用作映射键将生成第二个副本。这可能会产生很多垃圾,这是可以避免的。
这里有一个很好的答案建议使用java.nio.ByteBuffer
和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提供,其中使用了Arrays.deepEquals
,该方法在https://projectlombok.org/features/EqualsAndHashCode中有记录。请注意,Lombok不是运行时依赖项,只是编译时依赖项,您可以安装开源插件到您的IDE中,以便您的IDE“看到”所有生成的样板方法。
使用此实现,您仍然需要考虑字节的可变性。如果有人传递给您可能被改变的byte[]
,您应该使用clone()
进行防御性复制:
kvs.put(ByteSequence.of(key.clone()), value);