Why simple “capture of ?” does not compile even type-safety could be compile-time inferred?

406 Views Asked by At

I have a class with strict, simple generic type:

public class GenericTools<T> {

    private final Supplier<T> supplier;
    private final Consumer<T> consumer;

    public GenericTools(Supplier<T> supplier, Consumer<T> consumer) {
        this.supplier = supplier;
        this.consumer = consumer;
    }

    public Supplier<T> getSupplier() {
        return supplier;
    }

    public Consumer<T> getConsumer() {
        return consumer;
    }
}

What is the exact reason for the fact that "capture of ?" cannot be used here and file does not compile?

GenericTools<?> tools = new GenericTools<>(Math::random, System.out::println);
tools.getConsumer().accept(tools.getSupplier().get());

Error:(27, 59) java: incompatible types: java.lang.Object cannot be converted to capture#1 of ?

With explicit <Double> it compiles with no problems:

GenericTools<Double> tools = new GenericTools<>(Math::random, System.out::println);
tools.getConsumer().accept(tools.getSupplier().get());

I have used Java 1.8 to compile.

Please note that this is completely not a duplicate of Java generics “capture of ?”, where poster have no idea what we need to pass as "?"-typed argument when code requires it. In my case I am quite aware of capture mechanism, but stil type looking like supposed to work cannot be used. Most importantly, here I am asking about the exact reason (specification reference or something), not about what I should pass.

2

There are 2 best solutions below

0
On BEST ANSWER

It's because the type ? is invariant, and Object is not a subtype of all types within the bounds of ?.

I believe the Java 8 type inference is capable of inferring that T is Double on the RHS, but since you explicitly assign to a GenericTools<?> on the LHS, the capture is of an unbounded type variable, which unifies with the unbounded variable T which also has no bounds.

Without any bounds, the T in the signatures of Supplier::get and Consumer::accept are not guaranteed to be the same type -- remember, the type variable is invariant, as no co- or contra-variant bound is expressed. The erasure of the T on the Supplier side is just Object, and the compiler cannot insert a runtime check that the runtime type is actually ? (because ? is not reifiable!). Therefore: the type Object cannot be implicitly converted to ? and compilation fails.

0
On

As written in the Generics tutorial:

Collection<?> c = new ArrayList<String>();
c.add(new Object()); // Compile time error

Since we don't know what the element type of c stands for, we cannot add objects to it. The add() method takes arguments of type E, the element type of the collection. When the actual type parameter is ?, it stands for some unknown type. Any parameter we pass to add would have to be a subtype of this unknown type. Since we don't know what type that is, we cannot pass anything in. The sole exception is null, which is a member of every type.

On the other hand, given a List<?>, we can call get() and make use of the result. The result type is an unknown type, but we always know that it is an object. It is therefore safe to assign the result of get() to a variable of type Object or pass it as a parameter where the type Object is expected.

This is equivalent to

GenericTools<?> tools = ...
tools.getConsumer().accept(new Object()); //or tools.getSupplier().get() which returns Object

Once you declare your generic type this is what the compiler will know for the rest of the calls. getConsumer().accept() will have to accept ? (but it can't) and getSupplier().get() will have to return Object.

When declaring GenericTools<Double> you retain type information and both getConsumer().accept() and getSupplier().get() know that they are working with Double.