美文网首页
Java泛型原理到实战一文通

Java泛型原理到实战一文通

作者: JohnYuCN | 来源:发表于2021-07-22 17:11 被阅读0次

一. 泛型的本质是什么:

  1. 是写给编译器使用的“语法糖”
        List<String> list=new ArrayList<String>(); //<String>会被编译器看到,但会在字节码中删除
        list.add("abc");//编译成功:编译器对“abc” instaceof String检查,符合通过
//        list.add(100); //编译失败:编译器对100 instaceof String检查,不符合失败
        String s=list.get(0); //编程器将代码转换为:String s=(String)list.get(0); 再进行编译

而JDK6+的编译器将更加智能,第一行代码可以自动推测出对象实际类型的“泛型类型”

List<String> list=new ArrayList<>();
  1. 以下运行结果为真:
        ArrayList<Number> list1=new ArrayList<>();
        ArrayList<Object> list2=new ArrayList<>();
        boolean b=list1.getClass()==list2.getClass();//rs: true

没有新的Class,所有的泛型信息,都会在编译后进行“型别擦除”。

  1. 结论:
    (1) 泛型是在源代码中加入的语法,为编译器提供:(1)编译检查(2)源代码生成,这两大工作。
    (2) 扩展认知:“注解技术(Annotation)”实质上是对泛型技术的扩展,通过我们对源代码的“修饰”(加入@xxx),从而让编译器(进一步:可以是执行器)获取更多的“糖”,从而可以做到:语法检查(@Override)、源代码生成(@Getter)、运行特殊生命周期(@Autowared)等一系列的工作,而以上这些工作,只需要在源代码中修改,而不在需要使用繁琐的XML,从而使用Java以一种全新的姿态展现给开发者,即:后Java时代。
    (3) 回想下:junit,lombok,spring,mybatis,JPA,spirng-cloud等,都是在这种大技术背景下的产物。

二. 泛型的作用:

1. 可以让编译器提前检查 一些运行时异常:

List<String> list=new ArrayList<String>();
 list.add(100);//编译失败:

2. 让“数据类型参数化”:

(1)如下代码完成了一个通用的“结果类型”设计:

public class CommonResult<T> {
    private int code;
    private String message;
    //可以认为:T 是一种可变的数据类型(数据类型参数化)
    private T entity;
    public T getEntity(){
        return entity;
    }
    public void setEntity(T entity){
        this.entity=entity;
    }
}

如此设计:CommonResult将不会加为entity的数据类型有多种可能,而去设计出相应的CommonResult。
(2)结论:变量名称的参数化,让“方法的调用”与“方法的实现”在【变量名称】达到解藕的目的;变量类型的参数化,让“方法的调用”与“方法的实现”在【变量类型】达到解藕的目的。
(3)联想:java认为“数据类型”是“算法+数据结构”的结合体,但FaaS时代的迫切任务是:把“算法”独立成为一种数据类型,“Java的Lamda”就是为此而诞生(以前是使用接口完成,但太笨重了!!!,此论点与泛型关系不大,但需要注意会有Lamda类型参数的存在,同样可以结合泛型,达到更灵活的目的)

三、泛型语法施加的主体有哪些:

1. 泛型类:

以下形式都是泛型类,T、E、V代表着可变的数据类型,class中任何出现变量类型的地方都可以使用(域,形参类型,返回类型、局部变量类型、异常类型), 以下是一个相对极端的例子:

public class CommonResult<T,E,V extends Throwable> {
    private T entity;
    private E e;
    private V v;
    public CommonResult(T entity,E e,V v){
        this.entity=entity;
        this.e=e;
        this.v=v;
    }
    public E ProcessEntity(T entity) throws V{
        this.entity=entity;
        if(Math.random()>0.5) {
            throw v;
        }
        return e;
    }
...

要引起注意的是: T,E,V代表着未知的类型,所以在代码中不可以做任何有假设其类型的行为:如new(假设它是一个非抽象类),instanceof(假设它是某一类型)等操作,这也是使用泛型的副作用,没有银弹。

2. 泛型接口:

public interface WorkService<T> {
    T produce();
    void consume(T t);
}

3. 泛型方法:

