Java泛型定义和基本使用笔记

Posted xzj_2013

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java泛型定义和基本使用笔记相关的知识,希望对你有一定的参考价值。

1、 泛型的作用以及定义

1.概述

	在我的理解中,泛型主要面向程序封装和架构设计的使用,在面向对象编程及各种设计模式中有非常广泛的应用。
	为什么这么说呢,首先我们要理解什么是泛型,为什么要使用泛型?
	Java泛型是J2 SE1.5中引入的一个新特性,其本质是参数化类型,也就是说所操作的数据类型被指定为一个参数(type 
	parameter)这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。

	看到参数化类型,我们最先想到的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?
	从名字上理解,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可
	以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
	其作用就是在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型

 

2. 举例说明

举个最基础的例子

	List fxList = new ArrayList();
    fxList.add("abc");
    fxList.add(20);
    fxList.add(true);

    for(int i = 0; i< fxList.size();i++)
       String item = (String)fxList.get(i);
       Log.d("test","item = " + item);
    

毫无疑问,程序的运行结果会抛出异常java.lang.ClassCastException
ArrayList可以存放任意类型,例子中添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,因此程序崩溃了。
我们将第一行声明初始化list的代码更改一下,指定了添加的类型,编译器会在编译阶段就能够帮我们发现类似这样的问题。

 	List<String> fxList = new ArrayList<String>();
    arrayList.add(100); 
    在编译阶段,编译器就会报错

如果使用时都以Object使用,是不会出现崩溃,那既然可以使用Object,我们还需要泛型呢?
没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。
由此可见对比Object,泛型具备以下优点:

	1.消除强制类型转换。泛型可以消除源代码中的许多强制类型转换,这样可以使代码更加可读,并减少出错的机会,
	  会使得代码加清晰和筒洁;
	2.编译的时候检查类型安全。
	  如果使用Object类的话,你没法保证返回的类型一定是你需要的类型,也许是其它类型。这时你就会在运行时得到一个类型转换异常(ClassCastException); 
	  泛型的主要目标是提高Java程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在非常高的层
	  次上验证类型假设。没有泛型,这些都是假设,没有见过验证,可能会导致异常。
	  通过在变量声明中捕获这一附加的类型信息,泛型允许编译器实施这些附加的类型约束。
	  类型错误现在就可以在编译时被捕获了,而不是在运行时当作ClassCastException展示出来。
	  将类型检查从运行时挪到编译时有助于Java开发人员更早、更容易地找到错误,并可提高程序的可靠性		 
	除此以外还具备以下优点:
	3.更高的运行效率
	  在非泛型编程中,将筒单类型作为Object传递时会引起Boxing(装箱)和Unboxing(拆箱)操作,这两个过程
      都是具有很大开销的。引入泛型后,就不必进行Boxing和Unboxing操作了,所以运行效率相对较高,特别在对
      集合操作非常频繁的系统中,这个特点带来的性能提升更加明显。
	4.潜在的性能收益。
	  泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,Java系统开发人员会
	  指定这些强制类型转换)插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的JVM的优
	  化带来可能;

为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。

3. 特性描述

