1、泛型简介

1.1 什么是泛型

泛型是JDK 1.5引入的特性,泛型即参数化类型,把一个类或者一个方法的定义过程中需要的参数类型也作为一个参数传进去,这里的参数指的是引用类型的参数(int等基本类型的参数不能使用泛型)。通过泛型的使用,只要代码在编译阶段没有出现警告,那么运行时期就不会出现ClassCastException异常。
以下是关于泛型的一些术语澄清:

  • ArrayList:泛型;
  • ArrayList中的E:类型参数变量;
  • ArrayList中的Integer:实际类型参数。

    1.2 为什么需要泛型

    1.1小节中介绍泛型概念时的一句话:通过泛型的使用,只要代码在编译阶段没有出现警告,那么运行时期就不会出现ClassCastException异常。
    举个例子:

    1. public class Main {
    2. public static void main(String[] args) {
    3. List list = new ArrayList();
    4. list.add(111);
    5. list.add("111");
    6. for (int i = 0; i < list.size(); ++i) {
    7. String val = (String) list.get(i);
    8. System.out.println(val);
    9. }
    10. }
    11. }

    上面的demo在编译器编写代码时只会有提示但不会报错,但编译上述代码会报错:

    Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
      at generic.Main.main(Main.java:13)
    

    这是因为ArrayList类的定义使用到了泛型,试想一下如果ArrayList类没有使用泛型,则上述代码在编译阶段也不会报错,只有在运行期间才会报错(抛出运行时异常:ClassCastException),那这样我们编写的程序出bug的风险就很高。因此泛型的作用就是让有问题的代码(比如上面例子中的类型强制转换)在编译阶段就报错,将问题及时暴露出来。

    1.3 泛型的使用场景

    在平时开发中我们会写一些工具类,工具类里经常会使用到泛型,因为我们知道使用工具类方法时的传参类型不确定,可能是String类型的入参,也可能是int类型的入参。泛型和反射在底层框架(比如Spring、MyBatis源码)中使用的非常多,这里举几个例子:
    (1)jdk中的集合类:

    public class ArrayList<E> {
      ...
      public boolean add(E e) {}
      public E get(int index) {}
      ...
    }
    

    (2)Http请求体响应体:
    (3)fastjson里的静态方法parseObject:

    public static <T> T parseObject(String text, Class<T> clazz) {
      return parseObject(text, clazz, new Feature[0]);
    }
    

    2、泛型类

    泛型类就是把泛型定义在类上,是在实例化类的时候指明泛型的具体类型,1.3中介绍的集合类以及http请求体的类都是泛型类。在使用泛型类实例化对象时必须指定具体类型。
    举例:

    // 此处E可以随便写为任意标识,常见的如T、E、K、V等
    public class Generic<E> {
      // data属性的类型由外部指定的类型E确定
      private E data;
    
      Generic(E data) {
          this.data = data;
      }
    
      // 此处虽然在类中的方法使用到了泛型,但该方法并不是泛型方法
      public E get() {
          return this.data;
      }
    
      public void set(E data) {
          this.data = data;
      }
    }
    

    使用:

    public class Main {
      public static void main(String[] args) {
          Generic<String> generic1 = new Generic<>("111");
          generic1.get();
    
          Generic<Integer> generic2 = new Generic<>(111);
          generic2.set(222);
      }
    }
    

    注意:

  • 泛型类的类参数只能是引用类型(即类名),不能是基本类型;

  • 不能对泛型类的实例对象使用instanceof,比如generic1 instanceof Generic<String>

    3、泛型接口

    泛型接口与泛型类的使用基本相同,泛型接口是指泛型作用在接口上,jdk中的List接口和它的父接口Collection接口就是一个泛型接口: ```java public interface List extends Collection {…}

public interface Collection extends Iterable {…}

举例:<br />泛型接口:
```java
public interface Generator<E> {
    void print(E e);
}