    public <M> M process(M m){
        return m;
    }

以上代码理解:
(1) 此方法可以位于泛型或非泛型类中
(2)<M> 的作用有两个:一是声明此方法是泛型方法;二是此方法中可以使用M做为参数化类型的符号。
(3)常见的错误:认为在泛型类,使用了带有参数化类型的方法,就是泛型方法:

public interface WorkService<T> {
    T produce();//这是不是泛型方法,只是使用了参数化类型符号的方法
}

四、所谓的“形参”和“实参”问题:

在定义类、接口、方法时,相当于定义了数据类型的“形参”,传入方法总结如下:

1. 【声明变量类型】时传入实参:

List<String> list;
WorkService<Number> ws;

此时,泛型类或接口中所有的包含T(或其它)的变量声明将被Strring或Number替换,语法检查机制和代码生成机制将随之变化。
需要注意,并不需要在实例化时指定,目前的编译器可以智能的填入。

2. 【实现或继承时】传入实参:

public interface WorkService<T> {
    T produce();
    void consume(T t);
}

//=========实现或继承式传入"类型实参"==============//
class TomWrok implements WorkService<String>{
    @Override
    public String produce() {
        return null;
    }
    @Override
    public void consume(String t) {

    }
}

注意几点:
(1)此时,所有的替换工作必须由硬编码方式完成,
(2)TomWork类并不是泛型类,当然也可以根据需要将之变成泛型类,但此时和它的泛型接口r 的泛型类型已经没有任何关系了,因为已经确定为String类型了。

/**============================================
 * 实现或继承式传入"类型实参"
 * 继承将Tomwork变为泛型类,T只是符号,可以是任何字母
 * TomWrok确定了接口中实参,但又引入了新的形参
 ============================================**/

class TomWrok<T> implements WorkService<String>{
    private T t;
    @Override
    public String produce() {
        return null;
    }
    @Override
    public void consume(String t) {

    }
}

(3)以这种方式,只是将形参进行了传递,并没有实现,子类仍为泛型类

class Work<T> implements WorkService<T>{
    @Override
    public T produce() {
        return null;
    }

    @Override
    public void consume(T t) {

    }
}

3. 【方法调用时】传入:

此方式只是针对“泛型方法”:

 public static <M> M process(M m) {
        return m;
    }

    public static void main(String[] args) {
        String s = "abc";
        String rs = process(s);//由传入实参的声明类型做为实参
        Number m = new Integer(123);
        Number rm = process(m);//由传入实参的声明类型做为实参
        int i = process(123);//由传入实参的实际类型推断做为实参
    }

四、对“参数化类型”的模糊限定:

1. 原理:

(1) 我们的需要是:全方面的“IS 关系”的检查,但是目前编译器做不到!!!

Java编译器,可以对变量类型进行"IS关系"进行检查,但无法对”泛型类型“的变量中的参数化类型进行”IS关系检查“。即:List<Number> :只能对ArrayList IS List的检查,但是不能做Integer IS Number的检查。
上述问题:就是本节要讨论的解决方法,我们要进行【准全面检查】

原因是:List<String>和List<Integer>‘是同一个List class类型,这也称为类型擦除,这样编译器在进行”参数化类型的检查时“,就无法获取有效的信息;但实际上编译器可以做的更加高级,从源代码中获取类型,再进行递归方式的检查,但这样就会把编译器做的足够的复杂,导致编译速度过慢,从而导致基于编译器的工具链不具备生产价值。

Java在运行时无法对传入“由泛型类”生成的对象,进行“泛型类型”检查。即:List<String>和List<Integer>‘是同一个List class类型(这称为类型擦除)。
观察如下案例:

    /*
     * @param list:此时只能通过基于 "IS List"的检查,
     * 而不能通过"IS LIST && IS Number"的检查
     */
    public static void fn1(List<Number> list){

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

        ArrayList<Number> list1=new ArrayList<>();
        /**
         * 此时编译器有足够的"类型信息",对"IS List"进行检查,此时编译检查通过
         */
        fn1(list1);


        ArrayList<Integer> list2=new ArrayList<>();
        /**
         * 此时编译器有足够的"类型信息",对"IS List"进行检查,
         * 但没有足够的信息,对"IS Number"进行检查(因为型别擦除,同时现阶段编译器没有能力读到Number后进行检查)
         * 基于安全角度,此时编译不通过(信息不足,任可错杀)
         */

        //fn1(list2);
    }
2. 通配符闪亮登场:

泛型体系中设计了“?extends”,原理是对“参数化类型”进地适当的限定,从而使编译器适当的介入,达到在编译期读取足够多的信息,完成“准全方面的”的“IS”检查。
观察如下案例:

    public static void fn2(List<? extends Number> list){

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

        ArrayList<Integer> list2=new ArrayList<>();
        /**
         * 此时编译器有足够的"类型信息",对"IS List"进行检查,
         * 也有了足够的信息,对"IS Number"进行检查(不是从类信息查询,而是从源代码中读到了 extends Number后进行检查)
         * 此时:ArrayList IS List && Integer IS Number,编译通过
         */
        fn2(list2);

        ArrayList<String> list3=new ArrayList<>();
        //此时:ArrayList IS List 但是 String IS Not Number,编译不通过
//        fn2(list3);
...
  }

泛型体系又进而衍生出了一种更加“高级”的方式:“? super ”,完成了“参数化类型”的向上检查。
观察以下案例:


