如何使 HashMap 以数组为键工作?

Posted

技术标签:

【中文标题】如何使 HashMap 以数组为键工作?【英文标题】:How to make HashMap work with Arrays as key? 【发布时间】:2013-03-12 15:27:34 【问题描述】:

我使用布尔数组作为 HashMap 的键。但是问题是当不同的数组作为键传递时,HashMap 无法获取键,尽管元素是相同的。 (因为它们是不同的对象)。

我怎样才能使它使用数组作为键? 这是代码:

public class main 
public static HashMap<boolean[], Integer> h;


public static void main(String[] args)
    boolean[] a = false, false;

    h = new HashMap<boolean[], Integer>();
    h.put(a, 1);


    if(h.containsKey(a)) System.out.println("Found a");

    boolean[] t = false, false;

    if(h.containsKey(t)) System.out.println("Found t");
    else System.out.println("Couldn't find t");




at 两个数组都包含相同的元素,但 HashMap 不为 t 返回任何内容。

如何让它发挥作用?

【问题讨论】:

你不需要,除非你的数组对象是一个自定义的子类,满足计算哈希值和equals的要求。 Can a java array be used as a HashMap key 的可能重复项 【参考方案1】:

问题

    正如其他人所说,Java数组从Object继承.hashcode().equals(),它使用数组或对象的地址的哈希值,完全忽略了它的内容。解决此问题的唯一方法是将数组包装在一个对象中,该对象根据数组的内容实现这些方法。这就是 Joshua Bloch 写第 25 条:“Prefer lists to arrays”的原因之一。 Java 提供了几个执行此操作的类,或者您可以使用 Arrays.hashCode()Arrays.equals() 编写自己的类,其中包含这些方法的正确和有效的实现。可惜它们不是默认实现!

    只要可行,就对任何基于散列的集合的键使用深度不可修改(或不可变)的类。如果您在将数组(或其他可变对象)存储为哈希表中的键之后对其进行修改,那么它几乎肯定会在该哈希表中的未来.get().contains() 测试中失败。另见Are mutable hashmap keys a dangerous practice?

具体解决方案

// Also works with primitive:    (boolean... items)
public static List<Boolean> bList(Boolean... items) 
    List<Boolean> mutableList = new ArrayList<>();
    for (Boolean item : items) 
        mutableList.add(item);
    
    return Collections.unmodifiableList(mutableList);

    ArrayList 根据其内容实现.equals().hashCode()(正确且高效),因此每个bList(false, false) 与其他bList(false, false) 具有相同的哈希码,并且将等于其他所有bList(false, false)

    将其包裹在 Collections.unmodifiableList() 中可防止修改。

修改您的示例以使用 bList() 只需要更改一些声明和类型签名。它与您的原件一样清晰且几乎一样简短:

public class main 
    public static HashMap<List<Boolean>, Integer> h;

    public static void main(String[] args)
        List<Boolean> a = bList(false, false);

        h = new HashMap<>();
        h.put(a, 1);

        if(h.containsKey(a)) System.out.println("Found a");

        List<Boolean> t = bList(false, false);

        if(h.containsKey(t)) System.out.println("Found t");
        else System.out.println("Couldn't find t");
    

通用解决方案

public <T> List<T> bList(T... items) 
    List<T> mutableList = new ArrayList<>();
    for (T item : items) 
        mutableList.add(item);
    
    return Collections.unmodifiableList(mutableList);

上述解决方案的其余部分没有改变,但这将利用 Java 的内置类型推断来处理任何原语或对象(尽管我建议只使用不可变类)。

库解决方案

使用 Google Guava's ImmutableList.of() 或我自己的 Paguro's vec() 或其他提供此类预测试方法的库(加上不可变/不可修改的集合等),而不是 bList()


劣质解决方案

这是我在 2017 年的原始答案。我把它留在这里是因为有人觉得它很有趣,但我认为它是二流的,因为 Java 已经包含 ArrayList 和 Collections.unmodifiableList() 可以解决这个问题。使用 .equals() 和 .hashCode() 方法编写自己的集合包装器比使用内置方法更费力、更容易出错、更难验证,因此更难阅读。

这应该适用于任何类型的数组:

