指定通配符上限时是否有明显区别?

22
假设我有一个通用的类 Generic<A extends BaseType>
就Java语言规范而言,下面这两个类型声明之间是否有显著差异?
Generic<?>
Generic<? extends BaseType>

嵌套通配符怎么办?

List<Generic<?>>
List<Generic<? extends BaseType>>

思考这一点,我认为这些应该是等价的。 Generic 指定类型参数 A 具有 BaseType 作为上限。

因此,无论我是否显式指定,通配符始终应该被 "自动" 或 "隐式" 地限制为 BaseType

下面,我尝试将我的直觉与 JLS 协调一致。


我找不到关于“implicit”边界的信息,所以我开始查看子类型规则。

阅读有关subtyping $4.10.2的JLS部分,它说:

给定一个泛型类型声明 C<F1,...,Fn>(n > 0),参数化类型 C<T1,...,Tn> 的直接超类型,其中 Ti(1 ≤ i ≤ n)是类型,如下所示:

  • D<U1 θ,...,Uk θ>,其中 D<U1,...,Uk> 是一个直接超类型为泛型类型 C<T1,...,Tn> 的泛型类型,θ 是替换 [F1:=T1,...,Fn:=Tn]。

  • C<S1,...,Sn>,其中 Si 包含 Ti(1 ≤ i ≤ n)(§4.5.1)。

(强调是我的)

据我所知,“通配符”在JLS中不被视为“类型”。因此,这不适用于前两个,但适用于两个 List 示例。

相反,应该适用于以下内容:

给定一个泛型类型声明 C(n > 0),其中至少有一个 Ri(1 ≤ i ≤ n)是通配符类型参数,那么参数化类型 C 的直接超类型是将捕获转换应用于 C 的结果参数化类型 C 的直接超类型(§5.1.10)。
(强调我的)
捕获转换 $5.1.10 应用于 Generic 和 Generic;我认为我可以得到新类型变量的相同边界。在捕获转换后,我可以使用“包含”规则来建立子类型关系。
对于第一个例子,通过
如果 Ti 是 ? 形式的通配符类型参数(§4.5.1),那么 Si 是一个新的类型变量,其上限是 Ui[A1:=S1,...,An:=Sn],下限是 null 类型(§4.1)。

由于A1BaseType,所以新变量的上限为BaseType

对于第二种情况,通过以下方式:

如果Ti是形式为? extends Bi的通配符类型参数,则Si是一个新的类型变量,其上限为glb(Bi, Ui[A1:=S1,...,An:=Sn]),下限为null类型。

glb(V1,...,Vm)定义为V1&...&Vm。

我得到了glb(BaseType, BaseType),再次是BaseType

因此根据JLS,Generic<?>Generic<? extends BaseType>之间的子类型关系是双向的,这与我的直觉相符。


对于嵌套通配符,我会使用"contains"规则

类型参数T1包含另一个类型参数T2,记为T2 <= T1,如果在以下规则的自反和传递闭包下,由T2表示的类型集合可被证明是由T1表示的类型集合的子集(其中<:表示子类型关系(§4.10)):

  • ? extends T <= ? extends S if T <: S

  • ? extends T <= ?

  • ? super T <= ? super S if S <: T

  • ? super T <= ?

  • ? super T <= ? extends Object

  • T <= T

  • T <= ? extends T

  • T <= ? super T

结合上面的

C<S1,...,Sn>,其中Si包含Ti(1≤i≤n)(§4.5.1)。

我得到:

List<Generic<?>>List<Generic<? extends BaseType>>的直接超类型,如果Generic<?>包含Generic<? extends BaseType>>

尽管如此,我不明白如何使用包含规则。根据规则,我可以使用的唯一附加信息是子类型。我已经知道两种类型之间的子类型关系是双向的。

然而,如果包含和子类型之间的关系是答案,我还可以展示List<String>List<Object>的子类型,但它实际上不是也不应该是。

此外,我需要展示形式为Type <= OtherType的内容,而唯一具有"类型"形式右侧的规则是T <= T,所以这些规则似乎完全没有帮助。

