Java:方法参数中的协变通配符边界

Java: Covariant Wildcard Bounds in Method parameters

我对通配符边界的规则感到困惑。似乎有时声明一个绑定不满足class声明的绑定的方法参数是可以的。在下面的代码中,方法 foo(...) 可以正常编译,但 bar(...) 不能。我不明白为什么允许其中一个。

public class TestSomething {
    private static class A<T extends String> {}

    public static void foo(A<? extends Comparable<?>> a) {

    }

    public static void bar(A<? extends Comparable<Double>> a) {

    }
}

让我们首先考虑方法void foo(A<? extends Comparable<?>> a)A<? extends Comparable<?>> 是 "compatible" 和 A<T extends String> 因为存在通配符类型 P 和可比较的通配符类型 Q 来满足以下条件:

P <: Comparable<Q> && P <: String

因为 String <: Comparable<String>Q 必须是 String,并且 P 可以是 String 的任何子类型(因为 String 被声明为 final,您的选择有限)

现在让我们考虑一下方法void bar(A<? extends Comparable<Double>> a)。没有可以满足

的通配符类型P
P <: Comparable<Double> && P <: String

因为 String 已经实现了 Comparable<String> 而不是 Comparable<Double>String 的任何子类都不可能实现 Comparable<Double>.

仅仅因为你写了一个签名 A<? extends Comparable<?>> a 并不意味着你可以通过任何方法 A<? extends Comparable<?>>。您可以更改声明以接受任何 A<? extends Object>,它也会编译,但您只能实例化 A<T extends String>,因此它不是必须使用 String 或其子类的漏洞。

有趣的是,我的 Eclipse IDE 甚至没有发现上面声明的 bar 中的编译错误,但是如果 bar 接受 A<? extends Integer> 则它会发现。

请参阅 Java 规范的 this part 以获得完整的理解。

Two type arguments are provably distinct if one of the following is true:

  • 两个参数都不是类型变量或通配符,并且两个参数的类型不同。
  • 一个类型参数是类型变量或通配符,上限(如果需要,来自捕获转换)为 S;并且另一个类型参数 T 不是类型变量或通配符;也不 |S| <:|T|也不|T| <: |S|.
  • 每个类型参数都是一个类型变量或通配符,具有 S 和 T 的上限(如果需要,来自捕获转换);也不 |S| <:|T|也不|T| <: |S|.

A type argument T1 is said to contain another type argument T2, written T2 <= T1, if the set of types denoted by T2 is provably a subset of the set of types denoted by T1 under the reflexive and transitive closure of the following rules (where <: denotes subtyping (§4.10)):

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

这是参数化类型何时为 "well-formed" 的问题,即允许使用什么类型的参数。 JLS 在这个主题上写得不是很好,编译器正在做一些超出规范的事情。 以下是我的理解。 (根据 JLS8、oracle javac 8)

一般我们讲泛型class/interface声明G<T extends B1>;举个例子

    class Foo<T extends Number> { .. }

泛型声明可以看作是一组具体类型的声明;例如Foo 声明类型 Foo<Number>, Foo<Integer>, Foo<Float>, ...

无通配符

具体类型 G<X>(其中 X 是一个类型)是良构的当且仅当 X<:B1,即 XB1 的子类型。

  • Foo<Integer> 是合式的,因为 Integer<:Number
  • Foo<String> 格式不正确;它在类型系统中不存在。

严格执行此约束,例如,这不会编译

    <T> void m1(Foo<T> foo)  // Error, it's not the case that T<:Number

?超级

给定类型 G<? super B2>,我们期望 B2<:B1。这是因为我们最常需要对其应用捕获转换,导致G<X> where B2<:X<:B1,暗示B2<:B1。如果 B2<:B1 为假,我们将在类型系统中引入矛盾,导致奇怪的行为。

事实上,Foo<? super String>被javac拒绝了,这很好,因为类型显然是程序员的错误。

有趣的是,我们在 JLS 中找不到这个约束;或者至少,它在 JLS 中没有明确说明。并且实验表明 javac 并不总是强制执行此约束,例如

    <T> Foo<? super T> m2()  // compiles, even though T<:Number is false

    <String>m2();  // compiles! returns Foo<? super String> !

尚不清楚为什么允许它们。不过,我不知道这在实践中会引起什么问题。

?延伸

给定 G<? extends B2>,捕获转化率 G<X> where X<:B1&B2

问题是交集类型 B1&B2 何时合式。最自由的方法是允许任何交集;即使交集为空,即 B1&B2 等同于 null 类型,也不会导致类型系统出现问题。

但实际上,我们希望编译器拒绝 Foo<? extends String> 引入的 Number&String 之类的东西,因为它很可能是程序员的错误。

一个更具体的原因是javac需要构造一个"notional class",它是B1&B2的子类型,这样javac就可以推断出可以在该类型上调用哪些方法。为此,不允许使用 Number&String,而允许使用 Number&IntegerNumber&ObjectNumber&Runnable 等。这部分在JLS#4.9

中指定

String & Comparable<Double> 是不允许的,因为名义上的 class 会同时实现 Comparable<String>Comparable<Double>,这在 Java 中是非法的。

B1B2 可以有多种形式,导致更复杂的情况。这是规范没有经过深思熟虑的地方。例如,从规范的文本中不清楚,如果其中一个是类型变量怎么办? javac 的行为对我们来说确实是合理的

    <T extends Runnable> Foo<? extends T> m3()  // error
    <T extends Object  > Foo<? extends T> m4()  // error
    <T extends Number  > Foo<? extends T> m5()  // ok
    <T extends Integer > Foo<? extends T> m6()  // ok

再比如,Number & Callable<?>应该允许吗?如果是,那么名义上的 class 的超级接口应该是什么?请记住 Callable<?> 不能是超级接口

    class Bar extends Number implements Callable<?> // illegal

在更复杂的 中,我们有类似 Foo<Number> & Foo<CAP#1> 的东西,其中 CAP#1 是捕获转换引入的类型变量。规范明确禁止它,但用例表明它应该是合法的。

javac 比 JLS 更自由地处理这些情况。查看 Maurizio and Dan

的回复

? ? ?

那么,作为程序员,我们该怎么办呢? - 跟随您的直觉并构建对您有意义的类型。很可能 javac 会接受它。如果不是,则可能是您的错误。在极少数情况下,类型有意义,但 spec/javac 不允许;你运气不好:)你必须找到解决方法。