7

为什么第一种方法可以编译,而第二种方法不行?Set和的泛型ImmutableSet.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 来构建。我在第二种方法中收到以下错误,但在第一种方法中没有。我相信在这两种情况下我都应该得到错误,因为将 aNumber放入? 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

4 回答 4

1

我想我开始想出答案了。ImmutableSet.Builder方法add重载,有一个备用签名add(E... elements)。我javap -v在生成的 .class 文件上运行,我看到这个替代方法是实际被调用的方法。可变参数elements实际上是一个 Java 数组,Java 数组是协变的。即,关于这个具体的例子,我们称

builder.add(n);

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

于 2013-10-23T17:14:52.240 回答
1

Indeedadd(E)不适用,但该方法的问题不太清楚add(E...)

Java 语言规范定义

当且仅当以下所有条件都成立时,方法 m 是适用的可变参数方法:

  • 对于 1 ≤ i < 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 的实现者使用了后者。

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

如果要分配的值的类型与组件类型不兼容(第 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 之间的差异似乎源于对不可具体化的可变参数方法参数的类型检查规则的解释略有不同。

于 2013-10-29T21:53:23.970 回答
0

有一种叫做 GET & PUT 原则的东西。始终建议遵循它。

GET & PUT 原理

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

于 2014-02-24T09:52:35.983 回答
0

当你说它Set<? extends Number> builder;意味着它可以有任何东西extends Number,例如Integer。现在,您将 aNumber放入builder.add(n);其中,而不是Integer. 如果你需要添加一些你应该做的事情Set<? super Number> builder;

于 2013-10-29T16:06:59.933 回答