Java HashMap嵌套泛型与通配符

7

我正在尝试创建一个哈希映射,其中包含自定义类的不同子类的哈希集合的哈希映射值,如下所示:

HashMap<String, Hashmap<String, HashSet<? extends AttackCard>>> superMap

AttackCard有一些子类,例如:MageAssassinFighter。超级Map中的每个HashMap将只包含一种AttackCard

当我尝试添加一个

HashMap<String, HashSet<Assassin>>

当我将代码移植到SuperMap时,我遇到了编译错误: comiler error 以下是出错的代码段:
public class CardPool {

private HashMap<String, HashMap<String, HashSet<? extends AttackCard>>> attackPool =
    new HashMap<>();

private ArrayList<AuxiliaryCard> auxiliaryPool;

public CardPool() {
(line 24)this.attackPool.put("assassins", new AssassinPool().get());
/*  this.attackPool.put("fighters", new Fighter().getPool());
    this.attackPool.put("mages", new Mage().getPool());
    this.attackPool.put("marksmen", new Marksman().getPool());
    this.attackPool.put("supports", new Support().getPool());
    this.attackPool.put("tanks", new Tank().getPool());
*/  
    this.auxiliaryPool = new ArrayList<>(new AuxiliaryCard().getPool()); 
}

以下是AssassinPool get方法的代码片段:

private HashMap<String, HashSet<Assassin>> pool = new HashMap<>();

    public HashMap<String, HashSet<Assassin>> get() {
        return pool;
    }

我想评论一下,通过将所有 AttackCardPools 比如 AssassinPool 返回并包含 AttackCard 的 HashSet 而不是它们各自的子类,我可以轻松解决我的问题并得到一个良好运行的程序。不过,现在我正在尝试理解这个编译错误 :)

