详解Java泛型

往期推荐

为什么会引入泛型

泛型的优点

1、泛型的本质是为了参数化类型,也就是在在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型,很明显这种方法提高了代码的复用性。

2、泛型的引入提高了安全性,泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型。

3、在没有泛型的情况的下,通过对类型 Object 的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是本身就是一个安全隐患。

那么泛型的好处就是在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的

为什么提高了安全性?

不安全举例


package keyAndDifficultPoints.Generic;


import java.util.ArrayList;
import java.util.List;

/**
 * @Author: Akiang
 * @Date: 2021/9/9 16:09
 * <p>
 * 功能描述:
 */
public class Test_Safe {

    public static void main(String[] args) {
        test();
    }

    public static void test() {
        List arrayList = new ArrayList();
        arrayList.add("aaaa");
        arrayList.add(100);

        for (int i = 0; i < arrayList.size(); i++) {
            String s = (String) arrayList.get(i);
            System.out.println(s);

        }
    }
}    
复制代码

结果:

aaaa
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
	at keyAndDifficultPoints.Generic.Test_Safe.test(Test_Safe.java:25)
	at keyAndDifficultPoints.Generic.Test_Safe.main(Test_Safe.java:16)

复制代码

很明显的一个类型转换错误。ArrayList可以存放任意类型,例子中添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,因此程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生

泛型提高安全性


public static void test01(){
       List<String> arrayList = new ArrayList<>();
       arrayList.add("aaaa");
       //下面代码编译时就直接报错了
       arrayList.add(100);

       for (int i = 0; i < arrayList.size(); i++) {
           String s = (String) arrayList.get(i);
           System.out.println(s);

       }
   }
   
复制代码

通过泛型来提前检测类型,编译时就通不过。

泛型类

泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:ListSetMap


package keyAndDifficultPoints.Generic;

/**
 * @Author: Akiang
 * @Date: 2021/9/9 16:38
 * <p>
 * 功能描述:
 */
public class Test_GenericClass {
    public static void main(String[] args) {
        test();
    }

    public static void test(){
        /**
         * 1、泛型的类型参数只能是类类型(包括自定义类),不能是简单数据类型(比如int,long这些)
         * 2、传入的实参类型需与泛型的类型参数类型相同,即为这里的Integer。
         * 3、new 后面的泛型参数可以省略
         */
        Generic<Integer> genericInteger1 = new Generic<Integer>(123);
        Generic<Integer> genericInteger = new Generic<>(123);

        Generic<String> genericString = new Generic<String>("my");

        System.out.println(genericInteger.getVar());
        System.out.println(genericString.getVar());
    }


}

/**
 * 1、此处T虽然可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
 * 但是为了代码的可读性一般来说:
 * K,V用来表示键值对
 * E是Element的缩写,常用来遍历时表示
 * T就是Type的缩写,常用在普通泛型类上
 * 2、还有一些不常见的U,R啥的
 */
class Generic<T> {
    //key这个成员变量的类型为T,T的类型由外部指定
    private T var;

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

    public T getVar() { //泛型方法getKey的返回值类型为T,T的类型由外部指定
        return var;
    }
}

class MyMap<K, V> {       // 此处指定了两个泛型类型
    private K key;     // 此变量的类型由外部决定
    private V value;   // 此变量的类型由外部决定

    public K getKey() {
        return this.key;
    }

    public V getValue() {
        return this.value;
    }

    public void setKey(K key) {
        this.key = key;
    }

    public void setValue(V value) {
        this.value = value;
    }
};

复制代码
  • 定义的泛型类,就一定要传入泛型类型实参么?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。

  • 如果继承于父类(泛型类)的子类是泛型类,那么子类和父类的泛型类型需一致。

    class children<T> extends GenericParent<T> { }

  • 如果继承于父类(泛型类)的子类为非泛型类,那么在子类在声明继承父类时必须明确指定父类的泛型类型。

    class children extends GenenricParent<String> { }

泛型方法

泛型类是我们在实例化类时指明泛型的具体类型,而泛型方法就是我们在调用方法时指明泛型的具体类型,泛型方法能使方法独立于类而产生变化

  • 定义泛型方法语法格式

0003.png

定义泛型方法时,必须在返回值前边加一个<T>,来声明这是一个泛型方法,持有一个泛型T,然后才可以用泛型T作为方法的返回值。

Class<T>的作用就是指明泛型的具体类型,而Class<T>类型的变量c,可以用来创建泛型类的对象。

  • 调用泛型方法语法格式

0004.png

