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
,即 X
是 B1
的子类型。
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&Integer
、Number&Object
、Number&Runnable
等。这部分在JLS#4.9
中指定
String & Comparable<Double>
是不允许的,因为名义上的 class 会同时实现 Comparable<String>
和 Comparable<Double>
,这在 Java 中是非法的。
B1
和 B2
可以有多种形式,导致更复杂的情况。这是规范没有经过深思熟虑的地方。例如,从规范的文本中不清楚,如果其中一个是类型变量怎么办? 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 不允许;你运气不好:)你必须找到解决方法。
我对通配符边界的规则感到困惑。似乎有时声明一个绑定不满足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
,即 X
是 B1
的子类型。
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&Integer
、Number&Object
、Number&Runnable
等。这部分在JLS#4.9
String & Comparable<Double>
是不允许的,因为名义上的 class 会同时实现 Comparable<String>
和 Comparable<Double>
,这在 Java 中是非法的。
B1
和 B2
可以有多种形式,导致更复杂的情况。这是规范没有经过深思熟虑的地方。例如,从规范的文本中不清楚,如果其中一个是类型变量怎么办? 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 不允许;你运气不好:)你必须找到解决方法。