带有对象列表的HashMap作为键

3

在 HashMap 中,当我将对象列表作为键传递时,我会得到不同的结果。

List<NewClass> list1 = new ArrayList<>();
List<NewClass> list2 = new ArrayList<>();

NewClass obj1 = new NewClass(1, "ddd", "eee@gmail.com");
NewClass obj2 = new NewClass(2, "ccc", "kkk@gmail.com");

list1.add(obj1);
list1.add(obj2);

list2.add(obj1);
list2.add(obj2);

Map<List<NewClass>, Integer> mapClass = new HashMap<>();
mapClass.put(list1, 1234);
mapClass.put(list2, 4567);

System.out.println(mapClass.size());
System.out.println(mapClass.get(list1));

NewClass obj4 = new NewClass(1, "ddd", "eee@gmail.com");
NewClass obj5 = new NewClass(2, "ccc", "kkk@gmail.com");
List<NewClass> list3 = new ArrayList<>();
list3.add(obj4);
list3.add(obj5);

System.out.println(mapClass.get(list3));

System.out.println(list1.hashCode());
System.out.println(list2.hashCode());
System.out.println(list3.hashCode());

以下是我看到的输出

hashCode() called - Computed hash: -1704251796
hashCode() called - Computed hash: -587009612
hashCode() called - Computed hash: -1704251796
hashCode() called - Computed hash: -587009612
1
hashCode() called - Computed hash: -1704251796
hashCode() called - Computed hash: -587009612
4567
hashCode() called - Computed hash: -1704251796
hashCode() called - Computed hash: -587009612
**null**
hashCode() called - Computed hash: -1704251796
hashCode() called - Computed hash: -587009612
-1879206775
hashCode() called - Computed hash: -1704251796
hashCode() called - Computed hash: -587009612
-1879206775
hashCode() called - Computed hash: -1704251796
hashCode() called - Computed hash: -587009612
-1879206775

尽管三个列表的哈希码相同,但mapClass.get(list3)返回null,而list3具有与list1/list2相同的对象。这种行为是为什么?

5
你的NewClass是否有适当的equals实现?这是必需的。 - luk2302
3
将(可变)列表用作映射键通常是一个非常糟糕的想法。 - luk2302
2
使用任何可变对象作为Map键通常是一个非常非常非常糟糕的想法。 - GhostCat
3个回答

2

从 map V get(Object key) 文档中得知:

 * ... if this map contains a mapping from a key
 * {@code k} to a value {@code v} such that
 * {@code Objects.equals(key, k)},
 * then this method returns {@code v}; otherwise
 * it returns {@code null}. ...

我不确定你是如何实现NewClassequals方法的,但以下的NewClass实现在调用System.out.println(mapClass.get(list3))时不会返回null

public class NewClass {
    private int id;
    private String name;
    private String mail;

    NewClass(int id,String name,String mail){
        this.id = id;
        this.name = name;
        this.mail = mail;
    }

    @Override
    public int hashCode() {
        return id * name.hashCode() * mail.hashCode();
    }

    @Override
    public boolean equals(Object o) {

        if (o == this) return true;
        if (!(o instanceof NewClass)) {
            return false;
        }
        NewClass newClass = (NewClass) o;

        return newClass.id == id &&
               newClass.name.equals(name) &&
               newClass.mail.equals(mail);
    }
}

此外,正如评论中提到的那样,可变键不是一个好主意,请查看此链接,其中详细说明了原因。

2
即使那些问题得到了解决,真正错误的部分是使用(可变)列表作为映射键。 - GhostCat
是的,那不好,我只是试图回答这个问题。我在答案中发布了一个链接,解释了为什么使用可变映射键是一个坏主意。 - happy songs

1
尽管这3个列表的哈希码相同,但mapClass.get(list3)返回null。list3具有与list1/list2相同的对象。为什么会出现这种情况?
我猜测这个问题是由于你的自定义类的equals()方法引起的。必须以这样的方式实现它,即根据equals()相等的一对对象中的每个对象都具有相同的哈希码。
作为一个经验法则,当使用集合时,应该为你的类提供适当的equals/hasHode实现。
由于你没有公开NewClass的代码,我将使用java.lang.String类进行演示,它维护了equals/hasHode不变式。
List<String> list1 = List.of("a", "b", "c");
List<String> list2 = List.of("a", "b", "c"); // list containing the same pooled strings (i.e. references to the same objects)
List<String> list3 = new ArrayList<>(List.of(new String("a"), new String("b"), new String("c")));
System.out.println("list1 is equal to list3: " + list1.equals(list3));
        
Map<List<String>, Integer> map = new HashMap<>();
map.put(list1, 1);
map.put(list2, 2);
map.put(list3, 3);
        
System.out.println("map's size: " + map.size()); // contains a single entry: list3 -> 3
map.forEach((k, v) -> System.out.println(k + " -> " + v));
    
// let's break it!
System.out.println("______________________");
        
