美文网首页
从0开始学习java泛型

从0开始学习java泛型

作者: 胜舟 | 来源:发表于2021-02-19 22:31 被阅读0次

    泛型是java中一个很重要的概念,虽然我们平时可能很少用上,但不代表就不需要学习。其实很多牛掰的框架模块,里面都经常使用泛型,随便点开几个源码就能看到了。

     

    1.什么是泛型?

    平时我们很少会用到泛型,但是它是无处不在的,例如随便打一个List,就能看到使用了泛型的类。简单来说我们时不时看到的那些尖括号包裹的,单独一个大写字母代替了具体类型的地方,就是泛型。

    image-20210219213952493

    泛型,即“参数化类型”,是在JDK5的时候推出的,就是将具体的类型参数化(类型形参),在使用和调用时才知道它的具体类型(类型实参)。这种参数类型可以用在类、接口和方法中,分别称为泛型类、泛型接口、泛型方法

     

     

    2.为什么需要泛型?

    说起为什么会需要泛型,就必须举一个集合的例子了

    首先看这段代码:

            List list = new ArrayList();
            //一个List集合中添加了字符串和整数两种不同类型的数据
            list.add("qqyumidi");
            list.add(100);
            for (int i = 0; i < list.size(); i++) {
                String name = (String) list.get(i); 
                System.out.println("name:" + name);
            }
    

    结果会抛出ClassCastException: java.lang.Integer cannot be cast to java.lang.String异常,因为类型转换失败了嘛,但是在编译时不会察觉到有任何问题(当然现在牛逼的IDEA已经有一定的提示了,但是终究只是提示,无法在编译前标红报错)。

    没有泛型的情况下,我们将一个对象加入集合中,集合不会记住它的类型,也就是统一用Object存储的。所以在取出时,我们不得不一个一个的进行类型转换。而一旦进行了类型转换,如果整个集合的数据都是同一个类型还好,万一加入了一个不一样的类型,立马就是报错ClassCastException。

    为了让这种问题能在编译时就暴露出来,在运行时不会发生ClassCastException异常,泛型应运而生。

     

    使用泛型后的集合

    既然泛型能解决这个问题,我们就看看泛型是如何解决的,还是这段代码,只是在初始化List时指定了类型

            List<String> list = new ArrayList<>();
            //添加了字符串和整数两种类型的数据
            list.add("qqyumidi");
            list.add(100);
            for (int i = 0; i < list.size(); i++) {
                String name = list.get(i);
                System.out.println("name:" + name);
            }
    

    无需启动,在编译时就已经提示list.add(100)是标红报错的了,通过指定类型,直接限定了List集合中只能装String类型的元素。既然无法添加其他类型,自然在取出来的时候也不用类型转换了。

     

     

    3.泛型的使用

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

    ①泛型类

    直接用例子来看会更快,先创建一个普通的泛型类:

    package com.lzh;
    public class Box<T> {
        private T data;
        public Box(T data) {
            this.data = data;
        }
        public T getData() {
            return data;
        }
    }
    

    就是个普通的类,唯一不同的是类名后面加了个<T>,并且类中的属性和方法,原本应该是具体类型的地方都用T代替了,这个T就是泛型。关于这个泛型有很多不同的字母,什么E、V、K、T,其实区别不大,具体我会放到后面再说。

    然后对这个泛型类初始化不同的类似参数,看看它们的类名会不会有什么不同。

    package com.lzh;
    public class Test {
        public static void main(String[] args) {
            Box<String> stringBox = new Box<>("giao");
            Box<Integer> integerBox = new Box<>(250);
            System.out.println("stringBox class:" + stringBox.getClass());
            System.out.println("integerBox class:" + integerBox.getClass());
            System.out.println(stringBox.getClass() == integerBox.getClass());
        }
    }
    

    结果是二者并没有任何区别,不管它泛型的实际参数填的是什么,它这个类都是Box类。并且我们发现,它明明只有一个构造方法,却可以传字符串或整数都可以成功构造对象。

    这样我们可以看出,实际上这个泛型和我们平时普通的new对象一样,唯一的特别之处就是用尖括号传了一个“参数”<Integer>,并且将泛型类里所有的泛型位置(字母T那些)都替换成了传入的参数。

    所以我们也无需把泛型类想的太复杂,就当是在new对象的同时还需要额外用尖括号传一个参数给它而已。

    注意:另外泛型的类型只能是引用类型(也就是对象类型),不能是简单类型如int、long之类的。

     

    ②泛型接口

    泛型接口与泛型类的定义和使用基本相同,还是看例子说话

    创建一个十分简单的接口,只定义了一个简单方法。

    package com.lzh;
    public interface Generator<T> {
        public T getInfo();
    }
    

    但是当类实现它时,就有了两种不同的情况:

    情况1:在实现泛型接口时不传入泛型实参,那么这个类与泛型类的定义是相同的,还是相当于一个泛型类

    package com.lzh;
    public class WaterGenerator<T> implements Generator<T> {
        @Override
        public T getInfo() {
            return null;
        }
    }
    

    情况2:在实现泛型接口时传入泛型实参,那就只是实现了一个接口的普通类,在它的眼里这个接口中方法所有的T泛型都用传入的实参String代替了,所以类中实现的方法也是代替后的。可以说这个类就是个普通的类,和泛型没有关系了。

    package com.lzh;
    public class WaterGenerator implements Generator<String> {
        @Override
        public String getInfo() {
            return null;
        }
    }
    

     

    ③泛型方法

    这个泛型方法有个误区,有些人会认为在泛型类里面带T的就是泛型方法,其实并不是,包括前面我们在泛型类里的方法,那都不是泛型方法,只能说是泛型类里的方法。

    不好区分,但是泛型方法也有个标识性的区别:public与返回值之间有个<T>。

    这样看起来,尖括号才是泛型的代名词嘛,如果没有<T>,那么泛型类中的方法的T都会标红,因为没有叫T的类。可以认为<T>就是对泛型的声明,只有标注了<T>,才可以在里面使用T。

    泛型方法也是,泛型方法并不依靠泛型类,那么声明的<T>就要加在方法上面,例如:

    package com.lzh;
    public class Person {
        public <T> T getSelf(T param) {
            return param;
        }
    }
    

    这样我们就可以很容易的区分是泛型方法还是泛型类里的方法了。

    另外,静态方法无法访问类上的泛型,所以静态方法如果要使用泛型,即便是在泛型类中,也要用泛型方法的方式。

     

    ④泛型通配符的区别

    我们在定义泛型时,经常会看到不同的通配符,比如T,E,K,V等等,它们有什么区别呢?

    其实它们都是通配符,对java来说并没有什么区别,甚至我们可以自己从A-Z中挑一个用都无所谓的,不会影响程序和泛型的使用。

    这只是一种约定俗成的东西,就像驼峰命名变量一样,就算我们不遵守也不会对程序的运行产生影响,但是好的程序员都是会遵守这个约定的。

    T:Type,表示具体的java类型

    K V:Key Value,表示java键值对中的Key和Value

    E:Element,表示集合中的元素

    N:Number,表示数值类型(Integer和Long那些)

    ?:表示不确定的java类型

    然后这里有个巨坑的地方,就是T和?的区别,因为A-Z和T是一样的,但是T和?却并非一种东西。

    T是确定的java类型,?是不确定的java类型。

    如果实在要说明一个区别,那么最大的区别就是,问号大多是用在泛型方法上的形参的,不能用于定义泛型类和泛型方法

    简单说明一下我自己对它们的理解:

    T和?最大的区别就是一致性,T是已知的类型,在一个泛型类中,只要我们声明了T,那么在使用这个类时,它里面所有T的位置都会被我们传入的类型参数替换,不会出现不一致的情况。泛型方法也是如此,所以我们可以在方法内部使用T的类型,如果是集合类型,也可以单独取出每个对象赋给T类型:

        public <T> T getSelf(T param) {
            T t = param;
            return t;
        }
    

    但是?就不行了,它表示未知的类型,所以我们无法在方法中定义?类型,只能操作它作为形参的对象。既然是未知的类型,那么它也就不能用在泛型类上声明泛型。

     

    感觉理解的还有有些模糊,但我们只需要知道问号最常用的地方是下面的场景就好了

    ⑤通配符边界

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

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

    当我们需要用一个方法接收List集合的参数,而这个集合中的元素可以是一种类的子类或父类时,终于就可以用上?了。

    先用例子说明一下场景,假设我们有一个泛型类,其中有这样一个方法:

        public void doWord(List<Animal> animal)
    

    它接收了List<Animal>的参数,我们先不管它做了什么,现在我们需要传List<Dog>进去调用,Dog是Animal的子类。

    这时就会发现失败了,虽然List还是同一个List,但由于泛型不同,所以不能调用,尽管它们是父子类。

    这时候就可以利用问号和边界关键字了:

        public void doWord(List<? extends Animal> animal)
    

    通过这种方式,我们就可以传入List<Dog>并执行方法了,super关键字用法也是类似,只是范围是指定类的父类。

     

    ⑥T和?的其他区别

    T和?的区别这块我到最后都有点晕,只能尽可能整理它们的区别了。

    (1)T没有super关键字

    (2)T的extends XX类和? extends XX类效果是类似的,但是T需要提前定义好泛型,无论是泛型类还是泛型方法都要定义好,而?是随处都可以用的。

    (3)一个方法中的T都是统一的,不管有几个T,在运行时都是同一个类,而多个?并不统一,可以相互不一样。

    (4)T用于泛型类和泛型方法中的定义,?用于方法中声明或参数。(声明是声明变量)

     

     

    *4.类型擦除

    关于类型擦除,具体可以了解一下这篇,我写的也只是相当于读后感——

    Java泛型类型擦除以及类型擦除带来的问题:https://www.cnblogs.com/wuqinglong/p/9456193.html

    ①java泛型的实现方法:类型擦除

    java的泛型是伪泛型,因为java在编译期间,所有的泛型信息都会被擦除掉。

    在代码里定义的List<Object>或List<String>类型,在编译后都会变成List。

    如何证明这点呢?

    前面我们有用过一个例子,new两个不同泛型的数组,然后getClass比较会发现它们是同样的类,这就是一种证明方式。证明泛型类型都被擦除了,只剩下原始类型。

    另一种方式是定义一个ArrayList<Integer>数组,按理说它是不能添加字符串类型的元素的,但当我们用反射的方式添加时,会发现它可以存储字符串了,说明Integer泛型实例在编译后被擦除掉了。

     

    ②原始类型是什么

    被擦除后的类变成了原始类型,而这个原始类型通常就是Object,可以理解为在编译时泛型类里面所有的T都用Object进行了替换,所以我们用反射的方式可以添加不同类型的数据。

    而如果是有边界的泛型,例如Pair<T extends Comparable>,那么它的原始类型就是边界本身Comparable。

     

    ③类型擦除引起的问题和解决方法

    因为一些原因,Java不能实现真正的泛型,只能用类型擦除来实现伪泛型,这样虽然不会有类型膨胀问题,但是也会引来一些新问题,所以SUN对这些问题做出了一些限制,避免我们发生错误。

    例如java编译器在进行泛型擦除前,会先检查代码中泛型的类型,避免我们在ArrayList<Integer>中添加String元素。

    通过这个例子可以更好的理解这个检查:

    public class Test {  
        public static void main(String[] args) {  
            //普通的声明一个泛型ArrayList
            ArrayList<String> list1 = new ArrayList();  
            list1.add("1"); //编译通过  
            list1.add(1); //编译错误  
            String str1 = list1.get(0); //返回类型就是String
            //声明时不用泛型,new的时候才是有泛型的ArrayList
            ArrayList list2 = new ArrayList<String>();  
            list2.add("1"); //编译通过  
            list2.add(1); //编译通过  
            Object object = list2.get(0); //返回类型是Object,和String毫无关联
            
            new ArrayList<String>().add("11"); //编译通过  
            new ArrayList<String>().add(22); //编译错误  
            String str2 = new ArrayList<String>().get(0); //返回类型是String  
        }  
    }  
    

    通过上面的例子,我们可以明白,类型检查是针对变量类型的,如果变量声明时没有声明泛型,那么默认就是Object,不管它真正new出来的类型有没有声明泛型。

    个人理解是java在编译时会把所有的T替换成Object,同时存储泛型的类型,在反射或取值出来时会通过存储的泛型类型进行强制转换,以便我们直接使用。

     

     

     

    参考资料:

    java 泛型详解:

    https://www.cnblogs.com/coprince/p/8603492.html

    聊一聊-JAVA 泛型中的通配符 T,E,K,V,?:

    https://juejin.im/post/5d5789d26fb9a06ad0056bd9

    Java泛型(一)类型擦除:

    https://www.jianshu.com/p/2bfbe041e6b7

    相关文章

      网友评论

          本文标题:从0开始学习java泛型

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