都2020了,还很差好学学泛型?

1、概述

Java 泛型(generics)是 JDK 1.5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制容许开发者在编译时检测到非法的类型。java

1.1 什么是泛型?

  • 泛型,即参数化类型

一提到参数,最熟悉的就是定义方法时有形参,而后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,相似于方法中的变量参数,此时类型也定义成参数形式(能够称之为类型形参),而后在使用/调用时传入具体的类型(类型实参)。git

  • 泛型的本质是为了参数化类型

在不建立新的类型的状况下,经过泛型指定的不一样类型来控制形参具体限制的类型。也就是说在泛型使用过程当中,操做的数据类型被指定为一个参数,这种参数类型能够用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。github

1.2 举个栗子:

@Test
public void genericDemo() {
    List list = new ArrayList();
    list.add("风尘博客");
    list.add(100);

    for(int i = 0; i< list.size();i++){
        String item = (String)list.get(i);
        log.info("item:{}", item);
    }
}
复制代码

毫无疑问,程序的运行结果会以崩溃结束:数组

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
复制代码

ArrayList能够存听任意类型,例子中先添加了一个String类型,又添加了一个Integer类型。使用时都以String的方式使用,所以程序崩溃了。为了解决相似这样的问题(在编译阶段就能够解决),泛型应运而生。安全

1.3 特性

泛型只在编译阶段有效app

  1. 在编译的时候可以检查类型安全,而且全部的强制转换都是自动和隐式的;
  2. 在逻辑上看以当作是多个不一样的类型,实际上都是相同的基本类型。

2、泛型的使用

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

2.1 泛型类

泛型类型用于类的定义中,被称为泛型类。经过泛型能够完成对一组类的操做对外开放相同的接口。最典型的就是各类容器类,如:ListSetMapjvm

2.1.1 一个最普通的泛型类:

public class Generic<T> {
    /** * key这个成员变量的类型为T,T的类型由外部指定 */
    private T key;

    /** * 泛型构造方法形参key的类型也为T,T的类型由外部指定 * @param key */
    public Generic(T key) {
        this.key = key;
    }

    /** * 泛型方法getKey()的返回值类型为T,T的类型由外部指定 * @return */
    public T getKey(){
        return key;
    }
}
复制代码

说明:ide

  1. 此处T能够随便写为任意标识,常见的如TEKV等形式的参数经常使用于表示泛型;
  2. 在实例化泛型类时,必须指定T的具体类型。

2.1.2 泛型的使用

  • 指定泛型类型
@Test
public void genericDemoWithType() {
    //泛型的类型参数只能是类类型(包括自定义类),不能是简单类型,好比这里Integer改成int编译将不经过
    Generic<Integer> integerGeneric = new Generic<Integer>(123456);
    log.info("integerGeneric key is:{}", integerGeneric.getKey());

    //传入的实参类型需与泛型的类型参数类型相同,即为String.
    Generic<String> stringGeneric = new Generic<String>("风尘博客");
    log.info("stringGeneric key is:{}", stringGeneric.getKey());
}
复制代码
  • 不指定泛型类型

若是不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型能够为任何的类型。post

@Test
public void genericDemoWithOutType() {
    Generic generic = new Generic("111111");
    Generic generic1 = new Generic(4444);
    Generic generic2 = new Generic(55.55);
    Generic generic3 = new Generic(false);
    log.info("generic key is:{}",generic.getKey());
    log.info("generic1 key is:{}",generic1.getKey());
    log.info("generic2 key is:{}",generic2.getKey());
    log.info("generic3 key is:{}",generic3.getKey());
}
复制代码

打印结果

... generic key is:111111
... generic1 key is:4444
... generic2 key is:55.55
... generic3 key is:false
复制代码

2.1.3 泛型类小结

  1. 泛型的类型参数只能是类类型,不能是简单类型;
  2. 不能对确切的泛型类型使用instanceof操做。

2.2 泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各类类的生产器中,例如:

public interface Generator<T> {
    public T next();
}
复制代码
  • 当实现泛型接口的类,未传入泛型实参时

未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一块儿加到类中。

public class FruitGenerator<T> implements Generator<T>{

    public T next() {
        return null;
    }
}
复制代码
  • 当实现泛型接口的类,传入泛型实参

在实现类实现泛型接口时,如已将泛型类型传入实参类型,则全部使用泛型的地方都要替换成传入的实参类型。

public class VegetablesGenerator implements Generator<String>{

    private String[] vegetables = new String[]{"Potato", "Tomato"};

