TypeToken嵌套泛型类型的意外通用行为

Got*_*nal 6 java generics

我有TypeToken类用来表示这样的泛型类型:
TypeToken<List<String>> listOfStrings = new TypeToken<List<String>> {}
这样工作正常,TypeToken只是class TypeToken<T> {}用简单的方法来获得该类型.

现在我想为常见类型创建简单的方法,例如List更动态的用法:
TypeToken<List<? extends Number>> numbers = list(extendsType(Number.class))
使用:

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

(返回nulls,因为我只询问编译器而不是逻辑)

但由于某种原因,这不符合我的预期:(因为我期望有效的代码不能编译,而我预期无效的代码会编译)

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?
    }
}
Run Code Online (Sandbox Code Playgroud)

我在这里做错了什么?是什么导致泛型行为像这样?

我正在使用JDK 11,但我也在JDK 8上测试过它

编译错误:

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
Run Code Online (Sandbox Code Playgroud)

Mar*_*o13 3

我认为这个问题的核心已经以类似的形式被问过好几次了。但我不确定,因为这是特别难以理解这个概念的星座之一。

也许人们可以这样想象:

  • 这里的方法extendsType返回一个TypeToken<? extends Number>
  • 在对该方法的调用中list?被捕获在类型参数 中T。这种捕获可以(粗略地说)被想象为一个新的类型变量 - 类似于X extends Number,并且该方法返回一个TypeToken<List<X>>

  • 现在,根据通常的约束,TypeToken<List<X>>不可分配给。TypeToken<List<? extends Number>>也许这张带有指示<===可分配性和<=/=指示不可分配性的表会有所帮助:

                        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>>
    
    Run Code Online (Sandbox Code Playgroud)

所以最后,Angelika Langer 著名的泛型常见问题解答中泛型类型的实例化中存在哪些超级子类型关系的问题的答案可能再次是最相关的:

泛型类型实例之间的超子类型关系由两个正交方面确定。

一方面,超类型和子类型之间存在继承关系。

...

另一方面,存在基于类型参数的关系。前提条件是所涉及的类型参数中至少有一个是通配符。例如,Collection<? extends Number>是 的超类型Collection<Long>,因为该类型Long是通配符" ? extends Number "表示的类型系列的成员。


我认为捕获是“新类型X”的想法听起来很有说服力,尽管涉及到一些挥手:这在解析过程中有点“隐式”发生,并且在代码中不可见,但我发现它很有帮助,在某种程度上,当我为类型编写一个库时,所有这些问题都出现了......


更新以进一步详细说明这一点,参考评论:

我提到过“按照通常的约束”,类型是不可分配的。现在人们可以争论这些“约束”从何而来。但它们基本上总是有相同的原因:类型是不可分配的,因为如果它们是可分配的,则程序将不是类型安全的。(这意味着有可能以ClassCastException一种或另一种方式激怒)。

解释类型安全在哪里丢失(以及为什么ClassCastException可能导致 a)涉及一些扭曲。我将尝试根据评论中提到的示例在这里展示它。向下滚动到tl;dr ,查看具有相同结构但简单得多的示例。

评论中的例子是这样的:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

此处导致编译错误的行之所以这样做,是因为如果可以分配这些类型,则可以执行以下操作:

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>());
    }
}
Run Code Online (Sandbox Code Playgroud)

TypeToken<List<? extends Number>>因此,a不可从 a 赋值这一事实TypeToken<? extends List<? extends Number>>确实只能起到防止 a 的目的ClassCastException

长话短说:博士

更简单的变体是这样的:

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

    }
}
Run Code Online (Sandbox Code Playgroud)

相反,当类型声明为 时List<? extends Number>,则可以进行赋值,因为不可能将无效类型潜入这样的列表中:

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

长话短说:这都是关于类型安全的。