class ArrayHolder<T> 
    private final T[] array;
    @SafeVarargs
    ArrayHolder(T... ts)  array = ts; 
    @Override public int hashCode()  return Arrays.hashCode(array); 
    @Override public boolean equals(Object other) 
        if (array == other)  return true; 
        if (! (other instanceof ArrayHolder) ) 
            return false;
        
        //noinspection unchecked
        return Arrays.equals(array, ((ArrayHolder) other).array);
    

这是您转换为使用 ArrayHolder 的具体示例:

// boolean[] a = false, false;
ArrayHolder<Boolean> a = new ArrayHolder<>(false, false);

// h = new HashMap<boolean[], Integer>();
Map<ArrayHolder<Boolean>, Integer> h = new HashMap<>();

h.put(a, 1);

// if(h.containsKey(a)) System.out.println("Found a");
assertTrue(h.containsKey(a));

// boolean[] t = false, false;
ArrayHolder<Boolean> t = new ArrayHolder<>(false, false);

// if(h.containsKey(t)) System.out.println("Found t");
assertTrue(h.containsKey(t));

assertFalse(h.containsKey(new ArrayHolder<>(true, false)));

我使用过 Java 8,但我认为 Java 7 拥有您所需的一切。我使用TestUtils 测试了 hashCode 和 equals。

【讨论】:

【参考方案2】:

Map 的实现依赖于 key 的 equalshashCode 方法。 java中的数组直接从Object扩展而来,它们使用默认的equalsObjecthashCode,只比较identity

如果我是你,我会创建一个类Key

class Key 
    private final boolean flag1;
    private final boolean flag2;

    public Key(boolean flag1, boolean flag2) 
        this.flag1 = flag1;
        this.flag2 = flag2;
    

    @Override
    public boolean equals(Object object) 
        if (!(object instanceof Key)) 
            return false;
        

        Key otherKey = (Key) object;
        return this.flag1 == otherKey.flag1 && this.flag2 == otherKey.flag2;
    

    @Override
    public int hashCode() 
        int result = 17; // any prime number
        result = 31 * result + Boolean.valueOf(this.flag1).hashCode();
        result = 31 * result + Boolean.valueOf(this.flag2).hashCode();
        return result;
    

之后,您可以将您的密钥与Map 一起使用:

Map<Key, Integer> map = new HashMap<>();

Key firstKey = new Key(false, false);
map.put(firstKey, 1);

Key secondKey = new Key(false, false) // same key, different instance
int result = map.get(secondKey); // --> result will be 1

参考: Java hash code from one field

【讨论】:

【参考方案3】:

您可以使用接受外部散列和比较策略的库 (trove)。

class MyHashingStrategy implements HashingStrategy<boolean[]> 

    @Override
    public int computeHashCode(boolean[] pTableau) 
        return Arrays.hashCode(pTableau);
    

    @Override
    public boolean equals(boolean[] o1, boolean[] o2) 
        return Arrays.equals(o1, o2);
    



Map<boolean[], T> map = new TCustomHashMap<boolean[],T>(new MyHashingStrategy());

【讨论】:

【参考方案4】:

Map 使用equals() 来测试你的键是否相同。

Object 中该方法的默认实现测试==,即引用相等。所以,由于你的两个数组不是同一个数组,equals 总是返回 false。

您需要对两个数组进行映射调用Arrays.equals 以检查是否相等。

您可以创建一个使用Arrays.equals 的数组包装类,然后这将按预期工作:

public static final class ArrayHolder<T> 

    private final T[] t;

    public ArrayHolder(T[] t) 
        this.t = t;
    

    @Override
    public int hashCode() 
        int hash = 7;
        hash = 23 * hash + Arrays.hashCode(this.t);
        return hash;
    

    @Override
    public boolean equals(Object obj) 
        if (obj == null) 
            return false;
        
        if (getClass() != obj.getClass()) 
            return false;
        
        final ArrayHolder<T> other = (ArrayHolder<T>) obj;
        if (!Arrays.equals(this.t, other.t)) 
            return false;
        
        return true;
    


public static void main(String[] args) 
    final Map<ArrayHolder<Boolean>, Integer> myMap = new HashMap<>();

    myMap.put(new ArrayHolder<>(new Boolean[]true, true), 7);
    System.out.println(myMap.get(new ArrayHolder<>(new Boolean[]true, true)));

【讨论】:

我认为在这里使用== 可能会导致误导,因为在Java 中== 运算符用于比较身份。 Map 实际上是使用equals 来比较key。【参考方案5】:

您可以创建一个包含该数组的类。根据值实现该类的 hashCode() 和 equals() 方法:

public class boolarray 
  boolean array[];

  public boolarray( boolean b[] ) 
     array = b;
  

  public int hashCode() 
    int hash = 0;
    for (int i = 0; i < array.length; i++)
       if (array[i])
          hash += Math.pow(2, i);
    return hash;
  

  public boolean equals( Object b ) 
     if (!(b instanceof boolarray))
        return false;
     if ( array.length != ((boolarray)b).array.length )
        return false;
     for (int i = 0; i < array.length; i++ )
        if (array[i] != ((boolarray)b).array[i])
           return false;
     return true;
  

然后您可以使用:

 boolarray a = new boolarray( new boolean[] true, true  );
 boolarray b = new boolarray( new boolean[] true, true  );
 HashMap<boolarray, Integer> map = new HashMap<boolarray, Integer>();
 map.put(a, 2);
 int c = map.get(b);
 System.out.println(c);

【讨论】:

【参考方案6】:

不可能对数组执行此操作,因为任何两个不同的数组都不会比较 equals,即使它们具有相同的元素。

您需要从容器类进行映射,例如ArrayList&lt;Boolean&gt;(或简单的List&lt;Boolean&gt;。也许BitSet 会更合适。

【讨论】:

【参考方案7】:

你不能这样做。 ta 将具有不同的 hashCode() 值,因为 java.lang.Array.hashCode() 方法继承自 Object,它使用引用来计算哈希码(默认实现)。因此,数组的哈希码是依赖于引用的,这意味着您将获得ta 的不同哈希码值。此外,equals 不适用于这两个数组,因为这也是基于引用。

您可以做到这一点的唯一方法是创建一个将boolean 数组保留为内部成员的自定义类。然后,您需要覆盖 equalshashCode,以确保包含具有相同值的数组的实例相等且具有相同的哈希码。

一个更简单的选择可能是使用List&lt;Boolean&gt; 作为键。根据documentation,hashCode()List 实现定义为:

int hashCode = 1;
Iterator<E> i = list.iterator();
while (i.hasNext()) 
    E obj = i.next();
    hashCode = 31*hashCode + (obj==null ? 0 : obj.hashCode());

如您所见,它取决于列表中的值而不是引用,因此这应该适合您。

【讨论】:

@gaganbm 确保Map&lt;K, V&gt; 的通用键参数是List&lt;Boolean&gt; 而不是ArrayList&lt;Boolean&gt;。最好使用接口而不是具体类型,以便以后可以根据需要切换实现。所以你的Map 看起来像Map&lt;List&lt;Boolean&gt;, Integer&gt; 嗯,它说: List 类型不是通用的;它不能用参数 参数化 @gaganbm,你在用java.util.List吗? @zch 哦,好的。 Eclipse 为我填写了java.awt.List【参考方案8】:
boolean[] t;
t = a;

如果你给出这个,而不是boolean[] t = false, false;,那么你会得到想要的输出。

这是因为Mapreference 存储为key,在您的情况下,尽管t 具有相同的值,但它与a 的引用不同。

因此,当您提供t=a 时,它会起作用。

和这个非常相似:-

String a = "ab";
String b = new String("ab");

System.out.println(a==b); // This will give false.

ab 的值相同,但引用不同。因此,当您尝试使用== 比较参考时,它会给出false

但如果你给a = b;,然后尝试比较reference,你会得到true

【讨论】:

这不是我想要问的。假设我在运行时从某个地方获取了一个布尔数组,我将如何检查它是否存在于 Map 中? 您需要获取该元素及其引用,然后使用您刚刚获取的引用检查其值。您不能直接将新数组作为键,因为它的引用不在地图中。【参考方案9】:

可能是因为数组返回的 equals() 方法的行为与您预期的不同。您应该考虑实现自己的收集并覆盖 equals() 和 hashCode()。

【讨论】:

以上是关于如何使 HashMap 以数组为键工作?的主要内容,如果未能解决你的问题,请参考以下文章

JDK1.7中的HashMap

找出缺失整数

Java:在 HashMap 中使用 ArrayList,字符串为键

HashMap的工作原理(图文+例子)详解,绝对简单通俗易懂

阿里 HashMap 面试夺命连环 21 问

阿里 HashMap 面试夺命连环 21 问