    public String next() {
        Random rand = new Random();
        return vegetables[rand.nextInt(2)];
    }
}
复制代码

本小节示例代码地址

2.3 泛型方法

java中,泛型类的定义很是简单,可是泛型方法就比较复杂了。

咱们见到的大多数泛型类中的成员方法也都使用了泛型,有的甚至泛型类中也包含着泛型方法。

  • 泛型类和泛型方法的区别
名称 泛型类 泛型方法
区别 是在实例化类的时候指明泛型的具体类型 是在调用方法的时候指明泛型的具体类型

2.3.1 定义

public <T> T showKeyName(GenericMethodDemo<T> container){    
    return null;
}
复制代码
  1. 首先在public与返回值之间的<T>必不可少,这代表这是一个泛型方法,而且声明了一个泛型T
  2. 这个T能够出如今这个泛型方法的任意位置;
  3. 泛型的数量也能够为任意多个。
public class GenericMethodDemo {

    /** * 泛型类 * @param <T> */
    public class Generic<T> {
        private T key;

        public Generic(T key) {
            this.key = key;
        }

        /** * 这里虽然在方法中使用了泛型,可是这并非一个泛型方法, * 这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型, * 因此在这个方法中才能够继续使用 T 这个泛型。 * @return */
        public T getKey() {
            return key;
        }

        /** * 这才是一个真正的泛型方法 * @param container * @param <T> * @return */
        public <T> T keyName(Generic<T> container){
            T test = container.getKey();
            return test;        }

        /** * 这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类作形参而已。 * @param obj */
        public void showKeyValue1(Generic<Number> obj){

        }

        /** * 这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符? * @param obj */
        public void showKeyValue2(Generic<?> obj){

        }


        /** * 该方法编译器会报错 * 虽然咱们声明了<T>,也代表了这是一个能够处理泛型的类型的泛型方法。 * 可是只声明了泛型类型T,并未声明泛型类型E,所以编译器并不知道该如何处理E这个类型。 * @param container * @param <T> * @return */
        public <T> T showKeyName(Generic<E> container){
            return null;
        }
        
    }
}
复制代码

详见 Githu GenericMethodDemo.java

2.3.2 泛型方法的使用

泛型方法能够出现杂任何地方和任何场景中使用,可是有一种状况是很是特殊的,泛型方法出如今泛型类中

public class GenericFruit {

    class Fruit{
        @Override
        public String toString() {
            return "fruit";
        }
    }

    class Apple extends Fruit{
        @Override
        public String toString() {
            return "apple";
        }
    }

    class Person{
        @Override
        public String toString() {
            return "Person";
        }
    }

    class GenerateTest<T>{

        public void show_1(T t){
            System.out.println(t.toString());
        }

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

        //在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,能够与泛型类中声明的T不是同一种类型。
        public <T> void show_2(T t){
            System.out.println(t.toString());
        }
    }

    @Test
    public void test() {

        Apple apple = new Apple();
        Person person = new Person();

        GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
        //apple是Fruit的子类,因此这里能够
        generateTest.show_1(apple);
        //编译器会报错,由于泛型类型实参指定的是Fruit,而传入的实参类是Person
        //generateTest.show_1(person);

        //使用这两个方法均可以成功
        generateTest.show_2(apple);
        generateTest.show_2(person);

        //使用这两个方法也均可以成功
        generateTest.show_3(apple);
        generateTest.show_3(person);
    }
}
复制代码

详见 Githu GenericFruitTest.java

2.3.3 静态方法与泛型

静态方法没法访问类上定义的泛型;若是静态方法操做的引用数据类型不肯定的时候,必需要将泛型定义在方法上。

若是写成以下,编译器会报错

public staticvoid show(T t){
    
}
复制代码
  • 正确写法:
public static <T> void show(T t){
    
}
复制代码

2.3.4 泛型方法小结

泛型方法能使方法独立于类而产生变化,如下是一个基本的指导原则:

不管什么时候,若是你能作到,你就该尽可能使用泛型方法。也就是说,若是使用泛型方法将整个类泛型化,那么就应该使用泛型方法。另外对于一个static的方法而已,没法访问泛型类型的参数。因此若是static方法要使用泛型能力,就必须使其成为泛型方法。

3、泛型通配符

咱们在定义泛型类,泛型方法,泛型接口的时候常常会遇见不少不一样的通配符,好比 TEKV 等等,这些通配符又都是什么意思呢?

3.1 经常使用的 TEKV