实现泛型接口的泛型类:

public class FruitGenerator<E> implements Generator<E> {
    @Override
    public void print(E e) {
        System.out.println(e.toString());
    }
}

使用:

public class Main {
    public static void main(String[] args) {
        FruitGenerator<String> fruitGenerator1 = new FruitGenerator<>();
        fruitGenerator1.print("111");

        FruitGenerator<Integer> fruitGenerator2 = new FruitGenerator<>();
        fruitGenerator2.print(111);
    }
}

注意:

  • 实现泛型接口的类也是泛型类,即在声明类的时候,需将泛型的声明也一起加到类中,即:class FruitGenerator<T> implements Generator<T>{} ,如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:”Unknown class”。

    4、泛型方法

    有时我们只需要在类中的某一个方法中使用到泛型,此时将整个类声明成泛型就不合适了,只需要将该方法声明成泛型即可,该方法就称为泛型方法。
    注意:

  • 访问修饰符(比如public)与方法返回类型之间必须要有泛型声明(比如),可以说这个是判断方法是否是泛型方法的标志;

  • 类中使用到泛型的方法不一定是泛型方法,见第2节中的例子,判断一个方法是否是泛型方法的标志见上;
  • 泛型标志不限于,比如都可以。

    4.1 泛型方法的基本使用

    Tools:

    public class Tools {
      public static<T> T generateClass(Class<T> clazz) throws 
          IllegalAccessException, InstantiationException {
          T instance = clazz.newInstance();
          return instance;
      }
    }
    

    使用:

    public class Main {
      public static void main(String[] args) {
          try {
              Object obj = Tools.generateClass(String.class);
              System.out.println(obj.getClass());
          } catch (IllegalAccessException e) {
              e.printStackTrace();
          } catch (InstantiationException e) {
              e.printStackTrace();
          }
      }
    }
    

    4.2 类中的泛型方法

    这一节介绍以下在类中定义泛型方法需要注意的细节。首先举例:
    Fruit:

    public abstract class Fruit {
      abstract void print();
    }
    

    Apple:

    public class Apple extends Fruit {
      @Override
      void print() {
          System.out.println("apple");
      }
    }
    

    Dog:

    public class Dog {
      public void print() {
          System.out.println("dog");
      }
    }
    

    GenericMethod:

    public class GenericMethod<T> {
      public void showFirst(T t){
          System.out.println(t.toString());
      }
    
      /* 
       * 1、在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型
       *    可以类型与T相同,也可以不同。
       * 2、由于泛型方法在声明的时候会声明泛型<E>,因此即使在声明泛型类中并未声明泛型<E>,
       *    编译器也能够正确识别泛型方法中识别的泛型<E>。
       */
      public <E> void showSecond(E t){
          System.out.println(t.toString());
      }
    
      /* 
       * 在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,
       * 此处泛型方法的T与泛型类中声明的T是两个概念上的T,虽然名称上都是一样
       */
      public <T> void showThird(T t){
          System.out.println(t.toString());
      }
    }
    

    泛型类中定义泛型方法的细节见上面代码中的注释。
    Main:

    public class Main {
      public static void main(String[] args) {
          Apple apple = new Apple();
          Dog dog = new Dog();
          GenericMethod<Fruit> generateTest = new GenericMethod<Fruit>();
    
          // apple是Fruit的子类,所以这里可以
          generateTest.showFirst(apple);
    
          // 编译器会报错,因为泛型类型实参指定的是Dog
          // generateTest.showFirst(dog);
    
          //使用这两个方法都可以成功
          generateTest.showSecond(apple);
          generateTest.showSecond(dog);
    
          //使用这两个方法也都可以成功
          generateTest.showThird(apple);
          generateTest.showThird(dog);
      }
    }
    

    4.3 静态方法与泛型

    关于静态方法中的泛型应用,一句话:如果静态方法要使用泛型,必须将静态方法也定义成泛型方法 。举例如下:

    public class StaticGenerator<T> {
      ....
      ....
      /**
       * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
       * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
       * 如:public static void show(T t){..},此时编译器会提示错误信息:
            "StaticGenerator cannot be refrenced from static context"
       */
      public static <T> void show(T t){
          ...
      }
    }
    

    5、泛型通配符

    5.1 通配符的作用

    5.1.1 抛砖引玉的demo

    介绍泛型通配符之前先介绍一个例子,通过这个例子更容易理解泛型通配符的作用。我们知道Integer是Number的一个子类,由于Java中多态的概念,如果一个方法的入参是父类实例,在实际调用该方法时,入参可以传父类实例,也可以传继承该父类的子类的实例。回到泛型上来,第二节我们例子中有一个Generic的泛型类,那 Generic和Generic也能像Java多态那样使用么?写个demo如下:
    GenericWildCard:

    public class GenericWildCard {
      public static void print(Generic<Number> obj){
          System.out.println(obj.toString());
      }
    }
    

    Main:

    public class Main {
      public static void main(String[] args) {
          Generic<Number> generic1 = new Generic<>(111);
          Generic<Integer> generic2 = new Generic<>(111);
    
          GenericWildCard.print(generic1);
          // 编译报错,说明泛型类的使用不能像Java多态那样
          // GenericWildCard.print(generic2);
    
          // class generic.Generic
          System.out.println(generic1.getClass());
          // class generic.Generic
          System.out.println(generic2.getClass());
          // true,说明两个泛型类的class对象相同
          System.out.println(generic1.getClass() == generic2.getClass());
      }
    }
    

    GenericWildCard类提供了一个静态方法,该静态方法的入参是Generic类型,但调用时传入generic1没问题,但传入generic2就会编译报错:不兼容的类型: generic.Generic无法转换为generic.Generic。从上面例子中可以看出,Generic不能被看作为Generic的子类,这两个类的class对象是相同的。那如何解决泛型中无法直接使用多态(这种现象还有一个名词叫协变)呢?答案就是泛型通配符。

    5.1.2 泛型通配符

    泛型通配符就是为了解决泛型中无法协变的问题,将泛型中的参数类型T用?代替,泛型通配符的引入扩展了泛型参数类型的范围,使传入参数类型更加灵活。
    5.1.1小节中的例子,用泛型通配符解决,如下:
    GenericWildCard:

    public class GenericWildCard {
      public static void print(Generic<?> obj){
          System.out.println(obj.toString());
      }
    }
    

    此时调用print方法处,传入generic1和generic2编译都是成功的。

    5.1.3 ?和T的区别

  • T是指特定的一种参数类型,而?表示参数类型不确定,比如: ```java public static void show1(List list){ for (Object object : list) {

      System.out.println(object.toString());
    

    } }

public static void show2(List<?> list) { for (Object object : list) { System.out.println(object); } }

调用show1方法传入的list,组成list的元素必须是同一个类的实例,比如都是Student类的实例;而调用show2方法传入的list,组成list的元素类型没有限制,可以是Student类的实例,也可以是Teacher类的实例。

- 通配符?还有上边界和下边界的概念,T没有。
<a name="uw6G3"></a>
## 5.2 泛型的上边界
为泛型添加上边界,即传入的类型实参必须是指定类及其子类,格式如下:
```java
类名<? extends 父类名>