    public static void fn3(List<? super Number> list){

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

        ArrayList<Integer> list4=new ArrayList<>();
        //list4 is List;但是 Integer不是Number的祖先,编译不通过
        //fn3(list4);

        ArrayList<Object> list5=new ArrayList<>();
        //list5 is List && Object 是 Number的祖先,编译通过
        fn3(list5);
        ...
  }

五、使用通配符的副作用(建议把前面的熟悉之后再看):

没有银弹,在使用通配符时,我们会根据对参数的不同操作采用不同的?与 extends、super的组合,一般原则称为:PECS(Producer Extends Customer Super),案例如下:

1. Producer Extends :

    public static void fn2(List<? extends Number> list){
        /**
         * 进行消费类型的操作(传入对象),是不安全的
         * 由于使用extends,代表着"参数化类型"有上限,而无下限,
         * 此时可以使用ArrayList<BigDecimal> 类型的实参,从而导致运行时的异常,编译器为预期的错误而禁止
         */
//        list.add(100); //针对任何类型的参数,消费类型操作都无法进行,这是希望看到的结果

        /**
         * 进行生产类型的操作(产生该类型的对象),将是大体上安全的
         * 如果获取的引用声明是Number或父类型,是绝安全的
         * 如果是Number的子类型,需要做向下强转,但不一定安全
         */
        Number m1=list.get(0);
        Integer m2=(Integer) list.get(0);
        Object o1=list.get(0);//多态引用:Object ref到Number,没有问题

        /**
         * String并不在Number的继承树之下,这会在编译期中被检查出来,而无法通过
         * 这也是使用extends的原动力:即:Producer use Extends==PE
         */

        //String s1=(String)list.get(0); 编译无法通过,是希望看到的结果

    }


    public static void main(String[] args) {

        ArrayList<BigDecimal> list1=new ArrayList<>();
        fn2(list1);//此时fn2中如果使用list.add(100),将发生错误,所以编译器不允许通过
        ...
  }

2. Producer use Extends

    public static void fn3(List<? super Number> list){
        //传入的list可以是List<Number的祖先们>,所以无法直接由编译生成强转的代码,只能硬编码
//        Number number= list.get(0);
        Number number=(Number)list.get(0);//硬编码强转,提醒我们不应该在Producer中使用 super

        /**
         * 此时可以确认"参数化类型的下限是Number",此时传入fn3的list只能是:"IS LIST && ?是Number的祖先",
         * 形如:ArrayList<Object> 这样的类型,
         * 此时如下的操作都是安全的,
         */
        list.add(100);
        list.add(100.0);
        Number n=new Float(2.4);
        list.add(n);
        //Number的父类型,只能强转,提醒我们安全的消费类型是Number
        list.add((Number) new Object());
        /**
         * 由于String 并不是Number的祖先,所以编译器直接检出错误,
         * 即: Consumer use Super
         */
//        list.add("abc"); 这是我们希望看到的结果

    }


    public static void main(String[] args) {

        ArrayList<Number> list1=new ArrayList<>();
        fn3(list1);

        ArrayList<Object> list2=new ArrayList<>();
        fn3(list2);

相关文章

  • Java泛型原理到实战一文通

    一. 泛型的本质是什么: 是写给编译器使用的“语法糖” 而JDK6+的编译器将更加智能,第一行代码可以自动推测出对...

  • Java泛型教程

    Java泛型教程导航 Java 泛型概述 Java泛型环境设置 Java泛型通用类 Java泛型类型参数命名约定 ...

  • Kotlin 之 泛型

    参考: Kotlin 实战 Java 泛型推荐阅读:https://www.zhihu.com/question/...

  • Java泛型基础

    Java泛型基础 1. 认识泛型 泛型是在JDK1.5之后增加的新功能. 泛型可以解决数据的安全性问题, 主要的原...

  • 一文带你认识Java泛型基础

    Java泛型基础 1. 认识泛型 泛型是在JDK1.5之后增加的新功能. 泛型可以解决数据的安全性问题, 主要的原...

  • 【JAVA】浅谈Java范型

    1.Java泛型是什么?2.通常的泛型的写法示例3.类型擦除4.为什么要使用Java泛型5.通过示例了解PECS原...

  • Java 泛型

    Java 泛型是 Java 5 引入的一个重要特性,相信大多数 Java 开发者都对此不陌生,但是泛型背后的实现原...

  • 第二十八课:泛型

    泛型出现之前 泛型出现之后 Java深度历险(五)——Java泛型

  • 2019-10-19

    一文读懂Java泛型中的通配符 ?【转】 之前不太明白泛型中通配符"?"的含义,直到我在网上发现了Jakob Je...

  • Kotlin 泛型

    说起 kotlin 的泛型,就离不开 java 的泛型,首先来看下 java 的泛型,当然比较熟悉 java 泛型...

网友评论

      本文标题:Java泛型原理到实战一文通

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