6

I have TypeToken class used to represent some generic type like this:
TypeToken<List<String>> listOfStrings = new TypeToken<List<String>> {}
And this works fine, TypeToken is just class TypeToken<T> {} with simple method to get that type.

Now I wanted to create simple methods for common type like List for more dynamic usage:
TypeToken<List<? extends Number>> numbers = list(extendsType(Number.class))
using:

public static <T> TypeToken<? extends T> extendsType(Class<T> type) {return null;}
public static <T> TypeToken<List<T>> list(TypeToken<T> type) {return null;}

(return nulls as I'm only asking about compiler not logic)

But for some reason this does not work how I would expect: (as code that I expected to be valid does not compile, and code that I expected to be invalid does compile)

class TypeToken<X> {
    static <T> TypeToken<? extends T> extendsType(Class<T> type) {return null;}
    static <T> TypeToken<List<T>> list(TypeToken<T> type) {return null;}
    static void wat() {
        TypeToken<List<? extends Number>> a = new TypeToken<List<? extends Number>>() {}; // valid
        TypeToken<List<? extends Number>> b = list(extendsType(Number.class)); // invalid, why?
        TypeToken<? extends List<? extends Number>> c = list(extendsType(Number.class)); // valid, why?
    }
}

What I'm doing wrong here? And what is causing generics to behave like this?

I'm using JDK 11, but I also tested this on JDK 8

Compiler error:

error: incompatible types: no instance(s) of type variable(s) T#1,CAP#1,T#2 exist so that TypeToken<List<T#1>> conforms to TypeToken<List<? extends Number>>
            TypeToken<List<? extends Number>> b = list(extendsType(Number.class)); // invalid, why?
                                                      ^
  where T#1,T#2 are type-variables:
    T#1 extends Object declared in method <T#1>list(TypeToken<T#1>)
    T#2 extends Object declared in method <T#2>extendsType(Class<T#2>)
  where CAP#1 is a fresh type-variable:
    CAP#1 extends T#2 from capture of ? extends T#2
GotoFinal
  • 3,585
  • 2
  • 18
  • 33
  • Can you please show the relevant class declarations enclosing your generic static methods? – ernest_k Oct 13 '18 at 16:09
  • @ernest_k this class does not matter, but changed example to make it simpler to copy-paste – GotoFinal Oct 13 '18 at 16:11
  • 2
    I think the simplest way to understand this is that since we can't provide wildcards as type arguments directly (i.e. `TypeToken.>list(...)` is an error), the compiler infers something else. I expected the compiler to do capture conversion in this case, but [the error message](https://ideone.com/uCPGm5) seems to cast some doubt on that theory, so I would have to review the JLS. Unfortunately, I really don't have time to look in to this today, so hopefully somebody else can. – Radiodef Oct 13 '18 at 16:29
  • 1
    In the error message, I expected it to say that `T#1 extends CAP#1`, but instead it says `T#1 extends Object`, which is why I doubt that capture conversion is the full explanation. I wrote an explanation of capture conversion [here](https://stackoverflow.com/a/46061947/2891664), though, since I think it's at least part of the answer. – Radiodef Oct 13 '18 at 16:38
  • You can make it working by modifying list definition: static TypeToken> list(TypeToken type) {return null;} –  Oct 13 '18 at 16:51
  • 1
    `b` would need to be `TypeToken>> b`, surely? I find that a good way to get the compiler to tell you what is going on is to extract an expression to a variable in an IDE. – Andy Turner Oct 13 '18 at 16:52
  • @EugenCovaci then `list(new TypeToken(){})` will not work and return `List extends String>` instead of `List` – GotoFinal Oct 13 '18 at 16:54
  • @AndyTurner Intellij will give you code that does not even compile for `b`, but it will also think it does compile. I already reported this as a bug to them. `TypeToken>` – GotoFinal Oct 13 '18 at 16:57
  • @GotoFinal Why do you think list(new TypeToken(){}) should work? –  Oct 13 '18 at 17:03
  • @EugenCovaci as `list` method is supposed to create a `List` of given generic type, any `T` type. Just as `TypeToken>() {}` – GotoFinal Oct 13 '18 at 17:11
  • @GotoFinal You know that TypeToken> and TypeToken> are not compatible? You cannot convert from one to another, that's why it doesn't compile –  Oct 13 '18 at 17:13
  • @EugenCovaci but provided type as `T` is `? extends Number` so `T` should == `? extends Number` here, otherwise I still can't create method that will by type-safe, as your method will result with runtime error if wrong type is provided (as type token will read invalid type and that invalid type will be later used somewhere) – GotoFinal Oct 13 '18 at 17:21
  • @Radiodef maybe you will get something from this: https://ideone.com/N51NvS as here error is IMHO more interesting, as it seems to see that Number type `CAP#1 extends Number from capture of ? extends Number` – GotoFinal Oct 13 '18 at 18:45
  • That is more like the error I would have expected. – Radiodef Oct 13 '18 at 19:03
  • @Radiodef I would still expect no error at all – GotoFinal Oct 13 '18 at 19:06

1 Answers1

3

I think that at the core this question that has already been asked in a similar form several times. But I'm not sure, because this is one of the constellations where it is particularly hard to wrap one's head around the concept.

Maybe one can imagine it like that:

  • The extendsType method here returns a TypeToken<? extends Number>
  • In the call to the list method, the ? is captured in the type parameter T. This capture can (roughly speaking) be imagined as a new type variable - something like X extends Number, and the method returns a TypeToken<List<X>>

  • Now the TypeToken<List<X>> is not assignable to TypeToken<List<? extends Number>>, as of the usual constraints. Maybe this table, with <=== indicating assignability and <=/= indicating non-assignability, will help:

                        Number                     <===                Integer
    TypeToken<          Number>                    <=/=      TypeToken<Integer>
    TypeToken<? extends Number>                    <===      TypeToken<Integer>
                        List<? extends Number>     <===                List<X>
    TypeToken<          List<? extends Number>>    <=/=      TypeToken<List<X>>
    TypeToken<? extends List<? extends Number>>    <===      TypeToken<List<X>>
    

So in the end, the answer to the question which super-subtype relationships exist among instantiations of generic types in the famous Generics FAQ by Angelika Langer is probably once more the most relevant here:

Super-subtype relationships among instantiations of generic types are determined by two orthogonal aspects.

On the one hand, there is the inheritance relationship between a supertype and a subtype.

...

On the other hand, there is a relationship based on the type arguments. The prerequisite is that at least one of the involved type arguments is a wildcard. For example, Collection<? extends Number> is a supertype of Collection<Long>, because the type Long is a member of the type family that the wildcard " ? extends Number " denotes.


I think that the idea of the capture being a "new type X" sounds convincing, even though there is some handwaving involved: This happens somewhat "implicitly" in the resolution process, and not visible in the code, but I found it helpful, to some extent, when I wrote a library for types where all these questions came up...


Updated to elaborate this further, referring to the comment:

I mentioned that the types are not assignable "as of the usual constraints". Now one could argue about where these "constraints" come from. But they basically always have the same reason: The types are not assignable, because if they were assignable, the program would not be type safe. (Meaning that it would be possible to provoke a ClassCastException one way or the other).

Explaining where the type safety is lost here (and why a ClassCastException could be caused) involves some contortions. I'll try to show it here, based on the example that was referred to in the comment. Scroll down to tl;dr for a an example that has the same structure, but is much simpler.

The example from the comment was this:

import java.util.*;
import java.io.*;

class Ideone
{
    public static class LinkTypeToken<T> extends TypeToken<List<T>> {}
    public static class TypeToken<T> {}
    public static void main (String[] args) throws java.lang.Exception  {            
        LinkTypeToken<Number> listA = null;
        TypeToken<List<Number>> listB = listA;

        LinkTypeToken<? extends Number> listC = null;
        TypeToken<? extends List<? extends Number>> listD = listC;

        // error: incompatible types: LinkTypeToken<CAP#1> cannot be 
        // converted to TypeToken<List<? extends Number>>
        // TypeToken<List<? extends Number>> listE = listC;
        //                                           ^
        // where CAP#1 is a fresh type-variable:
        //   CAP#1 extends Number from capture of ? extends Number        
        TypeToken<List<? extends Number>> listE = listC;
    }
}

The line that causes the compilation error here does so because if it was possible to assign these types, then one could do the following:

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

class IdeoneWhy
{
    public static class ArrayListTypeToken<T> extends TypeToken<ArrayList<T>>
    {
        ArrayList<T> element = null;
        void setElement(ArrayList<T> element)
        {
            this.element = element;
        }
    }

    public static abstract class TypeToken<T>
    {
        abstract void setElement(T element);
    }

    public static void main(String[] args)
    {
        ArrayListTypeToken<? extends Number> listC = new ArrayListTypeToken<Integer>();
        TypeToken<? extends List<? extends Number>> listD = listC;

        // This is not possible:
        //TypeToken<List<? extends Number>> listE = listC;

        // But let's enforce it with a brutal cast:
        TypeToken<List<? extends Number>> listE = 
            (TypeToken<List<? extends Number>>)(Object)listC;

        // This throws a ClassCastException
        listE.setElement(new LinkedList<Integer>());
    }
}

So the fact that a TypeToken<List<? extends Number>> is not assignable from a TypeToken<? extends List<? extends Number>> indeed only serves the purpose of preventing a ClassCastException.

tl;dr :

The simpler variant is this:

import java.util.ArrayList;
import java.util.List;

public class WhySimpler
{
    public static void main(String[] args)
    {
        List<Float> floats = new ArrayList<Float>();

        // This is not possible
        //List<Number> numbers = floats;

        // Let's enforce it with a brutal cast:
        List<Number> numbers = (List<Number>)(Object)floats;

        Integer integer = 123;

        // This is possible, because Integer is a Number: 
        numbers.add(integer);

        // Now, we ended up placing an Integer into a list that
        // may only contain Float values.
        // So this will cause a ClassCastException:
        Float f = floats.get(0);

    }
}

Conversely, when the type is declared as List<? extends Number>, then the assignment is possible, because it is not possible to sneak an invalid type into such a list:

List<Float> floats = new ArrayList<Float>();
List<? extends Number> numbers = floats;
numbers.add(someInteger); // This is not possible

Long story short: It's all about type safety.

Marco13
  • 53,703
  • 9
  • 80
  • 159
  • It still seems to be weird that one wildcard can affect other type, I think this is much more visible in this example: https://ideone.com/V4lniC adding one wildcard somehow forces you to use wildcards in other place, but I guess that just limitation of compiler and nothing can be done. Also, pretty nice library :D didn't see it before and I already made my own code for all this stuff in my project. But without TypeVariable support inside parser – GotoFinal Oct 14 '18 at 00:35
  • But I will accept this answer if nothing other will be posted soon. – GotoFinal Oct 14 '18 at 00:41
  • 1
    So, the fundamental difference between regular subtyping and subtyping in generics is that generics are not covariant. You ca not assign a `List` to the `List` even though `Integer` extends `Number`. To relax this constraint you need to use a wilrdcards like `List extends Number>`. – Oleksandr Pyrohov Oct 14 '18 at 12:24
  • 1
    @Oleksandr That's exactly the point. But admittedly, the covariance issue is hidden in this example, due to the *capture* that is created for the wildcard in the method call. – Marco13 Oct 14 '18 at 12:33
  • @GotoFinal Indeed, you should wait a while before accepting the answer. There are some people around here who are *deeply* involved in the nitty-gritty details of type inference in Java, and maybe that manage to better (and more profoundly) explain the role that the type capture (`X`) plays here. I updated the answer with some more details regarding your first comment, though. – Marco13 Oct 14 '18 at 12:53
  • I think I was you thinking too much about my exact case where I only want to represent type without using any generic types in it to represent actual data, so there would never be any method/field with generic type inside. As with list inside it does break and it is easily visible, and there is just no way to separate this as this is using same mechanics. Thanks. – GotoFinal Oct 14 '18 at 13:25