我如何通过JLS证明List<Generic<?>>List<Generic<? extends BaseType>>是彼此的子类型?


无法解释原因,但您可以将原始键入的Generic值添加到List<Generic<?>>中而不会出现问题,但是如果您尝试将其添加到List<Generic<? extends Blah>>中,其中Generic<T extends Blah>,编译器会输出警告(javac 1.8.0_112)。 - Valentin Ruano
1
我花了一些时间才意识到你实际上有两个问题。第一个问题已经基本上被自己回答了,在捕获转换之后,List<Generic<?> 变成了 List<Generic<#1 extends BaseType>>List<Generic<? extends BaseType>> 变成了 List<Generic<#2 extends BaseType>>,因此对于大多数操作而言,形式并不重要,但另一个问题是,哪些形式规则允许得出任何一个是另一个子类型的结论,这甚至是字面上将 List<Generic<?>> 分配给 List<Generic<?>> 时相同的问题。 - Holger
@Holger 是的,在某种程度上是这样。子类型化“问题”是我想回答我的“第一个”/原始问题的方式。但也许它也可以在没有子类型化和捕获转换等情况下回答。你写这个时是指使用我的第二个例子吗?根据我对JLS的解释,捕获转换根本不适用于这些嵌套通配符。 - phant0m
4
好的问题。我花了一些时间。即使搜索了那么长时间,仍然很难相信规范中会有遗漏,特别是对于一个似乎并不那么晦涩的结构。 - Holger
1个回答

9

如果你从字面上理解你的初始问题,“Generic<?>”和“Generic<? extends BaseType>”之间是否有显著差异,答案必须是它们不相等。

JLS §4.5.1清楚地说明:

通配符“? extends Object”等同于无界通配符“?”。

因此,只有在“BaseType”为“Object”时,才等同于“? extends BaseType”,但即使在那种情况下,它们仍然存在显着差异,例如在没有捕获转换发生的地方:

boolean b1 = new Object() instanceof Supplier<?>; // valid code
boolean b2 = new Object() instanceof Supplier<? extends Object>; // invalid

Supplier<?>[] array1; // valid declaration
Supplier<? extends Object>[] array1; // invalid

值得注意的是,与第一直觉相反,对于声明 Generic<T extends BaseType>,指定 Generic<? extends Object> 与等效的 Generic<?> 一样有效。通配符的边界只要不与类型参数的边界可证明不同,并且由于边界始终是 Object 的子类型,因此 ? extends Object 始终有效。

因此,如果我们有一个类型声明,如下所示:

interface NumberSupplier<N extends Number> extends Supplier<N> {}

我们可以编写。
NumberSupplier<? extends Object> s1;
NumberSupplier<? extends Serializable> s2;
NumberSupplier<? extends BigInteger> s3;

或者甚至

NumberSupplier<? extends CharSequence> s4;

我们甚至可以不使用实际的类型扩展NumberCharSequence,而是使用() -> null来实现它

但不能。

NumberSupplier<? extends String> s5;

StringNumber可以证明的不同

在分配方面,我们可以使用问题中已经引用的子类型规则来得出NumberSupplier<? extends BigInteger>NumberSupplier<? extends Object>的子类型,因为? extends BigInteger 包含 ? extends Object(还包括包含 ? extends Number),因为BigIntegerObjectNumber的子类型,但正如你正确指出的那样,这并不适用于其类型参数不是通配符的参数化类型。

所以,如果我们有像List<NumberSupplier<?>>List<NumberSupplier<? extends Object>>List<NumberSupplier<? extends Number>>这样的声明,并且想要推断它们是否是根据§4.5.1的包含规则的子类型之一,唯一可能适用的规则是当类型参数是相同类型(T <= T)时,但是,那么我们不需要子类型规则,因为那时,所有这些列表类型都是相同类型:

如果两个引用类型具有相同的二进制名称(§13.1),并且它们的类型参数(如果有)相同,则它们是相同的编译时类型,递归地应用此定义。

