【问题标题】:Why I cannot use wildcard type in parameter to compute为什么我不能在参数中使用通配符类型来计算
【发布时间】:2021-02-22 19:39:38
【问题描述】:

直奔主题(我知道应该避免使用wildcard types as the returning type

我正在写这个answer 和以下代码:

public static Map<?, Long> manualMap(Collection<?> c){
    Map<?, Long> map = new HashMap<>();
    c.forEach(e -> map.compute(e, (k, v) -> (v == null) ? 1 : v + 1));
    return map;
}

收到以下警告:

Required type: capture of ?

Provided: capture of ?

以及来自 IntelliJ 的建议

将变量 'map' 更改为 'Map, Object'

这更没有意义。自然,当我尝试应用该建议时它会失败。

最初,我认为“好吧,这与它与计算签名不匹配这一事实有关”,即:

default V compute(K key, ...) 

所以我尝试了

public class MyMap <T>{
    public static <T> void nothing(Collection<T> c){
         // Empty
    }
}

 public static Map<?, Long> manualMap(Collection<Collection<?>> c, Map<?, Long> map){
     c.forEach(MyMap::nothing);
     return map;
 }

我没有问题。

以下两个版本:

public static <T> Map<?, Long> manualMap(Collection<?> c){
    Map<T, Long> map = new HashMap<>();
    c.forEach(e -> map.compute((T) e, (k, v) -> (v == null) ? 1 : v + 1));
    return map;
}

public static Map<?, Long> manualMap(Collection<?> c){
    Map<Object, Long> map = new HashMap<>();
    c.forEach(e -> map.compute(e, (k, v) -> (v == null) ? 1 : v + 1));
    return map;
}

工作没有任何问题(除了(T)情况下的警告)。

所以问题是

为什么第一个版本不起作用?

【问题讨论】:

  • map.compute(e, ...) 的问题与List&lt;?&gt; anyList; anyList.add(whateverNonNullValue) 的问题相同(错误)。由于anyList 可以保存任何列表,例如List&lt;Integer&gt; List&lt;String&gt; List&lt;Cat&gt;,Java 不能保证添加元素总是安全的。因此,当从List&lt;?&gt; 调用像List#add(T element) 这样的任何方法时,它的T element 实际上是? element,并且唯一可以分配给它的安全值是null。即使您尝试使用anyList1.add( anyList2.get(0) )anyList2.get(0) 也会被删除为Object,这不安全。
  • @Pshemo 感谢您的评论

标签: java generics lambda


【解决方案1】:

第一个方法由于“捕获转换”而无法编译,这发生在每个声明中。您可以阅读my other answer about thisthis one。但简单地说,你会有两个 separate 类型,你可以在编译时看到:

 javac --debug=verboseResolution=all

输出将包含:

.....
CAP#1 extends Object from capture of ?
CAP#2 extends Object from capture of ?
...

这意味着有 两种 类型已被捕获转换。这些类型彼此无关,就像你拥有它的方式一样。

另一方面:

public static <T> void nothing(Collection<T> c){

}

被称为 wildcard capture 方法(它“捕获”通配符),并在official tutorial 中记录了它的工作原理和方式;因此,您对此没有任何问题。


但这里的主要问题是您不能将任何东西(null 除外)分配给通配符。因此,在您的 compute 示例中,第一个参数将被推断为 ?,您无法为其分配任何内容。

【讨论】:

    【解决方案2】:
    public static Map<?, Long> manualMap(Collection<?> c){
        Map<?, Long> map = new HashMap<>();
        c.forEach(e -> map.compute(e, (k, v) -> (v == null) ? 1 : v + 1));
        return map;
    }
    

    这里有三个通配符:参数上的一个,映射变量上的一个,返回值上的一个。 (返回值 one 不是超级相关)。

    您正在尝试将集合中的元素(一种通配符类型)传递给映射的方法(另一种通配符类型)。

    编译器不知道这两个“应该是”相同的,因此它不接受需要“map”通配符的“collection”通配符。

    您可以通过类型变量表明它们是同一类型:

    public static <T> Map<?, Long> manualMap(Collection<T> c){
        Map<T, Long> map = new HashMap<>();
        c.forEach(e -> map.compute(e, (k, v) -> (v == null) ? 1 : v + 1));
        return map;
    }
    

    这里,T 是你不知道的类型;但你知道在这两种情况下它都是相同的未知类型。

    或者你可以用不需要两个通配符的方式声明它:

    return c.stream().collect(groupingBy(a -> a, counting()));
    

    【讨论】:

      【解决方案3】:

      问题是您的方法正在接受“我不在乎”类型的集合,但您也在创建一个带有“我不在乎”键的映射。这并不意味着您不关心的类型是相同的类型。为此,您需要通过使用类型变量来实际绑定类型,例如

      public static <T> Map<T, Long> manualMap(Collection<T> c){
          Map<T, Long> map = new HashMap<>();
          c.forEach(e -> map.compute(e, (k, v) -> (v == null) ? 1 : v + 1));
          return map;
      }
      

      该方法仍将接受任何类型的集合,但它足够关心跟踪类型并且任何地方都没有通配符或警告,耶!

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2020-07-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-16
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多