Java泛型不一致的行为?

时间:2013-10-23 15:46:41

标签: java generics

为什么第一个方法编译,第二个方法不编译? SetImmutableSet.Builder的泛型相同,其add方法的类型签名也相同。

import java.util.Set;
import java.util.HashSet;
import com.google.common.collect.ImmutableSet;

public class F {

    public static ImmutableSet<? extends Number> testImmutableSetBuilder() {
        ImmutableSet.Builder<? extends Number> builder = ImmutableSet.builder();
        Number n = Integer.valueOf(4);
        builder.add(n);
        return builder.build();
    }

    public static Set<? extends Number> testJavaSet() {
        Set<? extends Number> builder = new HashSet<Number>();
        Number n = Integer.valueOf(4);
        builder.add(n);
        return builder;
    }
}

我正在使用javac版本1.7.0_25进行构建。我在第二种方法上得到以下错误,但在第一种方法上没有。我相信我应该在两种情况下都会收到错误,因为将Number放入? extends Number的集合中并不正确。

error: no suitable method found for add(Number)
        builder.add(n);
               ^
    method Set.add(CAP#1) is not applicable
      (actual argument Number cannot be converted to CAP#1 by method invocation conversion)
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Number from capture of ? extends Number

4 个答案:

答案 0 :(得分:1)

我想我开始找出答案了。 ImmutableSet.Builder方法add被重载,有一个替代签名add(E... elements)。我在生成的.class文件上运行javap -v,我发现这个替代方法实际上是被调用的方法。 varargs elements实际上是一个Java数组,Java数组是协变的。即,就这个具体例子而言,我们称之为

builder.add(n);

Number n被转换为Number[]类型的单元素数组。但我不知道该数组是如何合法地转换为<? extends Number>

的数组

答案 1 :(得分:1)

确实add(E)不适用,但方法add(E...)的问题不太清楚:

Java语言规范defines

  

当且仅当满足以下所有条件时,方法m才是适用的变量方法:

     
      
  • 对于1≤i<1。 n,ei的类型Ai可以通过方法调用转换为Si转换。

  •   
  • 如果k≥n,则对于n≤i≤k,可以通过方法调用转换将ei,Ai的类型转换为Sn的组件类型。

  •   
  • ...

  •   

在我们的案例中,Sn是capture-of-? extends Number[]

现在什么是Sn的组件类型?不幸的是,JLS没有给出该术语的正式定义。特别是,它没有明确说明组件类型是编译时类型(不需要是可恢复的)还是运行时类型(必须是)。对于前者,组件类型将为capture-or-? extends Number,它不能通过方法调用转换接受Number。如果是后者,组件类型将是Number,显然可以。

似乎eclipse编译器的实现者使用了前者的定义,而javac的实现者使用了后者。

两个事实似乎暗示组件类型确实是运行时类型。首先,规范requires

  

如果所分配的值的类型与组件类型不是赋值兼容(第5.2节),则抛出ArrayStoreException。

     

如果数组的组件类型不可恢复(第4.7节),则Java虚拟机无法执行前一段中描述的存储检查。这就是为什么禁止使用具有不可恢复元素类型的数组创建表达式(第15.10节)。可以声明一个数组类型的变量,其元素类型是不可恢复的,但是将数组创建表达式的结果赋值给变量必然会导致未经检查的警告(第5.1.9节)。

第二,针对变量arity方法的方法调用表达式的运行时评估定义如下:

  

如果用k≠n实际参数表达式调用m,或者,如果用k = n实际参数表达式调用m并且第k个参数表达式的类型不与T []赋值兼容,则参数列表(e1,...,en-1,en,...,ek)被评估为好像它被写为(e1,...,en-1,new | T [] | {en, ...,ek}),其中| T [] |表示T []的擦除(第4.6节)。

随意阅读本段可能会让人觉得表达式不只是评估,而且类型检查那样,但规范并不完全这样说。

另一方面,使用擦除进行编译时类型检查是一个奇怪的概念(虽然规范在其他一些情况下需要这样做)。

总而言之,eclipse编译器和javac之间观察到的差异似乎源于对不可重构的变量方法参数的类型检查规则的略微不同的解释。

答案 2 :(得分:0)

当您说Set<? extends Number> builder;时,表示它可能包含extends Number的任何内容,例如Integer。现在,您将Number放在builder.add(n);而不是Integer。如果您需要添加某些内容,请执行Set<? super Number> builder;

答案 3 :(得分:0)

有一种叫做GET&amp; PUT原则。始终建议遵循它。

GET&amp; PUT原则

“当你只从结构中获取值时使用扩展通配符,当你只将值放入结构时使用超级通配符,并且当你同时获取和放置时不使用通配符”