泛型方法要求的参数是Class<T>类型,而Class.forName()方法的返回值也是Class<T>,因此可以用Class.forName()作为参数。其中,forName()方法中的参数是何种类型,返回的Class<T>就是何种类型。

  • 泛型方法例子:
public class GenericClass<T> {
   
    //使用泛型方法后,该方法就可以接收任何数据类型的参数
    //<T> 泛型标识,具体类型由调用该方法时具体指定
    public static <T> void genericMethod(T birthday){
        System.out.println(birthday);
    }
    
    //采用多个泛型类型
    public static <T,V,E> void genericMethod2(T t,V v,E e){
        System.out.println(t+"-"+v+"-"+e);
    }
    
   
    //泛型可变参数的定义
    public static <T> void GenericVariadic(T...t){
        for (int i = 0; i < t.length; i++) {
            System.out.println(t[i]);
        }
    }

    
    public static void main(String[] args) {
      
        GenericClass.genericMethod(new Date());
        GenericClass.genericMethod("2021-09-09");
        
        //调用多个泛型类型的泛型方法
        GenericClass.genericMethod2("Akiang",212,110);
       
        //泛型可变参数
        GenericClass.GenericVariadic(1,2,3,4,5,"4");
    }
}
复制代码

泛型接口

interface Info<T>{        // 在接口上定义泛型  
    public T getVar() ; // 定义方法,方法的返回值就是泛型类型  
} 
复制代码
  • 如果实现泛型接口的实现类不是泛型类,则在声明实现泛型接口时必须明确指定泛型接口的泛型类型

    class interfaceImpl implements GenericInterface<String> { }

  • 如果实现泛型接口的实现类是泛型类,则实现类的泛型类型必须包含有接口泛型类的泛型类型

    class interfaceImpl<T,其他泛型标识...> implements GenericInterface<T> { }

当实现泛型接口的类,未传入泛型实参时:

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class InfoImpl<T> implements Info<T>
 * 如果不声明泛型,如:class InfoImpl implements Info<T>,编译器会报错:"Unknown class"
 */
class InfoImpl<T> implements Info<T> {   // 定义泛型接口的子类
    private T var;

    public InfoImpl(T var) {
        this.setVar(var);
    }

    public void setVar(T var) {
        this.var = var;
    }

    public T getVar() {
        return this.var;
    }
}
复制代码

当实现泛型接口的类,传入泛型实参时:

/**
 * 传入泛型实参时:
 * 定义一个是先烈实现这个接口,虽然我们只创建了一个泛型接口Info<T>
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
 * 即:InfoImpl01<T>,public String getVar();中的的T都要替换成传入的String类型。
 */
class InfoImpl01 implements Info<String> {   // 定义泛型接口的子类
    private String var;

    public InfoImpl01(String var) {
        this.setVar(var);
    }

    public void setVar(String var) {
        this.var = var;
    }

    public String getVar() {
        return this.var;
    }
}
复制代码

泛型数组

官方sun文档

  • 可以声明带泛型的数组引用,如ArrayList<String> [] listArray;,但是不能直接创建带泛型的数组对象,如ArrayList<String> [] listArray = new ArrayList<String>[2];

package keyAndDifficultPoints.Generic;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author: Akiang
 * @Date: 2021/09/08 12:10
 * <p>
 * 功能描述: 测试泛型数组
 */
public class Test_GenericArray {

    public static void main(String[] args) {
        test02();
    }

    public static void test() {
        //编译错误
//        List<String>[] ls = new ArrayList<String>[10];
    }


    public static void test01() {
        //这样声明是正确的
        List<?>[] ls = new ArrayList<?>[10];
        ls[1] = new ArrayList<String>();

        //这样写编译就报错了
//        ls[1].add(1);

    }

    /**
     * 下面是sun官方文档里写的。其实不用太纠结,平时泛型虽然用的多,但也不会用的这么奇葩。
     */
    public static void test02(){
        List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.
        Object o = lsa;
        Object[] oa = (Object[]) o;
        List<Integer> li = new ArrayList<Integer>();
        li.add(new Integer(3));
        oa[1] = li; // Correct.
        Integer i = (Integer) lsa[1].get(0); // OK
        System.out.println(i);
    }

    //正确
    public static void test03() {
        List<String>[] ls = new ArrayList[10];
        ls[0] = new ArrayList<String>();
        ls[1] = new ArrayList<String>();

        ls[0].add("x");

    }

}
复制代码

泛型通配符

常用的通配符