包含规则仍然有用,例如它允许得出结论:Map<String,? extends Number>Map<String,Integer> 的子类型,因为对于第一个类型参数,String <= String 适用,并且第二个类型参数的类型参数由特定的通配符 包含 规则覆盖。


所以剩下的问题是,哪个规则使我们得出结论NumberSupplier<?>NumberSupplier<? extends Object>NumberSupplier<? extends Number>是相同类型,以便List<NumberSupplier<?>>List<NumberSupplier<? extends Object>>List<NumberSupplier<? extends Number>>可以相互赋值。

它似乎不是捕获转换,因为捕获转换意味着计算有效边界,但也为每个通配符创建一个“新类型变量”,这些类型变量肯定是不同的类型。但没有其他涵盖通配符兼容性的规则。或者我没有找到。尝试将规范与javac的实际行为相匹配,产生了一些非常有趣的结果:

给定

interface MySupplier<S extends CharSequence&Appendable> extends Supplier<S> {}

以下声明显然是有效的:
List<MySupplier<? extends CharSequence>> list1 = Collections.emptyList();
List<MySupplier<? extends Appendable>>   list2 = Collections.emptyList();

由于在这两种情况下,通配符的边界与匹配一个 S 的边界重复,因此我们可能会猜测它们实际上是相同的类型。

但是 javac 认为它们不是同一类型。

list1 = list2; // compiler error
list2 = list1; // dito

尽管涉及捕获转换的任何操作都会得出兼容的类型,例如。
list1.set(0, list2.get(0)); // no problem
list2.set(0, list1.get(0)); // no problem

而间接地完成被拒绝的任务也是有效的:

List<MySupplier<?>> list3;
list3 = list1;
list2 = list3; // no problem
// or
list3 = list2;
list1 = list3; // again no problem

但是在这里,?并不等同于? extends Object:
List<MySupplier<? extends Object>> list4;
list4 = list1; // compiler error
list2 = list4; // dito
// or
list4 = list2; // dito
list1 = list4; // dito

但是,间接赋值也可以工作。

list4 = list3 = list1; // works
list1 = list3 = list4; // works
list4 = list3 = list2; // works
list2 = list3 = list4; // works

所以无论javac在这里使用什么规则,它都不是可传递的,这排除了子类型关系和一般的“它是相同类型”的规则。看起来,这确实是未(下)指定的,并直接影响实现。而且,目前实现的?没有边界是特殊的,允许一个赋值链,这是任何其他通配符类型都不可能的。

为什么list1 = list2会导致编译错误?根据https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.5.1如果满足以下条件之一,则两个类型参数显然不同:每个类型参数都是类型变量或通配符,其上限(必要时来自捕获转换)为S和T;且既不|S|<:|T|也不|T|<:|S|。以及:https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.6类型变量(§4.4)的擦除是其最左边界的擦除。 - Klaimmore
1
@Klaimmore 但是关于何时和何种情况下应该在赋值中应用“可证明不同”的规则并没有明确的说明。你知道,你可以MySupplier<? extends CharSequence>分配给MySupplier<? extends Appendable>,反之亦然,这并不违反你引用的规则,因为它涉及到“从捕获转换(如果必要)”的边界。没有迹象表明编译器已经使用了“可证明不同”的规则来进行列表赋值,特别是,因为这不能解释为什么List<MySupplier<? extends Object>>是不可分配的。 - Holger
"boolean b2 = new Object() instanceof Supplier<? extends Object>;"的意思是,无论有什么微妙的子类型分析,该语句都会失败,这是因为`instanceof`运算符要求*可反射类型*,而唯一可反射的通配符类型是`<?>`(语义等效性已得到保证)。" - scottb
1
@scottb 这是一个矛盾吗?规范说“? extends Object 等同于无界通配符 ?”,但实际上并不是这样。这就是整个问题的关键所在。如果真正完全等效,那么 Supplier<? extends Object> 就会被视为可重用类型。 - Holger

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