你认为使用字节数组作为Map键有什么问题吗?我也可以执行new String(byte[])
并按String
哈希,但使用byte[]
会更直接。
答案 0 :(得分:72)
只要你只想要你的密钥的引用相等就没关系 - 数组不会以你可能想要的方式实现“值相等”。例如:
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
具有你不需要的所有额外能力,但这是一个选择。
答案 1 :(得分:57)
问题是byte[]
使用equals
和hashCode
的对象标识,因此
byte[] b1 = {1, 2, 3}
byte[] b2 = {1, 2, 3}
在HashMap
中不匹配。我看到三个选项:
String
,但是你必须小心编码问题(你需要确保字节 - &gt; String - &gt;字节给你相同的字节)。List<Byte>
(内存可能很贵)。hashCode
和equals
以使用字节数组的内容。答案 2 :(得分:39)
我们可以使用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
答案 3 :(得分:12)
您可以使用java.math.BigInteger
。它有一个BigInteger(byte[] val)
构造函数。它是一个引用类型,因此可以用作哈希表的键。并且.equals()
和.hashCode()
被定义为相应的整数,这意味着BigInteger具有与byte []数组一致的等于语义。
答案 4 :(得分:4)
我很惊讶答案并没有指出最简单的替代方案。
是的,无法使用HashMap,但没有人阻止您使用SortedMap作为替代方案。唯一的事情是编写一个需要比较数组的Comparator。它不像HashMap那样高效,但是如果你想要一个简单的替代方案,那么你可以去(如果你想隐藏实现,可以用Sort替换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并且你有一个确定的顺序
答案 5 :(得分:1)
我相信Java中的数组不一定直观地实现hashCode()
和equals(Object)
方法。也就是说,两个相同的字节数组不一定共享相同的哈希码,并且它们不一定声称是相等的。如果没有这两个特征,您的HashMap会出现意外行为。
因此,我建议反对使用byte[]
作为HashMap中的键。
答案 6 :(得分:1)
你应该使用像ByteArrKey这样的类创建并重载hashcode和相等的方法,记住它们之间的契约。
这将为您提供更大的灵活性,因为您可以跳过附加在字节数组末尾的0个条目,特别是如果您只从另一个字节缓冲区复制某个部分。
通过这种方式,您将决定两个对象应该如何相等。
答案 7 :(得分:0)
我发现问题因为您应该使用Arrays.equals和Array.hashCode代替默认数组实现
答案 8 :(得分:0)
Arrays.toString(字节)
答案 9 :(得分:0)
您还可以使用Base32或Base64将byte []转换为'safe'字符串,例如:
byte[] keyValue = new byte[] {…};
String key = javax.xml.bind.DatatypeConverter.printBase64Binary(keyValue);
当然上面有很多变种,比如:
String key = org.apache.commons.codec.binary.Base64.encodeBase64(keyValue);
答案 10 :(得分: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;
}
}
答案 11 :(得分: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;
}
}
}
答案 12 :(得分: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
之所以这样做是因为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);