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

Alo*_*bey 18 java generics static-factory openjdk-11

使用 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);
}
Run Code Online (Sandbox Code Playgroud)

您可以在 IdeOne.com 上实时运行此代码

ern*_*t_k 19

简而言之,编译器在第一次调用时坚持使用您声明的类型,但在第二次调用时有一定的空间来推断兼容类型。

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

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

为什么第一个有效而第二个无效?这仅仅是因为编译器在作为第二个参数给出的表达式类型方面具有一定的灵活性。intSet1是一个独立的表达式,并且Set.of(123, 1234, 101)是一个 poly 表达式(请参阅JLS有关 poly expression 的描述)。在第二种情况下,上下文允许编译器计算适用于TString第一个参数的类型兼容的具体类型的类型。

  • 好吧,我不知道“poly expression”这个名字。但是这个链接不应该转到 [JLS](https://docs.oracle.com/javase/specs/jls/se16/html/jls-15.html#jls-15.12),因为它现在是权威来源(或者可能链接到两者)? (3认同)
  • @NishantLakhara 对于开发人员来说,最糟糕的是“复杂”和“难以理解”。但如果我们没有多表达式的好处,那么使用泛型进行编程就会非常麻烦(例如,lambda 表达式会立即变得很痛苦)。不过,当编译器计算出开发人员没有预料到或检测到的类型时,可能会出现意外,但我确信任何经过测试的代码都可以轻松处理这个方面。不过,我不认为这是不安全的。 (2认同)

Era*_*ran 9

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

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

<Serializable> boolean com.codebroker.dea.test.StringTest.isValid(Serializable value, Set<Serializable> range)
Run Code Online (Sandbox Code Playgroud)

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

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

isValid(strValue, intSet1);

另一方面,当您第一次分配Set.of(123,1234,101)给一个变量时,它被解析为 a Set<Integer>。在这种情况下, aString和 aSet<Integer>不能传递给您的isValid()方法。

如果你改变

var intSet1 = Set.of(123, 1234, 101);
Run Code Online (Sandbox Code Playgroud)

Set<Serializable> intSet1 = Set.of(123,1234,101);
Run Code Online (Sandbox Code Playgroud)

然后

isValid(strValue, intSet1);
Run Code Online (Sandbox Code Playgroud)

也会通过编译。


Eug*_*ene 5

当您(作为人类)查看编译的第二个时 isValid,可能会想 - 这怎么可能?T编译器将类型推断 Stringor Integer,因此调用必须绝对失败。

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

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

 javac --debug=verboseResolution=all YourClass.java
Run Code Online (Sandbox Code Playgroud)

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

  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
Run Code Online (Sandbox Code Playgroud)

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