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

时间:2015-07-31 17:24:50

标签: java generics covariance

我对通配符边界的规则感到困惑。似乎有时可以声明一个方法参数,其边界不满足类声明的边界。在下面的代码中,方法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) {

    }
}

2 个答案:

答案 0 :(得分:6)

我们首先考虑方法@class MySwiftClassvoid foo(A<? extends Comparable<?>> a)A<? extends Comparable<?>>“兼容”,因为存在通配符类型A<T extends String>和可比较的通配符类型P以满足以下条件:

Q

由于P <: Comparable<Q> && P <: String String <: Comparable<String>必须为QString可能是P的任何子类型(因为声明了字符串{{1} },你的选择是有限的)

现在让我们考虑方法String。没有可以满足

的通配符类型final
void bar(A<? extends Comparable<Double>> a)

因为String已经实现了P而不是P <: Comparable<Double> && P <: String Comparable<String>的任何子类都无法实现Comparable<Double>

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

有趣的是,我的Eclipse IDE甚至没有在上面声明的bar中找到编译错误,但是如果bar接受A<? extends Object>就行了。

有关完整的理解,请参阅Java规范的this part

  

如果满足下列条件之一,则可以证明两个类型参数是不同的:

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

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

A<T extends String>

答案 1 :(得分:2)

这是一个参数化类型何时“格式良好”的问题,即允许哪种类型的参数。关于这个主题的JLS写得不是很好,而且编译器正在按规范做事。 以下是我的理解。 (根据JLS8,oracle javac 8)

一般来说,我们讨论泛型类/接口声明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类型,它也不会在类型系统中引起问题。

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

更具体的原因是javac需要构建一个“名义类”,它是B1&B2的子类型,因此javac可以推断出可以在类型上调用哪些方法。为此,Number&StringNumber&IntegerNumber&Object等都不允许Number&Runnable。此部分在JLS#4.9

中指定

String & Comparable<Double>是不允许的,因为名义类会同时实现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<?>吗?如果是,那么概念类的超级接口应该是什么?请记住,Callable<?>不能是超级界面

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

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

javac比JLS更自由地处理这些案件。请参阅MaurizioDan

的回复

? ? ?

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