还是以5.1.1中的GenericWildCard类为例进行扩展:

public class GenericWildCard {
    public static void print(Generic<?> obj){
        System.out.println(obj.toString());
    }

    // 泛型上边界为Number类
    public static void show(Generic<? extends Number> obj) {
        System.out.println(obj.toString());
    }
}

Main:

public class Main {
    public static void main(String[] args) {
        Generic<Number> generic1 = new Generic<>(111);
        Generic<Integer> generic2 = new Generic<>(111);
        Generic<String> generic3 = new Generic<>("111");

        GenericWildCard.show(generic1);
        GenericWildCard.show(generic2);
        // 编译报错
        // GenericWildCard.show(generic3);
    }
}

可以看到,show方法的入参使用了泛型类的上边界,上边界为Number类,因此generic1和generic2可以作为show方法的入参传入;而generic3类型是Generic,String并不是Number类的子类,因此generic3并不能作为show方法的入参。

5.3 泛型的下边界

为泛型添加下边界,即传入的类型实参必须是指定类及其父类,格式如下:

类名<? super 子类名>

还是以GenericWildCard类为例,改写5.2中的show方法,如下:

public class GenericWildCard {
    public static void print(Generic<?> obj){
        System.out.println(obj.toString());
    }

    public static void show(Generic<? super YellowDog> obj) {
        System.out.println(obj.toString());
    }
}

