JDK 11 使用 Set.of 时的泛型问题

JDK 11 Generics Issue when using Set.of

我无法理解以下使用 JDK 时的类型安全问题 11. 谁能解释一下当我直接在参数中传递 Set.of 时没有出现编译错误的原因:

public static void main(String[] args) {
    var intSet1 = Set.of(123, 1234, 101);
    var strValue = "123";
    isValid(strValue, intSet1);// Compilation error (Expected behaviour)
    **isValid(strValue, Set.of(123, 1234, 101));// No Compilation error**
}

static <T> boolean isValid(T value, Set<T> range) {
    return range.contains(value);
}

你可以运行这个code live at IdeOne.com

isValid(strValue, Set.of(123, 1234, 101));

当我将鼠标悬停在 Eclipse 上的这个 isValid() 调用上时,我看到它将执行以下方法:

<Serializable> boolean com.codebroker.dea.test.StringTest.isValid(Serializable value, Set<Serializable> range)

当编译器尝试解析用于 isValid 的泛型类型参数 T 的可能类型时,它需要找到 String 的公共超类型(类型strValue) 和 IntegerSet.of(123, 1234, 101) 的元素类型),并找到 Serializable.

因此 Set.of(123, 1234, 101) 被解析为 Set<Serializable> 而不是 Set<Integer>,因此编译器可以将 SerializableSet<Serialiable> 传递给 isValid(),这是有效的。

isValid(strValue, intSet1);

另一方面,当您第一次将 Set.of(123,1234,101) 赋给一个变量时,它会被解析为 Set<Integer>。在这种情况下,StringSet<Integer> 无法传递给您的 isValid() 方法。

如果你改变

var intSet1 = Set.of(123, 1234, 101);

Set<Serializable> intSet1 = Set.of(123,1234,101);

然后

isValid(strValue, intSet1);

也会通过编译。

简而言之,编译器在第一次调用时卡在你声明的类型上,但在第二次调用时有一定的自由度来推断兼容类型。

使用 isValid(strValue, intSet1);,您调用的是 isValid(String, Set<Integer>),编译器不会将两个参数的 T 解析为同一类型。这就是它失败的原因。编译器根本无法更改您声明的类型。

isValid(strValue, Set.of(123, 1234, 101)) 相比,Set.of(123, 1234, 101) 是一个多边形表达式,其类型在调用上下文中建立。因此,编译器致力于推断适用于上下文的 T 。正如 Eran 指出的那样,这是 Serializable.

为什么第一个有效而第二个无效?这仅仅是因为编译器对作为第二个参数给出的表达式的类型具有一定的灵活性。 intSet1 是一个独立的表达式,而 Set.of(123, 1234, 101) 是一个多边形表达式(参见 JLS and this description about poly expression) .在第二种情况下,上下文允许编译器计算适用于与第一个参数的类型 String 兼容的具体 T 的类型。

当您(作为人类)查看编译的 second isValid 时,可能会想 - 这怎么可能?编译器将类型 T 推断为 StringInteger,因此调用肯定会失败。

编译器在查看方法调用时以一种非常不同的方式思考。它会查看方法参数和提供的类型,并尝试为您推断出完全不同且出乎意料的类型。有时,这些类型是“不可表示的”,这意味着编译器可以存在的类型,但作为用户的您不能声明此类类型。

有一个特殊的(未记录的)标志,您可以使用它来编译您的 class 并了解编译器是如何“思考”的:

 javac --debug=verboseResolution=all YourClass.java

输出会很长,但我们关心的主要部分是:

  instantiated signature: (INT#1,Set<INT#1>)boolean
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>isValid(T,Set<T>)
  where INT#1,INT#2 are intersection types:
    INT#1 extends Object,Serializable,Comparable<? extends INT#2>,Constable,ConstantDesc
    INT#2 extends Object,Serializable,Comparable<?>,Constable,ConstantDesc

可以看到推断和使用的类型不是StringInteger