I am confused about the rules around wildcard bounds. It seems that sometimes it is OK to declare a method parameter whose bound does not satisfy the bound declared by the class. In the below code, method foo(...) compiles fine but bar(...) does not. I don't understand why either one is allowed.
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) {
}
}
Let's first consider method void foo(A<? extends Comparable<?>> a)
. A<? extends Comparable<?>>
is "compatible" with A<T extends String>
because there exists a wildcard type P
, and a comparable wildcard type Q
to satisfy the following:
P <: Comparable<Q> && P <: String
Since String <: Comparable<String>
, Q
must be String
, and P
may be any sub-type of String
(which since String is declared final
, your options are limited)
Now let's consider method void bar(A<? extends Comparable<Double>> a)
. There is no wildcard type P
that can satisfy
P <: Comparable<Double> && P <: String
because String has already implemented Comparable<String>
which is not a Comparable<Double>
, and it's impossible for any subclass of String
to implement Comparable<Double>
.
Just because you've written a signature A<? extends Comparable<?>> a
doesn't mean that you can pass the method any A<? extends Comparable<?>>
. You could change the declaration to accept any A<? extends Object>
and it will compile too, but you can only instantiate A<T extends String>
, so it isn't a loophole around having to use String or its subclasses.
Interestingly enough my Eclipse IDE doesn't even find the compile error in bar as declared above, but it does if bar accepts A<? extends Integer>
for example.
See this part of the Java specification for a complete understanding.
Two type arguments are provably distinct if one of the following is true:
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
This is a question of when parameterized types are "well-formed", i.e. what type arguments are allowed. The JLS isn't very well written on this topic, and compilers are doing things out of spec. The following is my understanding. (per JLS8, oracle javac 8)
In general we talk about generic class/interface declaration G<T extends B1>
; as an example
class Foo<T extends Number> { .. }
A generic declaration can be seen as a declaration of a set of concrete types; e.g. Foo
declares types Foo<Number>, Foo<Integer>, Foo<Float>, ...
A concrete type G<X>
(where X is a type) is well-formed iff X<:B1
, i.e. X
is a subtype of B1
.
Foo<Integer>
is well-formed because Integer<:Number
. Foo<String>
is not well-formed; it doesn't exist in the type system.This constraint is enforced rigorously, for example, this won't compile
<T> void m1(Foo<T> foo) // Error, it's not the case that T<:Number
Given a type G<? super B2>
, we would expect that B2<:B1
. This is because we most often need to apply capture conversion on it, resulting in G<X> where B2<:X<:B1
, implying B2<:B1
. If B2<:B1
is false, we'll introduce contradiction in the type system, leading to bizzare behaviors.
In fact, Foo<? super String>
is rejected by javac, which is nice, because the type is apparently a programmer's error.
Interestingly, we cannot find this constraint in JLS; or at least, it is not clearly stated in JLS. And experiments show that javac
does not always enforce this constraint, for example
<T> Foo<? super T> m2() // compiles, even though T<:Number is false
<String>m2(); // compiles! returns Foo<? super String> !
It's unclear why they are allowed. I'm not aware of any problem this would cause in practice though.
Given G<? extends B2>
, capture conversion yields G<X> where X<:B1&B2
.
The question is when the intersection type B1&B2
is well-formed. The most liberal approach would be to allow any intersection; even if the intersection is empty, i.e. B1&B2
is equivalent to the null type, it won't cause problems in the type system.
But practically, we would want the compiler to reject things like Number&String
, introduced by Foo<? extends String>
, since in all likelihood it must be a programmer's error.
A more specific reason is that javac
needs to construct a "notional class" that is a subtype of B1&B2
so that javac can reason about what methods can be called on the type. For that purpose, Number&String
cannot be allowed, while Number&Integer
, Number&Object
, Number&Runnable
etc are allowed. This part is specified in JLS#4.9
String & Comparable<Double>
cannot be allowed, because the notional class would be implementing both Comparable<String>
and Comparable<Double>
, which is illegal in Java.
B1
and B2
can be in many forms, leading to more complicated cases. This is where the spec isn't very well thought out. For example, it's unclear, from the text of the spec, what if one of them is a type variable; the behavior of javac
does seem reasonable to us
<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
Another example, should Number & Callable<?>
be allowed? And if it is, what should be the notional class's super interfaces? Remember that Callable<?>
cannot be a super interface
class Bar extends Number implements Callable<?> // illegal
In an even more complicated case we have something like Foo<Number> & Foo<CAP#1>
where CAP#1
is a type variable introduced by capture conversion. The spec clearly forbids it, yet the use case indicates that it should be legitimate.
javac
handles these cases more liberally than JLS. See responses from Maurizio and Dan
So, what do we do about it as a programmer? - Follow your intuition and construct types that make sense to you. Most likely javac
would accept it. If not, it's probably a mistake on your part. In rare cases, the type makes sense, yet the spec/javac doesn't allow it; you are out of luck:) and you'll have to find workarounds.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With