美文网首页
Java泛型的详细学习指南,全面总结

Java泛型的详细学习指南,全面总结

作者: 码农翻身记 | 来源:发表于2021-07-21 17:56 被阅读0次

    一、概述

    Java开发经常会用到泛型,常用的List、Map都用到了,泛型在Java中有很重要的地位,被广泛应用于面向对象编程和各种设计模式中。什么是泛型?为什么要用泛型?

    一道经典的测试题:

    List<String> l1 = new ArrayList<String>();
    List<Integer> l2 = new ArrayList<Integer>();
            
    System.out.println(l1.getClass() == l2.getClass());
    

    上面输出的结果是什么?了解泛型的童鞋就知道是true,缘由是泛型类型擦除。

    1.1、定义

    泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

    泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

    看文字有点难理解,结合后面的例子就比较好理解了。

    1.2、为什么需要泛型

    引入泛型有两个方面的作用,一个是提高代码的重用率,二是把运行时错误提前到编译时,提高安全性。看一个例子:

    List arrayList = new ArrayList();
         arrayList.add("string");
         arrayList.add(1);
    
         for(int i = 0; i< arrayList.size();i++){
              String item = (String)arrayList.get(i);
              System.out.println("泛型测试"+"item = " + item);
         }
    

    编译不会有错误,但运行就崩溃了,崩溃日志如下:

    java.lang.Integer cannot be cast to java.lang.String
    

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

    我们将第一行声明初始化list的代码更改一下,编译器会在编译阶段就能够帮我们发现类似这样的问题。

    List<String> arrayList = new ArrayList<String>();
    ...
    //arrayList.add(1); 在编译阶段,编译器就会报错  
    
    java: 对于add(int), 找不到合适的方法
        方法 java.util.Collection.add(java.lang.String)不适用
          (参数不匹配; int无法转换为java.lang.String)
        方法 java.util.List.add(java.lang.String)不适用
          (参数不匹配; int无法转换为java.lang.String)
    

    在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

    所以,综合上面信息,我们可以得到下面的结论。

    • 与普通的 Object 代替一切类型这样简单粗暴而言,泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力。它更符合面向抽象开发的软件编程宗旨。
    • 当具体的类型确定后,泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。所以说,它是一种类型安全检测机制,一定程度上提高了软件的安全性防止出现低级的失误。
    • 泛型提高了程序代码的可读性,不必要等到运行的时候才去强制转换,在定义或者实例化阶段,因为 Cache<String>这个类型显化的效果,程序员能够一目了然猜测出代码要操作的数据类型。

    二、泛型的使用

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

    2.1、泛型类

    泛型类是指泛型用于类的定义中,通过泛型可以完成对一组类向外提供同样的接口,最典型的也是我们最常用的就是各种集合器,如List、Map、Set。
    一个普通的泛型类:

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

    泛型的类型参数只能是类类型(包括自定义类),不能是简单类型。传入的实参类型需与泛型的类型参数类型相同,如定义是是Integer,则传入的参数必须是int型(会自动装箱为Integer),但不能传入其他类型如String。

    这里提个问题,定义的泛型类,就一定要传入泛型类型实参么吗?

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

    Generic generic = new Generic("String");
    Generic generic1 = new Generic(1);
    

    另外,泛型的定义一定要用<T>吗?其实,尖括号 <>中的 T 被称作是类型参数,用于指代任何类型。事实上,T 只是一种习惯性写法。
    但出于规范的目的,Java 还是建议我们用单个大写字母来代表类型参数。常见的如:

    • T 代表一般的任何类。
    • E 代表 Element 的意思,或者 Exception 异常的意思。
    • K 代表 Key 的意思。
    • V 代表 Value 的意思,通常与 K 一起配合使用。
    • S 代表 Subtype 的意思,文章后面部分会讲解示意。
      如果一个类被<T>的形式定义,那么它就被称为是泛型类。

    2.2、泛型接口

    泛型接口与泛型类的定义及使用基本相同。

    public interface List<E> extends Collection<E> {
        boolean add(E e);
    }
    

    当实现泛型接口时,如果未传入泛型实参,类定义与泛型接口定义一样,需带上泛型声明。

    public class ShapeList implements List<T> {
         //报错,cannot resole symbol 'T'
    }
    
    public class ShapeList<T> implements List<T> {
         //正确
    }
    

    当实现泛型接口的类,传入泛型实参时,则所有使用泛型的地方都要替换成传入的实参类型:

    public interface List<E> extends Collection<String> {
        boolean add(String e);
    }
    

    2.3、泛型方法

    在Java中,泛型方法的定义比泛型类和接口要复杂。

    泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。只有声明了 <T> 的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法

    /**
     * 泛型方法介绍
     * @param url
     * @param result 
     * @param <T> 只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法
     *           与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
     * @return T 返回值为T类型
     */
    public static <T> T doGetRequst(String url,Result<T> result){
    }
    

    上面是一个静态泛型方法。

    需要注意泛型类的普通方法和泛型方法的区别。

    public class Generic<T> {
        private T key;
    
        public Generic(T key) {
            this.key = key;
        }
    
        //这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
        public T getKey(){
            return key;
        }
    
        //编译器报错,Incompatible type, Require T,Found E
        //不能引用没有定义的类型参数
        public E setKey(E key){
            this.key = key;
        }
        
         //这才是泛型方法,在泛型类中定义的泛型方法
        public <T> T genericMenthod1(T t) {
           return t;
        }
        
        //编译器报错:cannot be referenced from a static context
        public static T genericMenthod2(T t) {
           return t;
        }
       
       //正确 静态方法需要声明泛型
        public static <T> T genericMenthod3(T t) {
          return t;
        }
    }
    

    泛型方法始终以自定义的类型参数为准。
    以上面的例子为例,方法genericMenthod中的T与类Generic中的T没有联系。

    Generic generic = new Generic<String>("1");
    generic.genericMenthod(1);
    
    

    泛型方法的总结:

    • 只有声明了 <T> 的方法才是泛型方法
    • 泛型类中的使用了泛型的成员方法并不是泛型方法
    • 静态方法无法访问泛型类中不能定义的类型参数
    • 泛型类中可以定义静态泛型方法,也可以定义泛型类方法,其参数类型与泛型类的参数类型没有联系。

    2.4、泛型数组

    在java中是 ”不能创建一个确切的泛型类型的数组” 的。

    List<String>[] ls = new ArrayList<String>[10];//不允许
    List<?>[] ls2 = new ArrayList<?>[10];//允许
    List<String>[] ls3 = new ArrayList[10];//允许
    

    下面采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。

    下面使用Sun的一篇文档的一个例子来说明:

    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 
    

    三、通配符

    除了用 <T>表示泛型外,我们还经常碰到 <?>这种形式的泛型, 被称为通配符。

    这里就有人提出疑问了,既然有了<T>,为什么还要<?>这种形式的泛型?

    我们再看一种应用场景。

    public interface Shape {
        int getShape();
    }
    
    public class Circle implements Shape{
        @Override
        public int getShape() {
            return 1;
        }
    }
    
    public class Triangle implements Shape{
        @Override
        public int getShape() {
            return 1;
        }
    }
    

    Circle和Triangle都实现了接口Shape,现在要打印列表中Shape的值,如下面方法,

    public static void showShapes(List<Shape> shapeList) {
        for (Shape shape : shapeList) {
            Log.e("shape", "" + shape.getShape());
        }
    }
    
    public static void testGeneric() {
        List<Circle> circleList = new ArrayList<>();
        circleList.add(new Circle());
    
        List<Triangle> triangleList = new ArrayList<>();
        triangleList.add(new Triangle());
    
        showShapes(circleList);//报错 Shape cannot applied to Circle
        showShapes(triangleList);//报错 Shape cannot applied to Triangle
    }
    

    上面调用showShapes方法报错的原因是:同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的

    在现实编码中,确实有这样的需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,对此 Java 引入了通配符这个概念。

    所以,通配符的出现是为了指定泛型中的类型范围

    另外,通配符<?>中的?是类型实参,不是类型形参。

    通配符有 3 种形式。

    1. <?>被称作无限定的通配符。
    2. <? extends T>被称作有上限的通配符。
    3. <? super T>被称作有下限的通配符。

    3.1、无限定通配符 <?>

    无限定通配符经常与容器类配合使用,它其中的 ? 其实代表的是未知类型,所以涉及到 ? 时的操作,一定与具体类型无关。

    public static void showShapes(List<?> shapeList) { 
    }
    

    <?>存在时,其实List 对象丧失了 add() 方法的功能,当调用时编译器不通过。

    public static void showShapes(List<?> shapeList) { 
        shapeList.add(new Circle());//报错 ? cannot applied to Circle
    }
    

    <?>提供了只读的功能,也就是它删减了增加具体类型元素的能力,只保留与具体类型无关的功能。它不管装载在这个容器内的元素是什么类型,它只关心元素的数量、容器是否为空。

    3.2、上界通配符<? extends T>

    上限通配符用于在方法中放宽对变量类型的限制。<? extends T> 代表类型 T 及 T 的子类。

    上面的方法改为:

    public static void showShapes(List<? extends Shape> shapeList) {
         for (Shape shape:shapeList){
             Log.i(TAG,"shape="+shape.getShape());
         }
    }
    
    showShapes(circleList);//可以运行
    showShapes(triangleList);//可以运行
    
    shapeList.add(null);//不报错
    shapeList.add(new Circle());//报错 Shape cannot applied to Circle
    

    可以对列表中的?对象进行操作了。但是,注意的是对于上界泛型列表,除空之外,我们不允许将任何对象添加到列表中。

    3.3、下界通配符<T super ?>

    上界泛型列表,无法添加对象,如果需要添加,可以用下界通配符实现。

    public static void showShapes(List<? super Shape> shapeList) {
        shapeList.add(new Circle());
        shapeList.add(new Circle());
    }
    

    3.4、PECS原则

    上下界通配符的使用应当遵循PECS原则:Producer Extends,Consumer Super。

    限定通配符总是包括自己

    上界类型通配符:add方法受限

    下界类型通配符:get方法受限

    如果你想从一个数据类型里获取数据,使用 ? extends 通配符

    如果你想把对象写入一个数据结构里,使用 ? super 通配符

    如果你既想存,又想取,那就别用通配符

    不能同时声明泛型通配符上界和下界。

    四、类型擦除

    泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。

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

    通俗地讲,泛型类和普通类在 java 虚拟机内是没有什么特别的地方。回顾文章开始时测试题:

    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。

    泛型信息被擦除了。

    可能有人会问,那么类型 String 和 Integer 怎么办?

    答案是泛型转译

    在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T>则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String>则类型参数就被替换成类型上限。

    4.1、类型擦除带来的局限性

    类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因。但也因为类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性。

    五、总结

    泛型在Java中的作用非常重要,本文详细介绍了Java泛型的类型及应用。泛型并不神秘,但是也有很多值得注意的地方。泛型能做的,普通代码也能实现,只是应用了泛型可以使代码更简洁更安全,提高了开发效率。而类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因,这是泛型的局限性。我还是要建议大家使用泛型,如官方文档所说的,如果可以使用泛型的地方,尽量使用泛型。

    关注V: “码农翻身记”,回复888,免费领取Android/Java高频面试题解析、进阶知识整理、图解网络、图解操作系统等资料。关注后,你将不定期收到优质技术及职场干货分享。

    相关文章

      网友评论

          本文标题:Java泛型的详细学习指南,全面总结

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