本质上这些都是表示通配符,没什么区别,只不过是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,?是这样约定的:

  • 表示不确定的 java 类型
  • T (type) 表示具体的一个java类型
  • K V (key value) 分别代表java键值对中的Key Value
  • E (element) 代表元素

无限通配符

通配符的使用
       通配符:?

       类A是类B的父类,G<A>和G<B>是没有关系的,二者共同的父类是:G<?>
复制代码
    @Test
    public void test3() {
        List<Object> list1 = null;
        List<String> list2 = null;

        List<?> list = null;

        list = list1;
        list = list2;
        //编译通过
//        print(list1);
//        print(list2);


        //
        List<String> list3 = new ArrayList<>();
        list3.add("AA");
        list3.add("BB");
        list3.add("CC");
        list = list3;
        //添加(写入):对于List<?>就不能向其内部添加数据。
        //除了添加null之外。
//        list.add("DD");
//        list.add('?');

        list.add(null);

        //获取(读取):允许读取数据,读取的数据类型为Object。
        Object o = list.get(0);
        System.out.println(o);


    }
复制代码

上界通配符 < ? extends E>

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

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

  • 如果传入的类型不是 E 或者 E 的子类,编译不成功
  • 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用
class Info<T extends Number>{    // 此处泛型只能是数字类型
    private T var ;        // 定义泛型变量
    public void setVar(T var){
        this.var = var ;
    }
    public T getVar(){
        return this.var ;
    }
    public String toString(){    // 直接打印
        return this.var.toString() ;
    }
}
public class demo1{
    public static void main(String args[]){
        Info<Integer> i1 = new Info<Integer>() ;        // 声明Integer的泛型对象
    }
}
复制代码

下界通配符 < ? super E>

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

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

class Info<T>{
    private T var ;        // 定义泛型变量
    public void setVar(T var){
        this.var = var ;
    }
    public T getVar(){
        return this.var ;
    }
    public String toString(){    // 直接打印
        return this.var.toString() ;
    }
}
public class GenericsDemo21{
    public static void main(String args[]){
        Info<String> i1 = new Info<String>() ;        // 声明String的泛型对象
        Info<Object> i2 = new Info<Object>() ;        // 声明Object的泛型对象
        i1.setVar("hello") ;
        i2.setVar(new Object()) ;
        fun(i1) ;
        fun(i2) ;
    }
    public static void fun(Info<? super String> temp){    // 只能接收String或Object类型的泛型,String类的父类只有Object类
        System.out.print(temp + ", ") ;
    }
}
复制代码

?和 T 的区别

T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 不行。

  • T 是一个 确定的 类型,通常用于泛型类和泛型方法的定义。
  • 是一个 不确定 的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。

区别1:通过T来确保泛型参数的一致性

package keyAndDifficultPoints.Wildcard_Character;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author: Akiang
 * @Date: 2021/09/09 11:28
 * <p>
 * 功能描述:
 */
public class Test_difference {

    public static void main(String[] args) {
        List<Integer> integerList = new ArrayList<>();
        List<Float> floatList = new ArrayList<>();

        //编译报错
//        test(integerList, floatList);
        //编译通过
        test1(integerList, floatList);


        //编译通过
        test(integerList, integerList);
        test1(integerList, integerList);

    }



    // 通过 T 来 确保 泛型参数的一致性
    public static <T extends Number> void test(List<T> dest, List<T> src){

    }

    //通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型
    public static void test1(List<? extends Number> dest, List<? extends Number> src){

    }
}
复制代码

区别2:T可以通过&进行多重限定

public class Test_difference {

    public static void main(String[] args) {


        /*---------------------测试多重限定符---------------------*/
        ArrayList list = new ArrayList<>();
        ArrayDeque deque = new ArrayDeque<>();
        LinkedList<Object> linkedList = new LinkedList<>();

        //多重限定时,在编译的时候取最小范围或共同子类

        test2(list);
        test3(list); 编译报错

        //编译报错
        test2(deque);
        test3(deque);

        //编译通过
        test2(linkedList);
        test3(linkedList);


    }


    //可以进行多重限定
    public static <T extends List & Collection> void test2(T t) {

    }

    //可以进行多重限定
    public static <T extends Queue & List> void test3(T t) {

    }

    //编译报错,无法进行多重限定
    public static <? extends List & Collection> void test4(List<T> dest, List<T> src){
   }

}
复制代码

区别3:?通配符可以使用超类限定而T不行

类型参数 T 只具有 一种 类型限定方式:

T extends A
复制代码

但是通配符 ? 可以进行 两种限定:

? extends A
? super A
复制代码

泛型擦除