泛型技术在C#和Java之中的使用方式看似相同,但实现上却有着根本性的分歧,C#里面泛型无论在程序源码中、
编译后的IL中(Intermediate Language,中间语言,这时候泛型是一个占位符)或是运行期的CLR中都是切实存在的,
List<int>与List<String>就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现
称为类型膨胀,基于这种方法实现的泛型被称为真实泛型。

Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经被替换为原来的原始类型
(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此对于运行期的Java语言来说,
 ArrayList<int>与ArrayList<String>就是同一个类。所以说泛型技术实际上是Java语言的一颗语法糖,
 Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型被称为伪泛型。

Java的泛型是伪泛型。为什么说Java的泛型是伪泛型呢?因为,在编译期间,所有的泛型信息都会被擦除掉。也就是说泛型只在编译阶段有效;
那么类型擦出(type erasure)具体是什么呢?

  Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。
  使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。
  
  如在代码中定义的List<object>和List<String>等类型,在编译后都会编程List。JVM看到的只是List,
  而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,
  但是仍然无法避免在运行时刻出现类型转换异常的情况。
  类型擦除也是Java的泛型实现方法与C++模版机制实现方式之间的重要区别

关于java泛型的类型擦除,下面举例进行说明

public class GenTest 

    public static void main(String... args)
        ArrayList<String> stringArrayList = new ArrayList<String>();
        List<Integer> integerArrayList = new ArrayList<Integer>();

        stringArrayList.add("anbc");
        integerArrayList.add(123456);

        Class classStringArrayList = stringArrayList.getClass();
        Class  classIntegerArrayList = integerArrayList.getClass();

        System.out.println("classStringArrayList is "+classStringArrayList.getName());
        System.out.println("classIntegerArrayList is "+classIntegerArrayList.getName());
        System.out.println("compare result is "+ classStringArrayList.equals(classIntegerArrayList));


        try 
            integerArrayList.getClass().getMethod("add", Object.class).invoke(integerArrayList, "asd");
            for (int i=0;i<integerArrayList.size();i++) 
                System.out.println(integerArrayList.get(i));
            
         catch (IllegalAccessException e) 
            e.printStackTrace();
         catch (InvocationTargetException e) 
            e.printStackTrace();
         catch (NoSuchMethodException e) 
            e.printStackTrace();
        
    


运行结果是
classStringArrayList is java.util.ArrayList
classIntegerArrayList is java.util.ArrayList
compare result is true
123456
asd

在这个测试中中,我们定义了两个ArrayList数组,不过一个是ArrayList泛型类型,只能存储字符串。一个是ArrayList泛型类型,只能存储整形。最后,我们通过arrayList1对象和arrayList2对象的getClass方法获取它们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下了原始类型。
但是当我们利用反射调用add方法的时候,却可以存储字符串。这说明了Integer泛型实例在编译之后被擦除了,只保留了原始类型。

什么是什么是原始类型?
原始类型(raw type)就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。无论何时定义一个泛型类型,相应的原始类型都会被自动地提供。类型变量被擦除(crased),并使用其限定类型(无限定的变量用Object)替换。
下面举例说明:

public class Sum<T> //这个类是个泛型类
    private T name;
    public Sum(T name)
        this.name = name;
    
    public T getName()
        return name;
    

对应的原始类型

public class Sum 
    private Object name;
    public Sum(Object name)
        this.name = name;
    
    public Object getName()
        return name;
    

因为在Sum 中,T是一个无限定的类型变量,所以用Object替换。其结果就是一个普通的类,如同泛型加入java变成语言之前已经实现的那样。在程序中可以包含不同类型的Sum ,如Sum 或Sum ,但是,擦除类型后它们就成为原始的Pair类型了,原始类型都是Object。
从上面的那个例子中,我们也可以明白ArrayList被擦除类型后,原始类型也变成了Object,所以通过反射我们就可以存储字符串了

如果类型变量有限定,那么原始类型就用第一个边界的类型变量来替换。
比如public class Sum<T extends Comparable& Serializable>
那么原始类型就是Comparable
注意:
如果Sum 这样声明public class Sum <T extends Serializable&Comparable> ,那么原始类型就用Serializable替换,而编译器在必要的时要向Comparable插入强制类型转换。为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界限定列表的末尾。

关于原始类型和泛型类型:
在调用泛型方法的时候,可以指定泛型,也可以不指定泛型。
在不指定泛型的情况下,泛型变量的类型为 该方法中的几种类型的同一个父类的最小级,直到Object。
在指定泛型的时候,该方法中的几种类型必须是该泛型实例类型或者其子类。

public class GenTest 

    public static void main(String... args)

        /**不指定泛型的时候*/
        int i=GenTest.add(1, 2); //这两个参数都是Integer,所以T为Integer类型
        Number f=GenTest.add(1, 1.2);//这两个参数一个是Integer,一个是Float,所以取同一父类的最小级,为Number
        Object o=GenTest.add(1, "asd");//这两个参数一个是Integer,一个是String,所以取同一父类的最小级,为Object

        /**指定泛型的时候*/
        int a=GenTest.<Integer>add(1, 2);//指定了Integer,所以只能为Integer类型或者其子类
        int b=GenTest.<Integer>add(1, 2.2);//编译错误,指定了Integer,不能为Float
        Number c=GenTest.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float
    

    //这是一个简单的泛型方法
    public static <T> T add(T x,T y)
        return y;
    


4. 泛型的使用

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法

3.1. 泛型类和泛型接口

如果定义的一个类或接口有一个或多个类型变量,则可以使用泛型。泛型类型变量由尖括号界定,放在类或接口名的后面,下面定义尖括号中的T称为类型变量。意味着一个变量将被一个类型替代替代类型变量的值将被当作参数或返回类型。对于List接口来说,当一个实例被创建以后,T将被当作一个函数的参数

a.泛型类

泛型类型用于类的定义中,被称为泛型类。
一个泛型类(generic class)就是具有一个或多个类型变量的类
通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。
泛型类的最基本写法:

 public class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>
  private 泛型标识 /*(成员变量类型)*/ var; 
  .....

  

下面举例写一个普通的泛型类

//此处T可以随便写为任意标识,常见如T、E、K等形式的参数常用于表示泛型,但是实例化泛型类时,必须指定T的具体类型
public class Sum<T>/泛型类
        private T name;//name这个成员变量的类型为T,T的类型由外部指定  
	    public Sum(T name)  //泛型构造方法形参name的类型也为T,初始化时必须有外部指定具体类型
     	   this.name= name;
         
		public T getName() //泛型方法getName的返回值类型为T,T的类型由外部指定
        	return name;
	    
		……

    public static void main(String... args)
        //泛型的类型参数只能是包装类型,不能是简单类型
        //传入的实参类型需与泛型的类型参数类型相同,即为Integer.
        Sum<Integer> sumInteger = new Sum<Integer>(123456);

        //传入的实参类型需与泛型的类型参数类型相同,即为String.
        Sum<String> sumString = new Sum<String>("name_vlaue");
        //传入泛型实参,则会根据传入的泛型实参做相应的限制
        Sum sumF = new Sum(50.125);
        Sum sumB = new Sum(false);


        System.out.println("泛型测试 name is " + sumInteger.getName());
        System.out.println("泛型测试 name is " + sumString.getName());
        System.out.println("泛型测试 sumF name is " + sumF.getName());
        System.out.println("泛型测试 sumB name is " + sumB.getName());
    

运行结果:

泛型测试 name is 123456
泛型测试 name is name_vlaue
泛型测试 sumF name is 50.125
泛型测试 sumB name is false
b.泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中.
泛型接口的最基本写法:

public interface Gen<T> //泛型接口
    T call();

泛型接口的实现
未指定泛型实参类型时,实现为

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class AppleGen<T> implements Gen<T>
 * 如果不声明泛型,如:class AppleGen implements Gen<T>,编译器会报错:"Unknown class"
 */
public class AppleGen<T> implements Gen<T> 
    @Override
    public T call() 
        return null;
    

如果指定了泛型实参类型,实现为:

/**
 * 传入泛型实参时:
 * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Gen<T>
 * 但是我们可以为T传入无数个实参,形成无数种类型的Gen接口。
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
 * 即:Gen<T>,public T call();中的的T都要替换成传入的String类型。
 */
public class Orange implements Gen<String> 
    private String name = "orange";
    @Override
    public String call() 
        return name;
    

c.泛型方法

泛型类在多个方法签名间实施类型约束。在 List 中,类型参数 V 出现在 get()、add()、contains() 等方法的签名中。当创建一个 Map<K, V> 类型的变量时,您就在方法之间宣称一个类型约束。您传递给 add() 的值将与 get() 返回的值的类型相同。

类似地,之所以声明泛型方法,一般是因为您想要在该方法的多个参数之间宣称一个类型约束。

是否拥有泛型方法,与其所在的类是否泛型无关。
泛型类,是在实例化类的时候指明泛型的具体类型;
泛型方法,是在调用方法的时候指明泛型的具体类型 。
要定义泛型方法,只需将泛型参数列表置于返回值前。

/**
     * 泛型方法的基本介绍
     * @param t 传入的泛型实参
     * 说明:
     *     1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
     *     2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
     *     3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
     *     4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K等形式的参数常用于表示泛型
     */
    public <T> void GenMethod(T t)
        System.out.println("泛型方法 实参类型 is "+t.getClass().getName());
    
  • 泛型方法的基本用法
public class Sum<T> //这个类是个泛型类
    private T name;
    public Sum(T name)
        this.name = name;
    
    public T getName()
        return name;
    

    /**
     * 这个方法在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
     * 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
     public E setName(E name)
        this.name = name
     
     */


public class GenTest 
    /**
     * 这才是一个真正的泛型方法。
     * 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
     * 这个T可以出现在这个泛型方法的任意位置.
     * 泛型的数量也可以为任意多个
     *    如:public <T,K> K showGenName(Gen<T> gen)
     *        ...
     *        
     */
    public <T> T showGenName(Gen<T> gen)
        System.out.println("container key :" + gen.name());
        T name = gen.name();
        return name;
    
	 //这也不是一个泛型方法,这就是一个普通的方法,Gen<Number>这个泛型类做形参而已。
    public void showNameValue1(Gen<Number> obj)
        System.out.println("泛型测试 1 name value is " + obj.name());
    

    //这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
    //?是一种类型实参,可以看做为Number等所有类的父类
    public void showNameValue2(Gen<?> obj)
        System.out.println("泛型测试 2 name value is " + obj.name());
    

下面是一些错误的泛型方法使用

 public <T> T showNameValue(Gen<E> gen)
     ...
 
 这个方法编译器会为我们提示错误信息:"UnKnown class 'E' " 
 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
 public void showNameValue(T gen)
	....
 
 这个方法编译器会为我们提示错误信息:"UnKnown class 'T' "
 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
 所以这也不是一个正确的泛型方法声明。  
  • 类中的泛型方法
    泛型方法可以出现杂任何地方和任何场景中使用,但是在泛型类中的泛型方法是一种比较特殊的情况
    下面就举例说明下:
public class Sum<T> //这个类是个泛型类

    private T name;

    public Sum(T name)
        this.name = name;
    

    public T getName()
        return name;
    

    /**
     * 这个方法在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
     * 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
     public E setName(E name)
        this.name= name
     
     */

    //在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
    //由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
    public <E> void showName(E e)
        System.out.println(t.toString());
    

    //在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
    //实质上和showName(E t)是同样的方法,如果把名字修改为showName,则编译器会提示showName(T)' is already defined in ...
    //因为这里的T,E,在编译器看来是同一个东西,都是一个泛型的标识
    public <T> void showName2(T t)
        System.out.println(t.toString());
    

静态方法与泛型
静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。

2、通配符与泛型边界

为什么要用通配符呢?
在java中,数组是可以协变的,比如pig extends Animal,那么Animal[] 与pig []是兼容的。而集合是不能协变的,也就是说List不是List的父类,这时候就可以用到通配符了。
首先了解下什么是数组的协变,看下面的例子:
Number[] nums = new Integer[10]; // OK
因为Integer是Number的子类,一个Integer对象也是一个Number对象,所以一个Integer的数组也是一个Number的数组,这就是数组的协变。

Java把数组设计成协变的,在一定程度上是有缺陷的。因为尽管把Integer[]赋值给Number[],Integer[]可以向上转型为Number[],但是数据元素的实际类型是Integer,只能向数组中放入Integer或者Integer的子类。如果向数组中放入Number对象或者Number其他子类的对象,对于编译器来说也是可以通过编译的。但是运行时JVM能够知道数组元素的实际类型是Integer,当其它对象加入数组是就会抛出异常(java.lang.ArrayStoreException)。

泛型的设计目的之一就是保证了类型安全,让这种运行时期的错误在编译期就能发现,所以泛型是不支持协变的。例如:
List nums = new ArrayList(); // incompatible types
当确实需要建立这种向上转型的类型关系的时候,就需要用到泛型的通配符特性了。例如:
List<? extends Number> nums = new ArrayList(); // OK

通配符的分类

  • 无边界通配符(Unbounded Wildcards)
    无边界的通配符的主要作用就是让泛型能够接受未知类型的数据.
    语法:class-name<?> var-name
    示例:
public static void print(List<?> list) 
    for (Object obj : list) 
        System.out.println(o);
    

List<?> list和List list的区别:

  a.  List<?> list是表示持有某种特定类型对象的List,但是不知道是哪种类型;
  	  List list是表示持有Object类型对象的List。
  b.  List<?> list因为不知道持有的实际类型,所以不能add任何类型的对象,
      但是List list因为持有的是Object类型对象,所以可以add任何类型的对象。
  c.  注意:List<?> list可以add(null),因为null是任何引用数据类型都具有的元素

使用例子:

public class fxTest     
    public static void main(String... args)
        Sum<Integer> sumInteger = new Sum<Integer>(123456);
        Sum<String> sumString= new Sum<sumString>("abc");
        showNameValue(sumNumber);    
        showNameValue(sumString);  
    
    public static void showNameValue(Sum<?> obj)
        System.out.println("泛型测试 name is " + obj.getName());
    

  • 上边界限定的通配符(Upper Bounded Wildcards)
    使用固定上边界的通配符的泛型, 就能够接受指定类及其子类类型的数据. 要声明使用该类通配符, 采用<? extends E>的形式, 这里的E就是该泛型的上边界. 注意: 这里虽然用的是extends关键字, 却不仅限于继承了父类E的子类, 也可以代指显现了接口E的类.
    语法:class-name<? extends superclass> var-name
    示例:
public static double sum(List<? extends Number> list) 
    double s = 0.0;
    for (Number num : list) 
        s += num.doubleValue();
       
    return s;


List<? extends Number> list = new ArrayList<Integer>(); // OK
List<? extends Number> list = new ArrayList<Object>(); // error

特性:
a、List<? extends Number> list表示某种特定类型(Number或者Number的子类)对象的List。跟无边界通 配符一样,因为无法确定持有的实际类型,所以这个List也不能add除null外的任何类型的对象。

list.add(new Integer(1)); // error
list.add(null); // OK

b、从list中获取对象是是可以的(比如get(0)),因为在这个List中,不管实际类型是什么,但肯定都能转型为Number。

Number n = list.get(0); // OK
Integer i = list.get(0); // error

c、事实上,只要是形式参数有使用类型参数的方法,在使用无边界或者上边界限定的通配符的情况下,都不能调用。比如以java.util.ArrayList为例:

public E get(int index) // 可以调用
public int indexOf(Object o) // 可以调用
public boolean add(E e) // 不能调用
  • 下边界限定的通配符(Lower Bounded wildcards)
    使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据. 要声明使用该类通配符, 采用<? super E>的形式, 这里的E就是该泛型的下边界. 注意: 你可以为一个泛型指定上边界或下边界, 但是不能同时指定上下边界.
    语法:class-name<? super subclass> var-name
    例子:
public static void writeTo(List<? super Integer> list) 
    // ...

List<? super Number> list = new ArrayList<Number>(); // OK
List<? super Number> list = new ArrayList<Object>(); // OK
List<? super Number> list = new ArrayList<Integer>(); // error

特性:
a、List<? super Integer> list表示某种特定类型(Integer或者Integer的父类)对象的List。可以确定这个List持有的对象类型肯定是Integer或者其父类,所以往list里面add一个Integer或者其子类的对象是安全的,因为Integer或者其子类的对象都可以向上转型为Integer的父类对象。但是因为无法确定实际类型,所以往list里面add一个Integer的父类对象是不安全的。

list.add(new Integer(1)); // OK
list.add(new Object()); // error

b、当从List<? super Integer> list获取具体的数据的时候,JVM在编译的时候知道实际类型可以是任何Integer的父类,所以为了安全起见,要用一个最顶层的父类对象来指向取出的数据,这样就可以避免发生强制类型转换异常了。

Object obj = list.get(0); // OK
Integer i = list.get(0); // error
  • PECS原则(Producer Extends Consumer Super)
    从上面上边界限定的通配符和下边界限定的通配符的特性,可以知道:
    a、对于上边界限定的通配符,无法向其中加入任何对象,但是可以从中正常取出对象
    b、对于下边界限定的通配符,可以存入subclass对象或者subclass的子类对象,但是取出时只能用Object类型变量指向取出的对象。
    简而言之,上边界限定(extends)的通配符适合于内容的获取,而下边界限定(super)的通配符更适合于内容的存入。所以就有了一个PECS原则来很好的解释这两种通配符的使用原则。
    1、当一个数据结构作为producer对外提供数据的时候,应该只能取数据而不能存数据,所以适合使用上边界限定(extends)的通配符。
    2、当一个数据结构作为consumer获取并存入数据的时候,应该只能存数据而不能取数据,所以适合使用下边界限定(super)的通配符。
    3、如果既需要取数据也需要存数据,就不适合使用泛型的通配符。
public static <T> void copy(List<? super T> dest, List<? extends T> src) 
    for (int i = 0; i < src.size(); i++) 
        dest.set(i, src.get(i));
    

3、注意事项

使用ava泛型应该注意如下几点:
①在定义一个泛型类时,在“<>”之间定义形式类型参数,例如:“class TestGen<K,V>”,其中“K”,“V”不代表值,而是表示类型。
②实例化泛型对象时,一定要在类名后面指定类型参数的值(类型),一共要有两次书写。
③泛型中<K extends ObjecD,extends>并不代表继承,它是类型范围限制。
④使用泛型时,泛型类型必须为引用数据类型,不能为基本数据类型,Java中的普通方法,构造方法,静态方法中都可以使用泛型,方法使用泛型之前必须先对泛型进行声明,可以使用任意字母,一般都要大写。
⑤不可以用一个本地类型(如int float)来替换泛型。
⑥运行时类型检查,不同类型的泛型类是等价的(Pair与Pair是属于同一个类型Pair),这一点要特别注意,即如果obj instance of Pai == true的话,并不代表objget First()的返回值是一个String类型。
⑦泛型类不可以继承Exception类,即泛型类不可以作为异常被抛出。
⑧不可以定义泛型数组。
⑨不可以用泛型构造对象,即:first = new T();是错误的。
⑩在static方法中不可以使用泛型,泛型变量也不可以用static关键字来修饰
⑪不要在泛型类中定义equals(Tx)这类方法,因为Object类中也有equals方法,当泛型类被擦除后,这两个方法会冲突。
⑫根据同一个泛型类衍生出来的多个类之间没有任何关系,不可以互相赋值。
⑬若某个泛型类还有同名的非泛型类,不要混合使用,坚持使用泛型类。

推荐一些很有用的关于泛型的文章
https://www.jianshu.com/p/5972220efc9a

以上是关于Java泛型定义和基本使用笔记的主要内容,如果未能解决你的问题,请参考以下文章

聊一聊Kotlin的泛型

JAVA泛型实现原理

浅谈java泛型

Java泛型方法和构造函数

java ArrayList的基本使用

浅析Java中的泛型