美文网首页
Java泛型语法,原理,高级用法和局限

Java泛型语法,原理,高级用法和局限

作者: alonwang | 来源:发表于2020-02-22 11:42 被阅读0次

    前言

    泛型是现代编程语言很重要的组成部分,它的出现有两个目的:

    • 提供更严密的编译器类型检查
    • 支持泛型编程

    Java从JDK5开始添加了对泛型支持,本文将详述Java(JDK8)中泛型的语法,原理,高级用法和局限.

    正文

    泛型类和方法的定义

        //非严谨定义,仅表示基本结构
        /**
        *泛型类
        */
        public class class_name<T1, T2, ..., Tn> { /* ... */ }
        
        /**
        *泛型方法
        */
        public <T1,T2,...,Tn> void method_name(T1 t1,T2 t2,...Tn){
         /* ... */ }
        
        //eg.
        
        //创建单Parameter的泛型类
        class A<T>{ /* ... */ }
        //创建多Parameter泛型类
        class B<T,V>{ /* ... */ }
    
    
    ​    
        class A<T> {
                //使用类Type Parameter的方法
                public void method1(T t) {
                    /* ... */
                }
            //泛型方法
                public <R> void method2(R r){
                    /* ... */
                }
                //混合类Type Parameter和方法Type Parameter的方法
                public <R> void method3(R r, T t) {
                    /* ... */
                }
            }
    
    

    T1,T2,...Tn被称为 Type Parameter(类型参数,详见Type Parameter 和 Type Argument )

    泛型类和方法的使用

    Parameterized Types和Raw Types

    如果生成泛型类对象时指定了具体的Type Argument,就称之为Parameterized Types,例如下面这句就生成了Parameterized Types为Integer的ArrayList.

    new ArrayList<Integer>();
    

    如果生成时没有指定Type Argument,就称为 Raw Types,例如下标这句生成了Raw Types的ArrayLis

    //编译时会提示  Unchecked assignment: 'java.util.ArrayList' to 'java.util.List<java.lang.Integer>'
    List<Integer> list=new ArrayList();
    

    为什么会有Raw Types呢? 假设现在有一个JDK5之前编写的方法,在Raw Types的支持下,老代码无需修改就能获得泛型的便利,我们可以直接这样写,一方面减轻了用户更新JDK版本的难度,另一方面保证了JDK的兼容性.

    public List getList(){/*...*/}
    
    List<String> result=getList()
    

    基本用法

        /**
        *创建泛型类对象
        */
        new class_name()<>();
        
        /**
        *手动指定Type Argument的泛型方法
        */
        obj.<Type argument>method_name(args)
    
    
    ​    
        class A<T> {
                //使用类Type Parameter的方法
                public void method1(T t) {}
            //泛型方法
                public <R> void method2(R r){}
                //混合类Type Parameter和方法Type Parameter的方法
                public <R> void method3(R r, T t) {}
            }
        
        //创建泛型类对象
        A<String> a = new A<>();
        //1 Parameterized Types在泛型类对象创建的时候已经确定了,A的  Parameterized Types是String
        a.method1("abc");
        //2 Parameterized Types会根据传入argument的类型自动推导为String
        a.method2("abc");
        //3 显式指定了Parameterized Types,不再使用自动推导.签名为method1(Long t)
        a.<Long> method2(0L);
        //4 来自泛型类的Type Parameter由泛型类确定,方法自身的泛型参数由传入的argument决定
        a.method3(0, "abc");
    

    Java中的泛型实现原理

    Java中的泛型使用类型擦除方式实现,JVM并不知道泛型,所有的泛型在编译阶段就已经被处理成了普通类和方法,并体现在生成字节码上

    擦除原理代码演示

    public static void eraseTest() {
            List l1 = new ArrayList();
            l1.add("abc");
            l1.add(Integer.valueOf(123));
            String s1 = (String) l1.get(0);
            Integer i1 = (Integer) l1.get(1);
            List<String> l2 = new ArrayList();
            l2.add("abc");
            // IDE报错提示: 类型不符
            // l2.add(Integer.valueOf(123));
            String s2 = l2.get(0);
            List<String> l3 = new ArrayList<>();
            l3.add("abc");
            // IDE报错提示: 类型不符
            // l3.add(Integer.valueOf(123));
            String s3 = (String) l1.get(0);
        }
    
    //对应的字节码
    public static void eraseTest();
        descriptor: ()V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=7, args_size=0
             0: new           #2                  // class java/util/ArrayList
             3: dup
             4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
             7: astore_0
             8: aload_0
             9: ldc           #4                  // String abc
            11: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
            16: pop
            17: aload_0
            18: bipush        123
            20: invokestatic  #6                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
            23: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
            28: pop
            29: aload_0
            30: iconst_0
            31: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
            36: checkcast     #8                  // class java/lang/String
            39: astore_1
            40: aload_0
            41: iconst_1
            42: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
            47: checkcast     #9                  // class java/lang/Integer
            50: astore_2
            51: new           #2                  // class java/util/ArrayList
            54: dup
            55: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
            58: astore_3
            59: aload_3
            60: ldc           #4                  // String abc
            62: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
            67: pop
            68: aload_3
            69: iconst_0
            70: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
            75: checkcast     #8                  // class java/lang/String
            78: astore        4
            80: new           #2                  // class java/util/ArrayList
            83: dup
            84: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
            87: astore        5
            89: aload         5
            91: ldc           #4                  // String abc
            93: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
            98: pop
            99: aload_0
           100: iconst_0
           101: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
           106: checkcast     #8                  // class java/lang/String
           109: astore        6
           111: return
    

    使用绑定的类型(或Object)替换泛型中的Type Parameter,这样生成的字节码中,只包含原始的类,接口和方法

    首先看 0~4,51~55,80~84 三种创建泛型类对象的方式生成的字节码是相同的,都是

    // Method java/util/ArrayList."<init>":()V
    

    再看11,62,93 ,add方法的签名也都相同,参数都是Object而非具体的Type Argument

    InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
    

    为了保证类型安全,在必要的地方加入类型转换

    36,75,106中 36对应的字节码checkcast 是由源代码编译出的,而75,106都是编译器对泛型处理后自动生成的

    在有继承的泛型类中使用桥接方法(bridge methods)保证多态

    class A {
            public <T> void method(T t) {
    
            }
    
        }
    
        class B extends A {
            @Override
            public <T> void method(T t) {
    
            }
        }
    //类A对应字节码
    public void method(T);
        descriptor: (Ljava/lang/Object;)V
        flags: ACC_PUBLIC
        Code:
          stack=0, locals=2, args_size=2
             0: return
          LineNumberTable:
            line 15: 0
        Signature: #14                          // (TT;)V
    
    //类B对应字节码
    public void method(java.lang.String);
        descriptor: (Ljava/lang/String;)V
        flags: ACC_PUBLIC
        Code:
          stack=0, locals=2, args_size=2
             0: return
          LineNumberTable:
            line 23: 0
      //桥接方法
      public void method(java.lang.Object);
        descriptor: (Ljava/lang/Object;)V
        flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
        Code:
          stack=2, locals=2, args_size=2
             0: aload_0
             1: aload_1
             2: checkcast     #3                  // class java/lang/String
             5: invokevirtual #4                  // Method method:(Ljava/lang/String;)V
             8: return
          LineNumberTable:
            line 19: 0
    

    泛型擦除后,A中method签名为 method(Object) 而B继承Parameterized Types为String的A后,重写的method签名为method(String),这就导致多态出现问题, 为了解决这个问题,编译器会自动生成一个桥接方法, 签名为 method(Object),这个方法内部去调用B重写的method. 保证多态.

    高级用法

    定义时限定Type Parameter上界

    限定上界,extends后如果跟随多个,第一个必须为类或抽象类,后面的必须为接口,使用时Type Argument 必须同时满足所有限定条件

    /**
    *限定上界的类定义
    */
    class class_name <T extends class_or_ abstract_class_name&interface_name&interface_name>
    /**
    * 限定上界的方法定义
    */
    public <T extends class_or_ abstract_class_name&interface_name&interface_name> void method2(T t)
    
    //eg.
    class C<T extends Number> extends ArrayList<T> {
    
    }
    
    public <T extends Number & Serializable> void method2(T t) {
    
    }
    

    没有限定下届的语法,个人认为是意义不大,所以没有.

    通配符?在泛型中的应用

    PECS(Producer Extends Consumer Super)规则

    1. 如果一个对象频繁向外读取数据的,就被称为 "in" ,适合用上界 extends
    2. 如果一个对象经常向里插入数据,就被称为"out"适合用下界super

    场景设定: Plate可以装载物品, Food,Fruit,Apple,Banana有继承关系.

    static class Plate<T> {
            private T t;
    
            public T getT() {
                return t;
            }
    
            public void setT(T t) {
                this.t = t;
            }
        }
    
        static class Food {
    
        }
    
        static class Fruit extends Food {
    
        }
    
        static class Apple extends Fruit {
    
        }
    
        static class Banana extends Fruit {
    
        }
    

    限定上界

    Image-extend.png
    Plate<Apple> applePlate = new Plate<>();
    applePlate.setT(new Apple());
    Plate<? extends Fruit> p = applePlate;
    // 编译错误,类型不符
    // p.setT(new Apple());
    Fruit f = p.getT();
    f.printName();
    

    不能存,只能取, 且取出的是上界对应的对象. <? extends Fruit>只知道里面存的是Fruit或Fruit的子类,标记为#A,具体是什么不知道,想插入数据时不知道和#A是否匹配,所以存不进去,取出时同理,只能当做Fruit取出.

    限定下界

    Image-super.png
    Plate<? super Fruit> plate = new Plate<>();
    plate.setT(new Fruit());
    Object obj = plate.getT();
    ((Fruit) obj).printName();
    

    能存,存的必须是Fruit或Fruit的直接/间接父类, 可以取,因为Object是所有类的基类,因此只能当做Object取

    无界

    Plate<Apple> applePlate = new Plate<>();
    applePlate.setT(new Apple());
    Plate<?> plate = applePlate;
    Object obj=plate.getT();
    ((Apple) obj).printName();
    

    不能存,只能取,且取出时只能为Object.它有两个用途

    • 泛型类仅用到Object中定义的方法, 例如 Object.hashcode().
    • 泛型类不依赖Type Parameter的类型.

    <?>和<Object>是有差别的,<?>只能取,不能存, 而<Object>是可以作为Object存的.

    为什么用了通配符?某些场景下不能存,某些可以

    上面讲过类型擦除为了保证类型安全,在必要的地方加入类型转换,这就要有有一个确定的类型,<? extends X> 可以确保是X或X的子类,由于类的继承原理: X的子类可以强转为X,添加强转是使用X即可.因此可以存,而<? super X>和<?> 都没有确定的类型,都表示一个范围,因此不能存.

    泛型的继承关系

    泛型不会继承普通类的继承关系

    Untitled.png

    要完成泛型中的继承,需要使用 ?, ?在继承中对泛型的作用类似于Object.

    Untitled 1.png

    擦除的弊端

    上面说过,擦除法是在编译进阶擦除类型信息,这也就导致运行时获取不到类型信息.

    • reifiable type: 运行时全部信息都可以获取到的types,包含primitive,no-generic,raw(未指定实际类型的泛型),无界类型(如List<?>)
    • no-reifiable type: 信息已在编译时因类型擦除被移除的类型,例如List<String>和List<Number>,JVM无从得知两者运行时的差别,很多场景下 non-reifiable是无法使用的

    泛型弊端演示

    • 无法编译的<?>

    下面这段代码是无法编译通过的,原因上面的完全wildcard讲过

    void foo(List<?> i) {
            i.set(0, i.get(0));
        }
    

    要解决这个问题,可以添加一个辅助方法

    void foo(List<?> i) {
            forHelp(i);
        }
    
        <T> void forHelp(List<T> i) {
            i.set(0, i.get(0));
        }
    
    • WildcardErrorBad
        void swapFirst(List<? extends Number> l1, List<? extends Number> l2) {
                Number temp = l1.get(0);
                l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
                // got a CAP#2 extends Number;
                // same bound, but different types
                l2.set(0, temp);        // expected a CAP#1 extends Number,
                // got a Number
            }
    

    这边这段代码从语法上就是错误的,但是报错信息可能会让人迷惑,现在假设有这些数据

        List<Integer> l1 = Arrays.asList(1, 2, 3);
        List<Double>  l2 = Arrays.asList(10.10, 20.20, 30.30);
        swapFirst(l1, l2);
    

    很明显, l2中的数据是Double类型,而l1中的数据时Integer类型,当然无法set.

    • Cannot Instantiate Generic Types with Primitive Types原始类型无法作为泛型实例化Type
        //compile error!
        List<int,int> il
    
    • Cannot Create Instances of Type Parameters无法创建Type Parameters的实例
        public static <E> void append(List<E> list) {
            E elem = new E();  // compile-time error
            list.add(elem);
        }
    

    一种可行的解决方案

        public static <E> void append(List<E> list, Class<E> cls) throws Exception {
            E elem = cls.newInstance();   // OK
            list.add(elem);
        }
    
    • Cannot Declare Static Fields Whose Types are Type Parameters不能声明泛型静态域
        //compile error
        public class MobileDevice<T> {
            private static T os;
    
            // ...
    
        }
    
    • Cannot Use Casts or instanceof With Parameterized Types不能对Parameterized Type使用cast或instanceof
        //ArrayList<Integer>, ArrayList<String> ,LinkedList<Character>都可能被传递进去,而runtime并不会追踪Type Parameters
        public static <E> void rtti(List<E> list) {
            if (list instanceof ArrayList<Integer>) {  // compile-time error
                // ...
            }
        }
        
        // compile-time error
        List<Integer> li = new ArrayList<>();
        List<Number>  ln = (List<Number>) li;  
        
        
        //OK
        List<String> l1 = ...;
        ArrayList<String> l2 = (ArrayList<String>)l1;
    
    • Cannot Create Arrays of Parameterized Types不能创建Parameterized Type的数组,编译期被禁止,因为运行时无法分辨
        Object[] stringLists = new List<String>[2];  // compiler error, but pretend it's allowed
        stringLists[0] = new ArrayList<String>();   // OK
        stringLists[1] = new ArrayList<Integer>();  // An ArrayStoreException should be thrown,
                                                    // but the runtime can't detect it.
    
    • Cannot Create, Catch, or Throw Objects of Parameterized Types不能创建,捕捉或抛出Parameterized Type
        // Extends Throwable indirectly
        class MathException<T> extends Exception { /* ... */ }    // compile-time error
        
        // Extends Throwable directly
        class QueueFullException<T> extends Throwable { /* ... */ // compile-time error
        
        public static <T extends Exception, J> void execute(List<J> jobs) {
            try {
                for (J job : jobs)
                    // ...
            } catch (T e) {   // compile-time error
                // ...
            }
        }
        
        
        class Parser<T extends Exception> {
            public void parse(File file) throws T {     // OK
                // ...
            }
        }
    
    • Cannot Overload a Method Where the Formal Parameter Types of Each Overload Erase to the Same Raw Type不能重载擦除后raw type相同的方法
        // compile error
        public class Example {
            public void print(Set<String> strSet) { }
            public void print(Set<Integer> intSet) { }
        }
    

    后记

    写这篇文章的起因是在做一个需求的时候,同事用泛型很好的封装了一个基础组件,优雅简洁,我却完全没有想到,还在傻傻的强转类型转换. 很早之前我就研究过泛型并且做了笔记, 趁这次机会回顾实践一下并写了这篇文章.


    官方文档

    JVM如何理解Java泛型类(转)

    相关文章

      网友评论

          本文标题:Java泛型语法,原理,高级用法和局限

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