compilation error at line 24: error: no suitable method found for `put(String, HashMap<String,HashSet<Assassin>>>` 
this.attackPool.put("assassins", new AssassinPool(). get()); 
method HashMap.putp.(String, HashMap<String,HashSet<? extends AttackCard>>>` is not applicable (actual argument `HashMap<String, HashSet<Assassin>>` cannot be converted to `HashMap<String, HashSet<? extends AttackCard>>` by method invocation conversion)

1
请将异常堆栈跟踪作为文本发布在此处,而不是作为图像或图像链接。 - Rahul
3
一个哈希表包含了多个哈希表,每个哈希表里又包含了多个哈希集合。这个层次太深了,你可以尝试编写一个自定义对象来处理顶层哈希表的值。 - sp00m
编译错误在第24行:错误:找不到适合的方法put(String,HashMap<String,HashSet<Assassin>>> this.attackPool.put("assassins", new AssassinPool().get()); 方法HashMap.putp。(String,HashMap<String,HashSet<? extends AttackCard>>>不适用(实际参数HashMap<String,HashSet<Assassin>>无法通过方法调用转换转换为HashMap<String,HashSet<? extends AttackCard>>) - user2651804
2
不要在SO上发布图像,除非它真的是一张图片。请用文本形式编写错误消息。 - Rohit Jain
3个回答

16
多级通配符有时可能会有些棘手,如果处理不当。您应该先学习如何阅读多级通配符。然后您需要学习解释多级通配符中extendssuper边界的含义。这些是您必须首先学习的重要概念,否则您很快就会发疯。 解释多级通配符: **多级通配符* 应该自上而下阅读。首先阅读最外层类型。如果那又是一个参数化类型,则深入到该参数化类型的类型中。理解具体参数化类型通配符参数化类型的含义在了解如何使用它们方面起着关键作用。例如:
List<? extends Number> list;   // this is wildcard parameterized type
List<Number> list2;            // this is concrete parameterized type of non-generic type
List<List<? extends Number>> list3;  // this is *concrete paramterized type* of a *wildcard parameterized type*.
List<? extends List<Number>> list4;  // this is *wildcard parameterized type*

前两个很清楚。

看看第三个。你会如何解释这个声明?只需思考,哪些类型的元素可以放在该列表中。所有可以转换为List<? extends Number>的元素都可以放在外部列表中:

  • List<Number> - 是
  • List<Integer> - 是
  • List<Double> - 是
  • List<String> -

参考资料:

考虑到列表的第三个实例可以容纳上述类型的元素,将引用分配给如下列表是错误的:

List<List<? extends Number>> list = new ArrayList<List<Integer>>();  // Wrong

上述赋值操作不应该生效,否则你可能会做类似这样的事情:
list.add(new ArrayList<Float>());  // You can add an `ArrayList<Float>` right?

所以,发生了什么?你只是向一个本应只能容纳List<Integer>的集合中添加了一个ArrayList<Float>。这肯定会在运行时给你带来麻烦。这就是为什么不允许这样做,并且编译器只在编译时防止这种情况发生。
然而,请考虑多级通配符的第4个实例化。该列表表示所有类型参数为List<Number>子类的List实例化的家族。因此,对于这些列表,以下赋值是有效的:
list4 = new ArrayList<Integer>(); 
list4 = new ArrayList<Double>(); 

参考资料:


关于单层通配符:

现在这可能已经在你的脑海中形成了一个清晰的图像,这与泛型的不变性有关。 List<Number> 不是 List<Double>,尽管 NumberDouble 的超类。同样,List<List<? extends Number>> 不是 List<List<Integer>>,即使 List<? extends Number>List<Integer> 的超类。

接下来是具体问题:

您已将您的映射声明为:

HashMap<String, Hashmap<String, HashSet<? extends AttackCard>>> superMap;

请注意,该声明中存在3级嵌套。要小心。它类似于List >>,这与List >不同。
现在你可以向superMap添加哪些元素类型?当然,你不能将HashMap >添加到superMap中。为什么?因为我们不能做这样的事情:
HashMap<String, HashSet<? extends AttackCard>> map = new HashMap<String, HashSet<Assassin>>();   // This isn't valid

你只能将HashMap<String,HashSet<? extends AttackCard>>分配给map,因此只能将该类型的地图作为值放入superMap中。

选项1:

因此,一种选择是修改您在Assassin类中的代码的最后一部分(我猜是这个)为:

private HashMap<String, HashSet<? extends AttackCard>> pool = new HashMap<>();

public HashMap<String, HashSet<? extends AttackCard>> get() {
    return pool;
}

...然后一切都将正常运作。

选项2:

另一个选项是将superMap的声明更改为:

private HashMap<String, HashMap<String, ? extends HashSet<? extends AttackCard>>> superMap = new HashMap<>();

现在,您可以将一个 HashMap<String, HashSet<Assassin>> 放入 superMap 中。怎么做呢?想一想。 HashMap<String, HashSet<Assassin>> 可以捕获转换为 HashMap<String, ? extends HashSet<? extends AttackCard>>。对吧?所以内部映射的以下赋值是有效的:
HashMap<String, ? extends HashSet<? extends AttackCard>> map = new HashMap<String, HashSet<Assassin>>();

因此,您可以在上面声明的 superMap 中放置一个 HashMap<String,HashSet<Assassin>> 。然后您在 Assassin 类中的原始方法将正常工作。

奖励分:

解决当前问题后,您还应考虑将所有具体类类型引用更改为它们各自的超级接口。您应将superMap的声明更改为:

Map<String, Map<String, ? extends Set<? extends AttackCard>>> superMap;

所以你可以将HashMapTreeMapLinkedHashMap分配给superMap的任何类型。此外,您还可以将HashMapTreeMap添加为superMap的值。理解Liskov替换原则的使用非常重要。

我想评论一下,我正在将superMap初始化为"new Hashset<>()"。superMap中包含的实际HashMap来自其他类,例如:superMap.put("key1", new OtherClass.getHashMap())。 - user2651804
最后,解决方案有点令人失望。因为问题首先出现是因为我想在各自的子类中进行强类型检查。例如,AssassinPool应该仅包含Assassin的HashSet。 - user2651804
我已经花了很长很长的时间盯着你的答案看,你可能犯了一个逻辑错误吗?你说唯一不进入list3的是List<String>。但是在评论中,你也说List<Integer>也不会进入list3? - user2651804
这就引出了一个简单的问题:<? extends SomeSuperclass> 是什么意思?(如果它不接受子类的话)我现在会尝试阅读一些泛型教程。 - user2651804
@user2651804 我想是的。你应该通过一些教程来更好地理解有界通配符。你也可以查看我的另一个答案,我在其中简要解释了它们的含义。 - Rohit Jain
显示剩余11条评论

2
不要使用HashSet<? extends AttackCard>,在所有声明中都应该使用HashSet<AttackCard>,包括superMap和所有被添加的Set。
你仍然可以将AttackCard的子类存储在Set<AttackCard>中。
应该使用抽象类型而不是具体实现来声明变量,例如:
Map<String, Map<String, Set<? extends AttackCard>>> superMap

请参见 里氏替换原则

虽然将 HashSet<? extends AttackCard> 更改为 HashSet<AttackCard> 将允许他在集合中添加子类。但这不会允许将 HashSet<Assassin> 添加到该 map 中。 - Rohit Jain
@RohitJain 我建议在所有地方使用 Set<AttackCard> 来声明。 - Bohemian
@Bohemian 好的没问题。但是我猜你的声明应该是 - Map<String,Map<String,Set<AttackCard>>>。但是你不能把一个 HashSet<Assassin> 放在内部映射中。这就是我所说的。 - Rohit Jain
好的,在我深入讲解泛型类之前,你能确认一下如果我的映射表不是两层的话,我就不会遇到任何问题吗?我的意思是,如果我想要一个HashMap<String,AttackCard>,我可以将Assassin添加到key1,将Fighter添加到key2,而不会出现问题吗? - user2651804
@user2651804,是的,你可以这样做。在开始使用多级通配符之前,我建议你先阅读一些泛型教程。 - Rohit Jain
以上评论是要发布给Rohit Jain的答案的 =] - user2651804

0

在我尝试理解线程失败后,我直接用super替换了extends。但是我仍然得到了相同的编译错误 =/ - user2651804

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