Java的泛型是伪泛型,为什么说Java的泛型是伪泛型呢?因为在编译期间,所有的泛型信息都会被擦除掉,我们常称为泛型擦除

  • Java中的泛型基本上都是在编译器这个层次来实现的,在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,编译器在编译的时候去掉,这个过程就称为类型擦除。

​ 如在代码中定义的List<object>List<String>等类型,在编译后都会编程List,JVM看到的只是List。而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方法与C++模版机制实现方式之间的重要区别。

擦除类定义中的类型参数 – 无限制类型擦除

当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如<T><?>的类型参数都被替换为Object。

0005.png

擦除类定义中的类型参数 – 有限制类型擦除

当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number><? extends Number>的类型参数被替换为Number<? super Number>被替换为Object。

0006.png

擦除方法定义中的类型参数

擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,这里仅以擦除方法定义中的有限制类型参数为例。

0007.png

桥接方法

首先我们定义一个Info泛型接口包含method()方法,然后InfoImpl实现Info接口并指定泛型接口的泛型类型为Integer并重写接口中的method()方法,通过反射查看编译后InfoImpl实现类中的方法有哪些及其返回值类型。

public interface Info<T> {
    T method(T t);
}

public class InfoImpl implements Info<Integer> {
    @Override
    public Integer method(Integer i) {
        return i;
    }

    public static void main(String[] args) {
        //获取泛型接口Info的实现类InfoImpl的字节码
        Class<InfoImpl> infoClass = InfoImpl.class;
        //获取所有方法
        Method[] mes = infoClass.getDeclaredMethods();
        for (Method method : mes) {
            //输出实现类中的方法名称和返回值类型
            if(method.getName()!="main")
            System.out.println(method.getName() + ":" + method.getReturnType());
        }
    }
}

程序输出:
method:class java.lang.Integer
method:class java.lang.Object
复制代码

运行以上main方法后,我们可以发现InfoImpl实现类中出现了两个名称一样但返回值类型和参数列表的类型都不同的method()方法。

其实其中返回值类型和参数类型为Object的method()是Java虚拟机会帮我们添加的,该方法就是一个桥接方法。因为我们定义的Integer类型的泛型接口Info编译后会被擦除变为Object类型,而我们的实现类InfoImpl为了保证我们对接口实现的规范和约束,所以Java 虚拟机会帮我们在实现类中生成一个返回值类型和参数类型为Objectmethod()方法,从而保持接口和类的实现关系

2.png

如何理解基本类型不能作为泛型类型?

比如,我们没有ArrayList<int>,只有ArrayList<Integer>, 为何?

因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储int值,只能引用Integer的值。

另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作。

如何证明类型的擦除呢?

  • 原始类型相等
public class Test {

    public static void main(String[] args) {

        ArrayList<String> list1 = new ArrayList<String>();
        list1.add("abc");

        ArrayList<Integer> list2 = new ArrayList<Integer>();
        list2.add(123);

        System.out.println(list1.getClass() == list2.getClass()); // true
    }
}

复制代码

在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList<String>泛型类型的,只能存储字符串;一个是ArrayList<Integer>泛型类型的,只能存储整数,最后,我们通过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型StringInteger都被擦除掉了,只剩下原始类型。

  • 通过反射添加其它类型元素
public class Test {

    public static void main(String[] args) throws Exception {

        ArrayList<Integer> list = new ArrayList<Integer>();

        list.add(1);  //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer

        list.getClass().getMethod("add", Object.class).invoke(list, "asd");

        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }

}

复制代码

在程序中定义了一个ArrayList泛型类型实例化为Integer对象,如果直接调用add()方法,那么只能存储整数数据,不过当我们利用反射调用add()方法的时候,却可以存储字符串,这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型。

如何理解泛型类型不能实例化?

不能实例化泛型类型, 这本质上是由于类型擦除决定的:

   我们可以看到如下代码会在编译器中报错:

T test = new T(); // ERROR

因为在 Java 编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了
此外由于`T` 被擦除为 `Object`,如果可以 `new T()` 则就变成了 `new Object()`,失去了本意。 
复制代码

    如果我们确实需要实例化一个泛型,应该如何做呢?可以通过反射实现:

static <T> T newTclass (Class < T > clazz) throws InstantiationException, IllegalAccessException {
    T obj = clazz.newInstance();
    return obj;
}
复制代码

以上就是对Java中泛型知识点的总结,掌握好泛型的使用,可以提高我们代码的重用率也通过消除了强制的类型转换可以提高我们程序的安全性。

1111.gif

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享