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

18

我无法理解在使用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);
}

你可以在 IdeOne.com 上运行这段代码

@Eran 感谢您的纠正,我没有看到那行的注释。为了方便其他人,我把我的IdeOne.com链接移到了问题中。 - Basil Bourque
3个回答

20
简单来说,编译器在第一次调用时被限制为您声明的类型,但在第二次调用时有一定的灵活性来推断兼容的类型。
使用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关于多态表达式的描述)。在第二种情况下,上下文允许编译器计算出一个适用于与第一个参数类型兼容的具体T

3
很好,我不知道“多态表达式”的名称。但是这个链接应该指向JLS,因为那是现在的权威来源(或者链接到两者都可以吗)? - Joachim Sauer
谢谢,@JoachimSauer。我也会添加JLS链接的。我刚想起来那个jsr链接,那是我第一次了解它的地方。 - ernest_k
这种类型的泛型不是有点奇怪并可能产生漏洞吗?虽然需要进行测试,但仍需小心。 - Nishant Lakhara
2
@NishantLakhara 最糟糕的情况是对开发人员来说复杂难以理解。但是,如果没有多态表达式的好处,使用泛型编程将非常麻烦(例如,lambda表达式会立即变得痛苦)。当编译器计算出开发人员没有预期或检测到的类型时,可能会出现意外,但我相信任何经过测试的代码都可以轻松处理这个方面。虽然我无法看到它不安全的地方。 - ernest_k
@JoachimSauer 多态表达式在泛型、Lambda等使用的地方随处可见。它们的类型取决于其使用的上下文。 - Eugene
1
@Eugene:谢谢,我很清楚它们是什么以及它们的作用,只是不知道那个概念的名称。 - Joachim Sauer

9

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

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

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

当编译器试图解析泛型类型参数的可能类型以用于isValid方法时,它需要找到String(strValue的类型)和Integer(Set.of(123, 1234, 101)元素类型)的共同超级类型,然后找到Serializable。
因此,Set.of(123,1234,101)被解析为Set而不是Set,这样编译器就可以将一个Serializable和一个Set传递给isValid(),从而是有效的。
另一方面,当你首次将Set.of(123,1234,101)赋给一个变量时,它会被解析为Set。在这种情况下,不能将一个String和一个Set传递给你的isValid()方法。
如果你更改
var intSet1 = Set.of(123, 1234, 101);

to

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

那么

isValid(strValue, intSet1);

也将通过编译。


1
可能Eclipse会为您简化此过程,但类型不会可序列化 - Eugene

6
当您(作为人类)查看编译后的第二个 isValid 时,可能会想 - 这怎么可能?编译器推断出类型 T 要么是 String,要么是 Integer,因此调用必定失败。
编译器在处理方法调用时的思考方式与我们截然不同。它会查看方法参数、提供的类型,并尝试为您推断一个完全不同和意外的类型。有时,这些类型是“非表示”类型,意味着编译器可以存在这种类型,但您作为用户不能声明此类类型。
有一个特殊的(未记录的)标志,您可以使用它来编译您的类,并深入了解编译器的“思考”方式:
 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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接