美文网首页
预热:泛型

预热:泛型

作者: 谷歌清洁工 | 来源:发表于2017-04-20 06:14 被阅读0次

    本文大量参考Thinking in java(解析,填充)。

    定义:多态算是一种泛化机制,解决了一部分可以应用于多种类型代码的束缚。虽然我们可以在参数定义一个基类或者是一个接口,但是他们的约束还是太强了,有的时候我们更希望编写更通用的代码,使代码能够应用于“某种不具体的类型”.而正是泛型的出现解决了这类问题,它实现了参数化类型的概念。其最初的目的是希望类或方法能够具备最广泛的表达能力(通过解耦类或方法与所使用的 类型之间的约束)。在你创建参数化类型的一个实例时,编译器会为你负责转型操作,并且保证类的正确性。

    泛型参数不能使用基本类型.

    泛型类:

    public class Holder<T> {
        private T a;
        public T getA() {
            return a;
        }
        
        public void setA(T a) {
            this.a = a;
        }
        
    public static void main(String[] args) {
        Holder<Object> aHolder=new Holder<>();
        aHolder.setA("asd");
        aHolder.setA(new Object());
        
    }
    }```
    
    创建Holder对象时可以指定泛型指向的对象,指明后就只能在Holder内部放入该类型(或其子类,多态与泛型不冲突),所以这里放入Object以及String都是可以的,Object是所有类的爹。
    
    ###泛型接口:同上
    
    ###泛型方法:
    
    ![泛型方法](https://img.haomeiwen.com/i3267534/beeaebe61351a122.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
     方法可以独立于类而变化。无论何时,只要你能够做到,你就应该尽量使用泛型方法。在使用泛型类时我们创建对象需要去指定类型参数的值,而使用泛型方法的时候通常不需要明确指明,如上图,编译器会自动找出相应的值(type argument inference)。同样也可以显示指明类型:
                `response.<String>f("123");`
        当然泛型方法与可变参数之间也是可以共存的:
    

    public class Holder1 {

    public static <T>ArrayList<T> test(T...a)
    {
    ArrayList<T> result=new ArrayList<>();
    for(T item:a)
    {
    result.add(item);
    }
    return result;
    }

    public static void main(String[] args) {
    System.out.println(test("123","234","345"));

    }
    }

    output:[123,234,345]
    
    ###擦除:
      尽管可以声明Arraylist.class但是无法声明ArrayList<Integer>.class.
    
        Class class1=new ArrayList<String>().getClass();
    Class class2=new ArrayList<Integer>().getClass();
    System.out.println(class1==class2);
    
    output:true
    
    通过这种方式可以看的更清楚:
    

    public class Holder1 {

    class A{};
    class B{};
    

    public static void main(String[] args) {
    List<A> list =new ArrayList<A>();
    HashMap<A, B> hashMap=new HashMap<>();
    System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
    System.out.println(Arrays.toString(hashMap.getClass().getTypeParameters()));
    }
    }

    output:
    [E]
    [K, V]
    Class.getTypeParameters()将返回一个TypeVariavle对象数组,表示有泛型声明所声明的类型参数。然而输出的只是标识符,所以可以得出以下结论:
     在泛型代码内部是无法获得任何有关泛型参数类型的信息。
           
    尽管你知道类型参数标识符和泛型类型边界这类的信息--你却无法知道用来创建某个特定实例的实际类型参数。
            这就是擦除带来的弊端。比如说:
    

    public class Holder1<T> {
    private T obj;
    public Holder1(T obj) {
    // TODO Auto-generated constructor stub
    this.obj=obj;
    }
    public void test()
    {
    //obj.test(); wrong
    }
    public T getObj()
    {
    return obj;
    }

    public static void main(String[] args) {
    Holder1<Test>holder1=new Holder1<Test>(new Test());
    holder1.test();
    }
    }
    class Test{
    public void test()
    {
    System.out.println("sb");
    }
    }

    由于擦除,这种调用在java中是无法实现的,而为了实现这种需求(obj需要调用f())就有了extends关键字,也就是泛型的边界
    extends:  
    ` public class Holder1<T extends Test>`
    <T extends Test>声明T必须具有类型Test或者从Test导出的类型.也就是当前定义的泛型必须是Test或其子类.
       
     为什么通过这样就能成功实现我们的需求?因为泛型类型参数在运行时是擦除到它的第一个边界,编译器实际上会把类型参数替换为它的最后擦除类,所以当前的T在擦除后实际上是Test,等于做了一个替换。
      
      那么这么做的意义在哪里,和我这样去写的差别在哪:
    

    public class Holder1 {
    private Test obj;
    public Holder1(Test obj) {
    // TODO Auto-generated constructor stub
    this.obj=obj;
    }
    public Test getObj() {
    return obj;
    }
    public void test()
    {
    obj.test();
    }
    }

    根本原因是通过泛化,能让当前代码跨越多个类工作,它不明确定义某个字类型,在使用时能返回确切的类型信息。比如:
    
    这里返回的就是我定义的Test1
    

    public class Holder1<T extends Test> {
    private T obj;
    public Holder1(T obj) {
    // TODO Auto-generated constructor stub
    this.obj=obj;
    }
    public T getObj() {
    return obj;
    }
    public void test()
    {
    obj.test();
    }

    public static void main(String[] args) {
    Holder1<Test1>holder1=new Holder1<Test1>(new Test1());
    holder1.getObj();//这里返回的就是我定义的Test1对象
    }
    }
    class Test{
    public void test()
    {
    System.out.println("sb");
    }
    }
    class Test1 extends Test
    {
    }

    为什么会有擦除,而不能像C++一样实现完整的泛化机制:
    >    这就是为了保证向前的兼容性,java早期并没有泛型的相关概念,并且能够减少JVM相关的改变,以及不破坏现有类库的前提下,以最小代价来实现相关概念。
          而这也使得泛型在java当中不是那么好用。所以在运行时期,所有泛型都将被擦除,替换成它们的非泛型上界,例如List<T>这种将被擦除为List,而普通的类型变量在没定义边界的情况下被擦除为Object.
    
    ####擦除的代价:
    不能用于显式地引用运行时类型的操作之中:转型(cast)、instanceof操作和new表达式。因为所有的参数类型信息在运行时期都会丢失,所以需要无时无刻提醒自己:参数的类型信息只是目前看起来拥有而已。最后只会留下它的上界。
    
    ###边界处的动作: 
    

    public class Holder1<T> {
    private Class<T> clazz;
    public Holder1(Class<T> class1) {
    // TODO Auto-generated constructor stub
    clazz=class1;
    }

    public T[] Test() {
      return (T[]) Array.newInstance(clazz, 2);//这里强转,并且有cast警告
    }
    

    public static void main(String[] args) {
    System.out.println(Arrays.toString(new Holder1<String>(String.class).Test()));

    }
    }

    output:[null, null]
    
      这里即使clazz被存储为Class<T>,但是由于擦除,实际上也只是Class,因此在运行时Array.newInstance内的clazz没有实际含义。接上文描述的:在泛型代码内部是无法获得任何有关泛型参数类型的信息。所以这里Array.newInstance实际上并未拥有clazz蕴含的类型信息(这里的T没有实际意义,不知道实际上是什么)。
         而另一个例子;     
    

    public class Holder1<T> {
    private List<T> list;
    public Holder1() {
    }
    public List<T> maker(T t,int n)
    {
    List<T>result=new ArrayList<>();
    for(int i=0;i<n;i++)
    {
    result.add(t);
    }
    return result;
    }

    public static void main(String[] args) {
    Holder1<String> sHolder1=new Holder1<>();
    System.out.println(sHolder1.maker("asd", 6));
    }
    }

    这个例子中,尽管在运行时会擦除所有T类型的相关信息,可是它仍旧可以确保在编译器你放置到Holder1当中的对象具有T类型,使其适合List<T>,确保了在方法或类中的类型内部一致性,这也可以认为是一种规法。不过还是那句话,内部并不知道T的实际含义.只能确保类型的统一.
        
    因为擦除在方法体中移除了类型信息,所以在运行时的问题就是边界:对象进入和离开方法的地点。
    
     看下面两个例子:
    1.不使用泛型
    

    public class Holder1 {
    private Object object;

    public Holder1() {  
    }
    public void setObject(Object object) {
        this.object = object;
    }
    public Object getObject() {
        return object;
    }   
    

    public static void main(String[] args) {
    Holder1 holder1=new Holder1();
    holder1.setObject("String");
    String string=(String) holder1.getObject();
    }
    }

     反编译后:  
    

    D:>javap -c Holder1
    Compiled from "Holder1.java"
    public class Holder1 {
    public Holder1();
    Code:
    0: aload_0
    1: invokespecial #1 // Method java/lang/Object."<init>":
    ()V
    4: return
    public void setObject(java.lang.Object);
    Code:
    0: aload_0
    1: aload_1
    2: putfield #2 // Field object:Ljava/lang/Object;
    5: return
    public java.lang.Object getObject();
    Code:
    0: aload_0
    1: getfield #2 // Field object:Ljava/lang/Object;
    4: areturn
    public static void main(java.lang.String[]);
    Code:
    0: new #3 // class Holder1
    3: dup
    4: invokespecial #4 // Method "<init>":()V
    7: astore_1
    8: aload_1
    9: ldc #5 // String String
    11: invokevirtual #6 // Method setObject:(Ljava/lang/Obje
    ct;)V
    14: aload_1
    15: invokevirtual #7 // Method getObject:()Ljava/lang/Obj
    ect;
    18: checkcast #8 // class java/lang/String
    21: astore_2
    22: return
    }

     前面的一大部分省略,set和get都是针对Object操作,观察第18行得知,get之后会有一个checkcast类型检查,翻阅
    Java Virtual Machine Online Instruction Reference:得知
    >  checkcast:ensure type of an object or array, checks that the top item on the operand stack (a reference to an object or array) can be cast to a given type.
    
     翻译:确保一个Object或者Array的类型。检查操作数栈的最上层item(object或array的引用)是否能被转换成相应的类型。
    
     checkcast 实际上可以被认为是:
    

    if (! (obj == null || obj instanceof <class>)) {
    throw new ClassCastException();
    }
    // if this point is reached, then object is either null, or an instance of
    // <class> or one of its superclasses.

    
    2.使用泛型:
    

    public class Holder1<T> {
    private T object;

    public Holder1() {  
    }
    public void setObject(T object) {
        this.object = object;
    }
    public T getObject() {
        return object;
    }   
    

    public static void main(String[] args) {
    Holder1<String> holder1=new Holder1<>();
    holder1.setObject("String");
    String string=holder1.getObject();
    }
    }

    反编译后:
    
    

    public static void main(java.lang.String[]);
    Code:
    0: new #3 // class Holder1
    3: dup
    4: invokespecial #4 // Method "<init>":()V
    7: astore_1
    8: aload_1
    9: ldc #5 // String String
    11: invokevirtual #6 // Method setObject:(Ljava/lang/Obje
    ct;)V
    14: aload_1
    15: invokevirtual #7 // Method getObject:()Ljava/lang/Obj
    ect;
    18: checkcast #8 // class java/lang/String
    21: astore_2
    22: return
    }

    11行setObject开始传入的就是Object,但是set()方法不需要类型检查,编译器已经检查过了,但是对get方法在18行checkcast还是进行了类型检查,只不过用了泛型以后由编译器自动插入,其实效果是一样的。
        
    在泛型中所有动作发生在边界处---对传进来的值做额外的编译期检查,并由编译器插入传出去的值的转型。这都是在编译期间完成的。
        
    ###泛型数组:
    

    class Gener<T>
    {
    public void gg()
    {
    System.out.println("ASD");
    }
    }
    public class Holder1 {
    static Gener<Integer>[] gia;

    public Holder1() {  
        
    }
    

    public static void main(String[] args) {
    // gia=(Gener<Integer>[]) new Object[10]; //编译器不会报错,但是运行会报错ClassCastException
    // gia[0].gg();
    gia=(Gener<Integer>[]) new Gener[10];
    gia[0]=new Gener<Integer>();
    gia[0].gg();
    }

    }

    那么既然数组无论它们持有的类型如何,都具有相同的结构,看起来是可以创建一个Object数组并将其转型为所希望的数组类型。事实上这样做会报错,为什么呢。
    
    
     通过编写下述代码,进行反编译 
    

    public class TT {
    public static void main(String[] args) {
    int[] aa=new int[10];
    String[]bb=new String[10];
    }
    }

    获得
    

    public class TT {
    public TT();
    Code:
    0: aload_0
    1: invokespecial #1 // Method java/lang/Object."<init>":
    ()V
    4: return
    public static void main(java.lang.String[]);
    Code:
    0: bipush 10 //push the int 10 onto the stack,将10放入堆栈中
    2: newarray int
    4: astore_1
    5: bipush 10
    7: anewarray #2 // class java/lang/String
    10: astore_2
    11: return
    }

    这里有个newarray和anewarray就是创建数组,同样翻阅
    Java Virtual Machine Online Instruction Reference:
    >newarray<type>:allocate new array for numbers or booleans.  
    newarray is used to allocate single-dimension arrays of booleans, chars, floats, doubles, bytes, shorts, ints or longs.
        newarray pops a positive int, n, off the stack, and constructs an array for holding n elements of the type given by <type>. Initially the elements in the array are set to zero. A reference to the new array object is left on the stack.
      
       翻译:newarray<type>给numbers或者booleans分配一个新的数组。它被用来分配booleans, chars, floats, doubles, bytes, shorts, ints or longs. 的一维数组。
            newarray从堆栈中弹出一个正整数n,然后构造一个type是你定义的类型的数组。初始化数组当中所有的元素(都设置默认为0)。在堆栈上留下对新数组对象的引用.  
    >anewarray<type>:allocate new array for objects.<type> is either the name of a class or interface, e.g. java/lang/String, or, to create the first dimension of a multidimensional array, <type> can be an array type descriptor, e.g. [Ljava/lang/String;  
            
            anewarray allocates a new array for holding object references. It pops an int, size, off the stack and constructs a new array capable of holding size object references of the type indicated by <type>.
           
            <type> indicates what types of object references are to be stored in the array (see aastore). It is the name of a class or an interface, or an array type descriptor. If it is java/lang/Object, for example, then any type of object reference can be stored in the array. <type> is resolved at runtime to a Java class, interface or array. See Chapter 7 for a discussion of how classes are resolved.
         
          A reference to the new array is pushed onto the stack. Entries in the new array are initially set to null.    
    
     翻译:给对象分配新的数组.<type>可以是class或者interface的名称,比如说java/lang/String,   或者为了创建多维数组的第一维,<type>可以是数组类型描述符,例如   [Ljava/lang/String;                              anewarray  分配一个新的数组去持有对象的引用。它从对战中弹出一个int类型的size(大小),并构造一个能够持有size个type对象引用的新数组。
         
      <type>表示object引用在数组当中是以什么类型被存储的。它是class或者interface的名称或者数组类型的描述。比如说type是 java/lang/Object,那么任意object引用都能被存储进当前数组中,而在运行时将<type>解析成java类,interface或者数组。
      
     一个新的数组引用将push到堆栈上,数组中所有条目都会被设置为null 
     
     从这里就可以看出,这里泛型数组的构建会调用anewarray,而anewarray需要明确的type,
    那么这样就可以知道:
     1.在gia=(Gener<Integer>[]) new Object[10];   中,即使gia看起来是转型为Gener<Integer>[],但是这也只是在编译期,运行时他仍然是Object[],正是因为anewarray运行时已经将type定义为Object,你无法对底层的数组进行更改。所以强制转型会引起ClassCastException.
    
       2.根据Oracle的java文档来看,泛型属于Non_Reifiable type,而引起这部分的原因也是因为类型擦除Non_Reifiable type会在编译期被移除泛型信息,所以在运行时无法获取具体的类型信息。而java明确规定数组内的元素必须是reifiable的,所以类似T[] a=new T[10]这类型的无法通过编译。
    
    参考例子:  
    

    String[] strArray = new String[20];
    Object[] objArray = strArray;
    objArray[0] = new Integer(1); // throws ArrayStoreException at runtime

    那么假如说泛型的数组可以直接创建:
    ArrayList<String>[] a=new ArrayList<String>[];
    那么随后也可以改为Object数组然后往里面放ArrayList<Integer>,我们在随后的代码中可以把它转型为Object[]然后往里面放Arraylist<Integer>实例。
    这样做不但编译器不能发现类型错误,就连运行时的数组存储检查对它也无能为力,它能看到的是我们往里面放Arraylist的对象,我们定义的<String>在这个时候已经被抹掉了.
    

    //下面的代码使用了泛型的数组,是无法通过编译的
    GenTest<String> genArr[] = new GenTest<String>[2];
    Object[] test = genArr;
    GenTest<StringBuffer> strBuf = new GenTest<StringBuffer>();
    strBuf.setValue(new StringBuffer());
    test[0] = strBuf;
    GenTest<String> ref = genArr[0]; //上面两行相当于使用数组移花接木,让Java编译器把GenTest<StringBuffer>当作了GenTest<String>
    String value = ref.getValue();// 这里是重点!

    最后一行中,根据之前讲到的泛型边界问题,取值的时候会是这样
    (String)ref.getValue();所以会有ClassCastException.这个程序虽然看起来是程序员的错误,
    而且也没有什么灾难性后果。但是从另一个角度看,泛型就是为了消灭ClassCastException出现的
    而这个时候他自己却引发这个错误,这就矛盾了。通常来说如果使用泛型,只要代码编译时没有警告,那么就不会出现错误ClassCaseException。
    
      究竟泛型数组应该怎么用,我们可以参考ArrayList的源码
    

    transient Object[] elementData; // non-private to simplify nested class access

    它内部使用的就是Object[]
    
    get方法对item使用了强转,才能让我们获取到正确的对象。
    

    // Positional Access Operations
    @SuppressWarnings("unchecked")
    E elementData(int index) {
    return (E) elementData[index];
    }
    /**
    * Returns the element at the specified position in this list.
    *
    * @param index index of the element to return
    * @return the element at the specified position in this list
    * @throws IndexOutOfBoundsException {@inheritDoc}
    */
    public E get(int index) {
    rangeCheck(index);
    return elementData(index);
    }

    add方法涉及到向上转型
    

    public boolean add(E e) {
    ensureCapacityInternal(size + 1); // Increments modCount!!
    elementData[size++] = e;
    return true;
    }

    事实证明,ArrayList大量使用了强转去实现。
    总的来说数组是协变的而泛型不是,如果 Integer扩展了 Number(事实也是如此),那么不仅 Integer是 Number,
    而且 Integer[]也是 Number[],在要求 Number[]的地方完全可以传递或者赋予 Integer[]。
    这也是矛盾发生的根本原因.
    
    ###边界:
    简单点就是 <T extends Base>这样在擦除的时候T就会转换成Base,而在泛型类或方法中可以直接视同Base
    的方法
    

    public class Response {
    public <T extends ABC>void f(T t)
    {
    t.test();

    }
    }
    class ABC{
    public void test()
    {
    System.out.println("asdasd");
    }
    }

    另一种用法就是
    

    interface has{
    void test();
    }
    public class Ges<T extends Object & has> {
    T mT;
    public T getmT() {
    return mT;
    }
    public void setmT(T mT) {
    this.mT = mT;
    }

    继承一个父类与多个接口的形式。和类继承的用法是一样的,不过class必须在第一个,接口跟在后面
    同样,如果希望将某个类型限制为特定类型或特定类型的超类型,请使用以下表示法:
    <T super LowerBoundType>
    
    ###通配符:
    泛型没有内建的协变类型,有时候想要在两个类型之间建立某种类型的向上转型关系:这正是通配符允许的
    

    public class Ges {
    public static void main(String[] args) {
    List<? extends fruit> list=new ArrayList<apple>();
    // list.add(new fruit());
    // list.add(new apple()); all wrong
    fruit fruit=list.get(0);//可以获取到fruit
    }
    }
    class fruit{

    }
    class apple extends fruit{

    }

    尽管list类型是List<? extends fruit>但是这并不实际意味着List将持有任何类型的fruit。实际上你不能往这个list当中安全的添加对象。尽管他可以合法指向一个list<apple>,
    你无法往里面丢任何对象。编译器只知道list内部的任何对象至少具有fruit类型,但是他具体是什么就不知道了。
    
    每当指定add方法的时候,![](https://img.haomeiwen.com/i3267534/3002ac82f9d98063.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)显示的都是null,由于不知道,所以干脆不接受任何类型的fruit。同样的编译器将直接拒绝对参数列表中
    涉及通配符的方法的调用。
    
    当然List<? extends fruit> list=new ArrayList<apple>();这种写法赋予了泛型一种协变性,
    像之前提到过的:List<fruit> list=new ArrayList<apple>()是无法通过的因为假如这么写,
    那就意味着可以list.add(new Banana());这就破坏了list定义时的承诺,它是一个苹果列表。
    
    那么针对这种情况(无法往内部添加)可以使用超类型通配符:<? super Class> ,有了超类型通配符,你就可以进行写入了:
    ![](https://img.haomeiwen.com/i3267534/dd793ecf68699d44.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
     这相当于定义了一个下界,无论你传入什么,最起码是apple,或者是他的子类,这样的类型传入是安全的.
    这种写法也可以称之为逆变,同样的get方法返回的是Object.
    ###关于协变和逆变:
        
        什么是协变(逆变):
    >如果A和B是类型(T),f表示类型转换,≤表示子类型关系,(例如A≤B,表示A是B的子类)那么:
    如果A≤B 则f(A) ≤ f(B) 那么 f是协变的 (假如说T和f(T)序关系一致,就是协变)
    如果A≤B 则f(B) ≤ f(A) 那么 f 是逆变的(假如说T和f(T)序关系相反,就是逆变)
    如果上面两种都不成立,那么f是无关的
    
    例如:class List<T>{...},可以理解为输入一个类型参数T,通过类型转换(f)成为,List<T>(f(T))
    所以当A=Object,B=String,那么f(A)=List<Object>,f(B)=List<String>,可是f(A)与f(B)不具备
    任何关系,所以单纯的泛型不具备协变性。
    
    所以ArrayList<? extends fruit>=ArrayList<fruit>,(? extends fruit)<=fruit
    ,f(? extends fruit)=f(fruit),通过这种写法具备了协变性。
    
    同样ArrayList<? super fruit>=ArrayList<fruit>,(? super fruit)>=fruit,
    f(? extends fruit)=f(fruit),这就是逆变性。
    
    #### 为什么要有协变逆变?优势在哪?
     而根据里氏替换原则:
     >派生类(子类)对象能够替换其基类(超类)对象被使用。当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
    当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
    
    从例子来看:
    

    public class f {
    // 定义三个类: Benz -> Car -> Vehicle,它们之间是顺次继承关系

    // 测试函数
    void test() {
        List<Vehicle> vehicles = new ArrayList<>();
        List<Benz> benzs = new ArrayList<>();
        Utils<Car> carUtils = new Utils<>();
        carUtils.put(vehicles, new Car());
        Car car = carUtils.get(benzs, 0);
        carUtils.copy(vehicles, benzs);
    }
    

    }
    class Vehicle {}
    class Car extends Vehicle {}
    class Benz extends Car {}
    // 定义一个util类,其中用到泛型里的协变和逆变
    class Utils<T> {
    T get(List<? extends T> list, int i) {
    return list.get(i);
    }

    void put(List<? super T> list, T item) {
        list.add(item);
    }
    
    void copy(List<? super T> to, List<? extends T> from) {
        for(T item : from) {
            to.add(item);
        }
    }
    

    }

    Car car = carUtils.get(benzs, 0);可以看出 List<Benz>对List<? extends Car>进行了替换(协变),
    
    List<Benz>的get方法返回Benz对象,而List<? extends Car>返回的是Car对象,这符合替换原则,方法的后置条件(即方法的返回值)要比父类更严格。
    
    carUtils.put(vehicles, new Car());List<Vehicle>对List<? super Car>进行替换(逆变),
    
    List<Vehicle>的put方法需要Vehicle对象作为形参,而List<? super Car>需要的是Car,这就满足替换原则
    的前置条件需求.
    
    最后一个copy体现的是协变与逆变的汇总,替换。所以总的来说泛型的协变与逆变定义上界与下界,同时也让程序
    能够在某种程度上满足替换原则,通过良好的替换让程序更具拓展性。这也是为什么大量的框架中
    例如rxjava,会在参数中大量使用这种形式的泛型写法。
    
    参考:
     Thinking in java
     http://ybin.cc/programming/java-variance-in-generics/
     https://www.zybuluo.com/zhanjindong/note/34147
     http://www.cnblogs.com/en-heng/p/5041124.html
     http://colobu.com/2015/05/19/Variance-lower-bounds-upper-bounds-in-Scala/#Java中的协变和逆变
     http://blog.csdn.net/hopeztm/article/details/8822545
     https://zh.wikipedia.org/wiki/%E9%87%8C%E6%B0%8F%E6%9B%BF%E6%8D%A2%E5%8E%9F%E5%88%99
     https://www.ibm.com/developerworks/cn/java/j-jtp01255.html
     https://www.zhihu.com/question/20928981
     http://cs.au.dk/~mis/dOvs/jvmspec/ref-Java.html
     http://www.blogjava.net/deepnighttwo/articles/298426.html

    相关文章

      网友评论

          本文标题:预热:泛型

          本文链接:https://www.haomeiwen.com/subject/uuskzttx.html