如何在Java Maps中使用Sets作为键

20

我有一个使用Set作为键类型的Map,就像这样:

Map<Set<Thing>, Val> map;

当我查询map.containsKey(myBunchOfThings)时,它返回false,我不明白为什么。我可以通过迭代keyset中的每个键并验证是否有一个键(1)具有相同的hashCode,并且(2)等于我的myBunchOfThings。

System.out.println(map.containsKey(myBunchOfThings)); // false.
for (Set<Thing> k : map.keySet()) {
  if (k.hashCode() == myBunchOfThings.hashCode() && k.equals(myBunchOfThings) {
     System.out.println("Fail at life."); // it prints this.
  }
}

我是否基本上误解了containsKey的约定?在将集合(或更一般地说,集合)作为映射键时,是否有使用技巧?

4个回答

25

еңЁдҪҝз”Ё Map ж—¶пјҢй”®дёҚеә”иҜҘиў«дҝ®ж”№гҖӮJavaж–ҮжЎЈдёӯжҢҮеҮәпјҡ

жіЁж„ҸпјҡеҰӮжһңдҪҝз”ЁеҸҜеҸҳеҜ№иұЎдҪңдёәmapзҡ„й”®пјҢеҲҷеҝ…йЎ»йқһеёёе°ҸеҝғгҖӮеҰӮжһңеңЁеҜ№иұЎдҪңдёәkeyж—¶д»ҘдёҖз§ҚеҪұе“ҚequalsжҜ”иҫғзҡ„ж–№ејҸжӣҙж”№дәҶе®ғзҡ„еҖјпјҢеҲҷmapзҡ„иЎҢдёәжңӘжҢҮе®ҡгҖӮиҝҷз§ҚзҰҒжӯўзҡ„зү№ж®Ҡжғ…еҶөжҳҜmapдёҚиғҪеҢ…еҗ«иҮӘиә«дҪңдёәдёҖдёӘkeyгҖӮиҷҪ然mapеҸҜд»ҘеҢ…еҗ«иҮӘиә«дҪңдёәдёҖдёӘvalueпјҢдҪҶжҳҜйңҖиҰҒжһҒеәҰи°Ёж…Һпјҡequalsе’ҢhashCodeж–№жі•еңЁиҝҷж ·зҡ„mapдёҠдёҚеҶҚе®ҡд№үгҖӮ

жҲ‘зҹҘйҒ“иҝҷдёӘй—®йўҳпјҢдҪҶзӣҙеҲ°зҺ°еңЁжүҚиҝӣиЎҢжөӢиҜ•е№¶иҜҰз»ҶиҜҙжҳҺпјҡ

   Map<Set<String>, Object> map  = new HashMap<Set<String>, Object>();

   Set<String> key1 = new HashSet<String>();
   key1.add( "hello");

   Set<String> key2 = new HashSet<String>();
   key2.add( "hello2");

   Set<String> key2clone = new HashSet<String>();
   key2clone.add( "hello2");

   map.put( key1, new Object() );
   map.put( key2, new Object() );

   System.out.println( map.containsKey(key1)); // true
   System.out.println( map.containsKey(key2)); // true
   System.out.println( map.containsKey(key2clone)); // true

   key2.add( "mutate" );

   System.out.println( map.containsKey(key1)); // true
   System.out.println( map.containsKey(key2)); // false
   System.out.println( map.containsKey(key2clone)); // false (*)

   key2.remove( "mutate" );

   System.out.println( map.containsKey(key1)); // true
   System.out.println( map.containsKey(key2)); // true
   System.out.println( map.containsKey(key2clone)); // true
key2 被改变之后,地图再也没有包含它了。我们可以认为在添加数据时,地图会“索引”数据,因此我们仍然期望它仍然包含具有相同键的对象(带有 * 标注的行)。但有趣的是,事实并非如此。
所以,根据Java文档所说,键不应该被改变,否则其行为是未指定的。就这样。
我猜这就是你的情况。

你关于 未指定 的事情说得完全正确。我想我需要实现一种类似树状查找结构的东西,其中节点除了子节点外还有值。 - Gabe Johnson

8

您应该尽量使用不可变类型作为Map的键。集合和数组通常很容易发生变化,因此通常不适合以这种方式使用。

如果您想将多个键值用作Map的键,则应使用专为此目的设计的类实现,例如Apache Commons Collections MultiKey

如果您确实必须使用Set或Collection作为键,则首先使其不可变(Collections.unmodifiableSet(...)),然后不要保留对可变后备对象的引用。

使用集合作为键的另一个困难是它们可能按不同的顺序构建。只有排序过的集合才有很高的匹配概率。例如,如果您使用一个顺序排列的ArrayList但第二次以不同的方式构造列表,则它将无法匹配键-散列码和值的顺序不同。

编辑:我在下面的陈述中犯了错误,从未使用过Set来进行键的操作。我刚刚阅读了AbstractHashSet中hashCode实现的一部分。它使用所有值的简单总和,因此不依赖于顺序。Equals还检查一个集合是否包含另一个集合中的所有值。然而,在Java中其他类型的集合仍然是正确的(ArrayList的顺序很重要)。
如果您的集合实际上是 HashSet ,则创建顺序也很重要。实际上,任何一种由哈希管理的集合都会更加棘手,因为任何容量变化都会触发整个集合的重建,这可能会重新排序元素。想想哈希冲突存储在发生冲突的顺序中的情况(所有元素的简单链接链,其中转换后的哈希值相同)。

1
根据Java文档,如果两个集合具有相同的元素,无论顺序如何,它们就是相等的。即使对于“SortedSet”也是如此。 “SortedSet”使用迭代器更改集合的遍历方式,但不影响相等性。元素的顺序只应在列表中考虑。您对“HashSet”的评论确定吗? - ewernli
任何容量更改都会触发整个集合的重建,这可能会重新排序元素。这是正确的,但我认为ewernli是正确的,equals()和hashCode()方法是这样编写的,以使得这种差异不会导致两个相等的集合因为它们的构造方式不同而比较为不相等。 - Tyler
我将避开Apache Collections,因为它似乎没有得到积极维护。至少,它似乎不支持泛型,这让我想要远离它。 - Gabe Johnson
@Gabe:同意,我在Google collections中也没有找到一个使用泛型的好替代品。 - Kevin Brock

2

您是否在插入后修改了集合?如果是这样,可能会导致集合被排序到不同的桶中而无法查找。在迭代时,它确实可以找到您的集合,因为它查找整个映射。

我认为HashMap的合约规定,您不允许修改用作键的对象的哈希码。


0

你在比较键时是否传递了准确的集合(即你要查找的集合)?


可能不会,但这并不重要。 - Jorn

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