list3.add("d"); // list3 contains "a", "b", "c", "d"
List<String> list4 = List.of("a", "b", "c", "d");
        
map.put(list4, 4);
System.out.println("map's size: " + map.size()); // contains two entries!
map.forEach((k, v) -> System.out.println(k + " -> " + v));
// let's check the hashes
System.out.println("hashCodes:\nlist3: " + list3.hashCode() + " list4: " + list4.hashCode());

输出结果将是:
list1 is equal to list3: true
map's size: 1
[a, b, c] -> 3
______________________
map's size: 2
[a, b, c] -> 3
[a, b, c, d] -> 4
hashCodes:
list3: 3910595 list4: 3910595

正如您所看到的,无论一个列表包含池化或非池化字符串,只要它们相等并且顺序相同,这些列表就会相等。

上面代码的第二部分演示了为什么将List用作不是一个好主意。

HashMap旨在与不可变对象一起使用。它由数组支持。数组的每个元素都是一个,对应于一系列哈希值,并且它可能包含一系列节点在达到一定阈值后,列表会转换为树以提高性能)。

这就是Node的样子:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
...


当您在HashMap上调用put()方法时,将计算给定key的哈希值。基于hash,它将为该key找到一个合适的bucket。然后,将检查该bucket中所有nodeshashes是否与new hash相等。如果找不到相等的hash,则会创建一个新的Node并放置在该bucket中。当两个哈希值冲突时,将使用equals()比较这些keys。如果返回false,则会创建一个new node,否则现有节点的value将被替换为given value
请注意,字段keyhashfinal的。
在某些情况下,计算哈希可能非常昂贵,因此由于HashMap不打算与可变对象一起使用,因此每次进行新比较时重复计算相同key的哈希值是不必要的。
因此,尽管哈希值相同,list4将被视为new key,因为map中的list3的哈希值永远不会更新。

1
你的代码问题存在于许多方面:使用List作为键、重复使用同一引用作为第二个元素以及NewClass实现。正如其他人已经指出的那样,你使用一个ArrayList作为HashMap的键是一个不好的设计选择,因为作为可变对象,它的hashCode可能会改变,从而导致失去对其配对元素的访问风险。此外,也会影响你的是ArrayList的hashCode和equals方法是在元素实现这些方法时定义的。

enter image description here

ArrayList的hashCode()方法

enter image description here

enter image description here

HashMap实现

HashMap hashCode() get equals()


代码解释

从输出结果中可以看到,在添加了具有不同列表作为键的第二个键值对后,我们可以看到映射的大小仍为1。这是因为您使用完全相同的引用(obj1和obj2)构建了第一个和第二个键,导致list1和list2具有相同的hashCode,因为ArrayList的hashCode是基于其元素构建的。一旦将第二个键对添加到HashMap中,它的键的hashCode返回第一个键的相同值,索引相同的桶,然后替换第一个键值对,因为第二个键等于第一个键值对的键。

现在回答你的问题。即使List的元素是不同的引用,但只要NewClass的equals()方法已经定义了传递给构造函数的确切三个字段(int、String、String),所描述的情况仍将发生。我猜测NewClass的equals()方法没有被定义或者遇到了不同的字段。equals()和hashCode()应该在相同的字段集上工作。实际上,如果我们按照下面的方式定义你的NewClass,第三个添加操作也会替换HashMap中唯一包含的一对。
public class Test {
    public static void main(String[] args) {
        List<NewClass> list1 = new ArrayList<>();
        List<NewClass> list2 = new ArrayList<>();

        NewClass obj1 = new NewClass(1, "ddd", "eee@gmail.com");
        NewClass obj2 = new NewClass(2, "ccc", "kkk@gmail.com");

        list1.add(obj1);
        list1.add(obj2);

        list2.add(obj1);
        list2.add(obj2);

        Map<List<NewClass>, Integer> mapClass = new HashMap<>();
        mapClass.put(list1, 1234);
        mapClass.put(list2, 4567);

        System.out.println(mapClass.size());
        System.out.println(mapClass.get(list1));

        NewClass obj4 = new NewClass(1, "ddd", "eee@gmail.com");
        NewClass obj5 = new NewClass(2, "ccc", "kkk@gmail.com");
        List<NewClass> list3 = new ArrayList<>();
        list3.add(obj4);
        list3.add(obj5);

        System.out.println(mapClass.get(list3));

        System.out.println(list1.hashCode());
        System.out.println(list2.hashCode());
        System.out.println(list3.hashCode());
    }
}

class NewClass {
    int id;
    String s1, s2;

    public NewClass(int id, String s1, String s2) {
        this.id = id;
        this.s1 = s1;
        this.s2 = s2;
    }

    public int hashCode() {
        return Objects.hash(id, s1, s2);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) return true;
        if (obj == null || obj.getClass() != getClass()) return false;
        NewClass nc = (NewClass) obj;
        return nc.id == id && Objects.equals(s1, nc.s1) && Objects.equals(s2, nc.s2);
    }
}


Output

enter image description here

结论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接