Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java: Covariant Wildcard Bounds in Method parameters

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) {

    }
}
like image 530
MattWallace Avatar asked Jul 31 '15 17:07

MattWallace


Video Answer


2 Answers

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:

  • Neither argument is a type variable or wildcard, and the two arguments are not the same type.
  • One type argument is a type variable or wildcard, with an upper bound (from capture conversion, if necessary) of S; and the other type argument T is not a type variable or wildcard; and neither |S| <: |T| nor |T| <: |S|.
  • Each type argument is a type variable or wildcard, with upper bounds (from capture conversion, if necessary) of S and T; and neither |S| <: |T| nor |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
like image 162
Samuel Avatar answered Oct 09 '22 13:10

Samuel


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>, ...

no wildcard

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

? super

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.

? extends

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.

like image 2
ZhongYu Avatar answered Oct 09 '22 14:10

ZhongYu