Inferred wildcard generics in return type

2020-02-05 02:02发布

Java can often infer generics based on the arguments (and even on the return type, in contrast to e.g. C#).

Case in point: I've got a generic class Pair<T1, T2> which just stores a pair of values and can be used in the following way:

Pair<String, String> pair = Pair.of("Hello", "World");

The method of looks just like this:

public static <T1, T2> Pair<T1, T2> of(T1 first, T2 second) {
    return new Pair<T1, T2>(first, second);
}

Very nice. However, this no longer works for the following use-case, which requires wildcards:

Pair<Class<?>, String> pair = Pair.of((Class<?>) List.class, "hello");

(Notice the explicit cast to make List.class the correct type.)

The code fails with the following error (provided by Eclipse):

Type mismatch: cannot convert from TestClass.Pair<Class<capture#1-of ?>,String> to TestClass.Pair<Class<?>,String>

However, explicitly calling the constructor still works as expected:

Pair<Class<?>, String> pair =
    new Pair<Class<?>, String>((Class<?>) List.class, "hello");

Can someone explain this behaviour? Is it by design? Is it wanted? Am I doing something wrong or did I stumble upon a flaw in the design / bug in the compiler?

Wild guess: the “capture#1-of ?” somehow seems to imply that the wildcard is filled in by the compiler on the fly, making the type a Class<List>, and thus failing the conversion (from Pair<Class<?>, String> to Pair<Class<List>, String>). Is this right? Is there a way to work around this?


For completeness’ sake, here is a simplified version of the Pair class:

public final class Pair<T1, T2> {
    public final T1 first;
    public final T2 second;

    public Pair(T1 first, T2 second) {
        this.first = first;
        this.second = second;
    }

    public static <T1, T2> Pair<T1, T2> of(T1 first, T2 second) {
        return new Pair<T1, T2>(first, second);
    }
}

1条回答
Lonely孤独者°
2楼-- · 2020-02-05 02:45

The reason the constructor works is that you're explicitly specifying the type parameters. The static method also will work if you do that:

Pair<Class<?>, String> pair = Pair.<Class<?>, String>of(List.class, "hello");

Of course, the whole reason you have a static method in the first place is probably just to get the type inference (which doesn't work with constructors at all).

The problem here (as you suggested) is that the compiler is performing capture conversion. I believe this is as a result of [§15.12.2.6 of the JLS]:

  • The result type of the chosen method is determined as follows:
    • If the method being invoked is declared with a return type of void, then the result is void.
    • Otherwise, if unchecked conversion was necessary for the method to be applicable then the result type is the erasure (§4.6) of the method's declared return type.
    • Otherwise, if the method being invoked is generic, then for 1in, let Fi be the formal type parameters of the method, let Ai be the actual type arguments inferred for the method invocation, and let R be the declared return type of the method being invoked. The result type is obtained by applying capture conversion (§5.1.10) to R[F1 := A1, ..., Fn := An].
    • Otherwise, the result type is obtained by applying capture conversion (§5.1.10) to the type given in the method declaration.

If you really want the inference, one possible workaround is to do something like this:

Pair<? extends Class<?>, String> pair = Pair.of(List.class, "hello");

The variable pair will have a wider type, and it does mean a bit more typing in the variable's type name, but at least you don't need to cast in the method call anymore.

查看更多
登录 后发表回答