美文网首页
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泛型原理到实战一文通

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