Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic parameter: only diamond operator seems to work

Background: the question came up in this answer (the first revision of the answer, to be exact). The code presented in this question is reduced to the bare minimum to explain the problem.

Suppose we have the following code:

public class Sample<T extends Sample<T>> {

    public static Sample<? extends Sample<?>> get() {
        return new Sample<>();
    }

    public static void main(String... args) {
        Sample<? extends Sample<?>> sample = Sample.get();
    }
}

It compiles without warning and executes fine. However, if one tries to somehow define the inferred type of return new Sample<>(); in get() explicitly the compiler complains.

Up until now, I was under the impression that the diamond operator is just some syntactic sugar to not write explicit types and thus could always be replaced with some explicit type. For the given example, I was not able to define any explicit type for the return value to make the code compile. Is it possible to explicitly define the generic type of the return value or is the diamond-operator needed in this case?

Below are some attempts I made to explicitly define the generic type of the returned value with the corresponding compiler errors.


return new Sample<Sample> results in:

Sample.java:6: error: type argument Sample is not within bounds of type-variable T
            return new Sample<Sample>();
                              ^
  where T is a type-variable:
    T extends Sample<T> declared in class Sample
Sample.java:6: error: incompatible types: Sample<Sample> cannot be converted to Sample<? extends Sample<?>>
            return new Sample<Sample>();
                   ^

return new Sample<Sample<?>> results in:

Sample.java:6: error: type argument Sample<?> is not within bounds of type-variable T
            return new Sample<Sample<?>>();
                                    ^
  where T is a type-variable:
    T extends Sample<T> declared in class Sample

return new Sample<Sample<>>(); results in:

Sample.java:6: error: illegal start of type
           return new Sample<Sample<>>();
                                    ^
like image 659
Turing85 Avatar asked Jun 19 '18 20:06

Turing85


2 Answers

The JLS simply says:

If the type argument list to the class is empty — the diamond form <> — the type arguments of the class are inferred.

So, is there some inferred X that will satisfy the solution? Yes.

Of course, for you to explicitly define such an X, you'd have to declare it:

public static <X extends Sample<X>> Sample<? extends Sample<?>> get() {
    return new Sample<X>();
}

The explicit Sample<X> is compatible with the return type Sample<? extends Sample<?>>, so compiler is happy.

The fact that return type is a messed up Sample<? extends Sample<?>> is an entirely different story.

like image 149
Andreas Avatar answered Oct 13 '22 01:10

Andreas


Instantiating Generics with Wildcards

There's a couple problems here, but before delving into them, let me address your actual question:

Is it possible to explicitly define the generic type of the return value or is the diamond-operator needed in this case?

It is not possible to explicitly instantiate a Sample<? extends Sample<?>> (or a Sample<?> for that matter). Wildcards may not be used as type arguments when instantiating a generic type, though they may be nested within type arguments. For example, while it is legal to instantiate an ArrayList<Sample<?>>, you cannot instantiate an ArrayList<?>.

The most obvious workaround would be to simply return some other concrete type that is assignable to Sample<?>. For example:

class Sample<T extends Sample<T>> {
    static class X extends Sample<X> {}

    public static Sample<? extends Sample<?>> get() {
        return new X();
    }
}

However, if you specifically want to return a generic instantiation of the Sample<> class containing wildcards, then you must rely on generic inference to work out the type arguments for you. There are a few ways to go about this, but it usually involves one of the following:

  1. Using the diamond operator, as you are doing right now.
  2. Delegating to a generic method that captures your wildcard with a type variable.

While you cannot include a wildcard directly in a generic instantiation, it's perfectly legal to include a type variable, and that's what makes option (2) possible. All we have to do is ensure that the type variable in the delegate method gets bound to a wildcard at the call site. Every mention of the type variable the method's signature and body then gets replaced with a reference to that wildcard. For example:

public class Sample<T extends Sample<T>> {
    public static Sample<? extends Sample<?>> get() {
        final Sample<?> s = get0();
        return s;
    }

    private static <T extends Sample<T>> Sample<T> get0() {
        return new Sample<T>();
    }
}

Here, the return type of Sample<T> get0() gets expanded to Sample<WC#1 extends Sample<WC#1>>, where WC#1 represents a captured copy of the wildcard inferred from the assignment target in Sample<?> s = get0().

Multiple Wildcards in a Type Signature

Now, let's address that method signature of yours. It's hard to tell for sure based on what code you've provided, but I would guess that a return type of Sample<? extends Sample<?>> is *not* what you really want. When wildcards appear in a type, each wildcard is distinct from all others. There is no enforcement that the first wildcard and second wildcard refer to the same type.

Let's say get() returns a value of type X. If it was your intention to ensure that X extends Sample<X>, then you have failed. Consider:

class Sample<T extends Sample<T>> {
    static class X extends Sample<X> {}
    static class Y extends Sample<X> {}

    public static Sample<? extends Sample<?>> get() {
        return new Y();
    }

    public static void main(String... args) {
        Sample<?> s = Sample.get(); // legal (!)
    }
}

In main, variable s holds a value that is a Sample<X> and a Y, but not a Sample<Y>. Is that what you'd intended? If not, I suggest replacing the wildcard in your method signature with a type variable, then letting the caller decide the type argument:

class Sample<T extends Sample<T>> {
    static class X extends Sample<X> {}
    static class Y extends Sample<X> {}

    public static <T extends Sample<T>> Sample<T> get() { /* ... */ }

    public static void main(String... args) {
        Sample<X> x = Sample.get();     // legal
        Sample<Y> y = Sample.get();     // NOT legal

        Sample<?> ww = Sample.get();    // legal
        Sample<?> wx = Sample.<X>get(); // legal
        Sample<?> wy = Sample.<Y>get(); // NOT legal
    }
}

The version above effectively guarantees that, for some return value of type A, the returned value extends Sample<A>. In theory, it even works when T is bound to a wildcard. Why? It goes back to wildcard capture:

In your original get method, the two wildcards could end up referring to different types. In effect, your return type was Sample<WC#1 extends Sample<WC#2>, where WC#1 and WC#2 are separate wildcards that are not related in any way. But in the example above, binding T to a wildcard captures it, allowing the same wildcard to appear in more than one spot. Thus, when T is bound to a wildcard WC#1, the return type expands to Sample<WC#1 extends Sample<WC#1>. Remember, there is no way to express that type directly in Java: it can only be done by relying on type inference.

Now, I said this works with wildcards in theory. In practice, you probably won't be able to implement get in such a way that the generic constraints are runtime-enforceable. That's because of type erasure: the compiler can emit a classcast instruction to verify that the returned value is, for example, both an X and a Sample, but it cannot verify that it's actually a Sample<X>, because all generic forms of Sample have the same runtime type. For concrete type arguments, the compiler can usually prevent suspect code from compiling, but when you throw wildcards into the mix, complex generic constraints become difficult or impossible to enforce. Buyer beware :).


Aside

If all this is confusing to you, don't fret: wildcards and wildcard capture are among the most difficult aspects of Java generics to understand. It's also not clear whether understanding these will actually help you with your immediate goal. If you have an API in mind, it might be best to submit it to the Code Review stack exchange and see what kind of feedback you get.

like image 26
Mike Strobel Avatar answered Oct 12 '22 23:10

Mike Strobel