新建了三个POJO类,继承关系从父类到子类依次是:Dog、YellowDog、YellowFatDog。
此外对Generic类做了些小改动,去掉了之前的有参构造函数,如下:

public class Generic<E> {
    private E data;

    public E get() {
        return this.data;
    }

    public void set(E data) {
        this.data = data;
    }
}

Main:

public class Main {
    public static void main(String[] args) {
        Generic<YellowDog> generic1 = new Generic<>();
        Generic<Dog> generic2 = new Generic<>();
        Generic<YellowFatDog> generic3 = new Generic<>();

        GenericWildCard.show(generic1);
        GenericWildCard.show(generic2);
        // 编译报错
        // GenericWildCard.show(generic3);
    }
}

从上面可以看出,由于YellowDog和Dog类都是show方法中Generic<? super YellowDog> obj的YellowDog类的本身及其父类,因此generic1和generic2都可以作为show方法入参传入;YellowFatDog作为YellowDog类的子类,因此generic3无法作为show方法的入参传入。

5.4 泛型中的PECS概念

5.2和5.3节介绍了泛型的上下边界的概念及用法,本节介绍泛型上下边界使用时要遵循的规则,这个规则也叫PECS:Producer extends Consumer super (生产者使用extends,consumer使用super),具体为:

  • 上边界:不能写,可以读;
  • 下边界:可以写,不能读。

验证上边界,类的基础关系从父类到子类依次是:Dog、YellowDog、YellowFatDog:

public class Main {
    public static void main(String[] args) {
        List<? extends Dog> list = new ArrayList();
        list.add(new Dog());
        list.add(new YellowDog());
        YellowDog yellowDog = list.get(0);
        Dog dog = list.get(0);
    }
}

编译情况如下图:
image.png
结论:泛型的上边界不能add,只能get父类的实例。

验证下边界:

public class Main {
    public static void main(String[] args) {
        List<? super YellowDog> list = new ArrayList();
        list.add(new Dog());
        list.add(new YellowDog());
        list.add(new YellowFatDog());

        Dog dog = list.get(0);
        YellowDog yellowDog = list.get(0);
        YellowFatDog yellowFatDog = list.get(0);
        Object obj = list.get(0);
    }
}

编译结果如下图所示:
image.png
结论:泛型的下边界,只能add当前类及其子类的实例,不能get(非要get只能用Object类来接)。

5.5 Class<?>和Class的区别

实际上它俩在功能上也确实没有什么区别,只是不使用通配符会有警告,如下:

// 会报rowTypes的warning警告
Class clazz1 = String.class; 

// 没有警告
Class<?> clazz2 = String.class;

因为Class在定义的时候使用了泛型,如果Class不传入类型参数,编译器会认为你使用了没有类型的Class,就好比你使用了一个无类型的List一样:

// 编译不会报错,只会提示
List list = new ArrayList();

参考

java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一
泛型就这么简单
Java 泛型中的通配符
泛型就这么简单
泛型PECS原则理解