本质上这些都是通配符,没啥区别,只不过是编码时的一种约定俗成的东西。好比上述代码中的 T ,咱们能够换成 A-Z 之间的任何一个字母均可以,并不会影响程序的正常运行,可是若是换成其余的字母代替 T ,在可读性上可能会弱一些。一般状况下,TEKV 是这样约定的:

  1. :表示不肯定的 java 类型;
  2. T (type):表示具体的一个java类型;
  3. K V (key value):分别表明java键值中的Key/Value
  4. E (element):表明Element

3.2 ?无界通配符

对于不肯定或者不关心实际要操做的类型,可使用无限制通配符(尖括号里一个问号,即 <?> ),表示能够持有任何类型。

3.3 上界通配符 <? extends E>

上界:用 extends 关键字声明,表示参数化的类型多是所指定的类型,或者是此类型的子类。

public void showKeyValue(Generic<? extends Number> obj){
    log.info("value is {}", obj.getKey());
}

@Test
public void testForUp() {
    Generic<String> generic1 = new Generic<String>("11111");
    Generic<Integer> generic2 = new Generic<Integer>(2222);
    Generic<Float> generic3 = new Generic<Float>(2.4f);
    Generic<Double> generic4 = new Generic<Double>(2.56);

    /*// 这一行代码编译器会提示错误,由于String类型并非Number类型的子类 showKeyValue(generic1);*/

    showKeyValue(generic2);
    showKeyValue(generic3);
    showKeyValue(generic4);
}
复制代码

在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:

  1. 若是传入的类型不是 E 或者 E 的子类,编译不成功;
  2. 泛型中可使用 E 的方法,要否则还得强转成 E 才能使用。

3.4 下界通配符 < ? super E>

下界: 用 super 进行声明,表示参数化的类型多是所指定的类型,或者是此类型的父类型,直至 Object

在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。

泛型的上下边界添加,必须与泛型的声明在一块儿

实例代码地址

3.5 ?T 的区别

?T 都表示不肯定的类型,区别在于咱们能够对 T 进行操做,可是对 ? 不行,好比以下这种 :

// 能够
T t = operate();

// 不能够
? car = operate();
复制代码

即:T 是一个肯定的类型,一般用于泛型类和泛型方法的定义,?是一个不肯定的类型,一般用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。

3.6 Class<T>Class<?> 区别

Class<T> 在实例化的时候,T 要替换成具体类。Class<?> 它是个通配泛型,?能够表明任何类型,因此主要用于声明时的限制状况。好比,咱们能够这样作申明:

// 能够
public Class<?> clazz;

// 不能够,由于 T 须要指定类型
public Class<T> clazzT;
复制代码

因此当不知道定声明什么类型的 Class 的时候能够定义一 个Class<?>。 那若是也想 public Class<T> clazzT; 这样的话,就必须让当前的类也指定 T

public class Wildcard<T> {

    public Class<?> clazz;

    public Class<T> clazzT;
}
复制代码

4、泛型中值得注意的地方

4.1 类型擦除

泛型信息只存在于代码编译阶段,在进入 JVM 以前,与泛型相关的信息会被擦除掉,专业术语叫作类型擦除。

public class GenericTypeErase {

    public static void main(String[] args) {
        List<String> l1 = new ArrayList<String>();
        List<Integer> l2 = new ArrayList<Integer>();
        System.out.println(l1.getClass() == l2.getClass());

    }
}
复制代码

打印的结果为 true;是由于 List<String>List<Integer>jvm 中的 Class 都是 List.class,泛型信息被擦除了。

4.2 泛型类或者泛型方法中,不接受 8 种基本数据类型

须要使用它们对应的包装类。

4.3 Java 不能建立具体类型的泛型数组

List<Integer>[] li2 = new ArrayList<Integer>[];
List<Boolean> li3 = new ArrayList<Boolean>[];
复制代码

List<Integer>List<Boolean>jvm 中等同于List<Object>,全部的类型信息都被擦除,程序也没法分辨一个数组中的元素类型具体是 List<Integer>类型仍是 List<Boolean>类型。

4.4 强烈建议你们使用泛型

它抽离了数据类型与代码逻辑,本意是提升程序代码的简洁性和可读性,并提供可能的编译时类型转换安全检测功能。

5、总结

5.1 示例源码

Githu 示例代码

5.2 参考文章

  1. java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一
  2. 聊一聊-JAVA 泛型中的通配符 T,E,K,V,?

5.3 技术交流

Github 示例代码

  1. 风尘博客:https://www.dustyblog.cn
  2. 风尘博客-掘金
  3. 风尘博客-博客园
  4. Github
  5. 公众号
    风尘博客
相关文章
相关标签/搜索