美文网首页
泛型(Generic)总结(三)

泛型(Generic)总结(三)

作者: 白花蛇草可乐 | 来源:发表于2019-08-17 16:51 被阅读0次

    六、泛型擦除

    6-1、什么是泛型擦除

    泛型这个概念,只存在于编译器中。而不存在于虚拟机(JVM)中。

    意思是说,编译器对带有泛型的java代码进行编译时,会去执行类型检查和类型推断,然后生成普通的不带泛型的字节码,供JVM接收并执行。

    这个过程就叫做泛型擦除。

    下面通过反射,向List<String>类型的容器中添加Integer元素,来证明:只要能想办法绕开编译器检查,泛型的约束?不存在的

        public static void main(String... args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
            List<String> strList = new ArrayList<>(3);
            strList.add("A1");
            strList.add("B2");
            // 这样写是一定会报编译错的: strList.add(333);  所以使用反射
            strList.getClass().getMethod("add",Object.class).invoke(strList,333);
            strList.forEach(System.out::println);
        }
    
    

    在最后一句打上断点,会发现333作为Integer类型已经被成功添加到了strList里面,但是在print的时候,仍然会报类型转换异常:java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

    那么就仍然使用反射的方法绕开泛型的类型检查

            Integer c = (Integer)strList.getClass().getMethod("get",int.class).invoke(strList,2);
            System.out.println(c);
            
    运行结果:333        
    
    小结:对于JVM来说,泛型信息是不可见的。

    6-2、擦除的过程

    Java 编译器会将泛型代码中的类型完全擦除,使其变成原始类型。

    然后在代码中加入类型转换,将原始类型转换成想要的类型。

    这些操作都是编译器在后台进行的,以保证类型安全。

    所以说泛型就是一个语法糖,对于实际运行不产生任何影响。

    看一个例子:

    public class ErasureTest<T> {
    
        private T t;
    
        public T getT() {
            return t;
        }
    
        public void setT(T t) {
            this.t = t;
        }
    
        public static void main(String... args) {
            ErasureTest<String> a = new ErasureTest<>();
            a.setT("abc");
            System.out.println(a.getT());
        }
    
    }
    

    使用 javap -c 命令查看这段代码的字节码

    (javap用法点这里)

      public T getT();
        Code:
           0: aload_0
           1: getfield      #2                  // Field t:Ljava/lang/Object;
           4: areturn
    
      public void setT(T);
        Code:
           0: aload_0
           1: aload_1
           2: putfield      #2                  // Field t:Ljava/lang/Object;
           5: return
    
      public static void main(java.lang.String...);
        Code:
           0: new           #3                  // class com/puhuijia/helloStudy/ErasureTest
           3: dup
           4: invokespecial #4                  // Method "<init>":()V
           7: astore_1
           8: aload_1
           9: ldc           #5                  // String abc
          11: invokevirtual #6                  // Method setT:(Ljava/lang/Object;)V
          14: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
          17: aload_1
          18: invokevirtual #8                  // Method getT:()Ljava/lang/Object;
          21: checkcast     #9                  // class java/lang/String
          24: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
          27: return
    

    注意看main()方法

    标号9的那一行,abc被创建出来时是String,但是马上就开始按照Object进行处理。

    标号21以前的处理,使用的类型都是Object。

    到了标号21的那一行,进行了“checkcast”,将Object进行了强制类型转换,变成String。

    (checkcast checks that the top item on the operand stack (a reference to an object or array) can be cast to a given type)

    总之,这再度印证了:所有的活都是Object干的;强制类型转换是打死也躲不开的;泛型只是让你舒服一些,把所有脏活累活都藏起来了。

    6-3、java擦除的特点

    • C++ 中泛型的实例化会为每一种类型都产生一套不同的代码,这就是所谓的代码膨胀。

    • java 中并不会产生这个问题。虚拟机中并没有泛型类型对象,所有的对象都是普通类。

    java 不同类型都能使用一套代码,就是因为采用了泛型擦除机制,字节码中根本就没有类型。

    实际上,擦除机制的出现,主要目的是为了JDK新老版本在泛型上的兼容性问题。

    6-4、擦除带来的一些问题

    6-4-1、类型信息丢失

    由于泛型擦除机制的存在,在运行期间无法获取关于泛型参数类型的任何信息,自然也就无法对类型信息进行操作;例如:instanceof 、创建对象等

    这是“4-1、不能实例化类型变量”的原因

    6-4-2、类型擦除对于多态的影响

    看下面这个例子,正常来说,这两个方法的参数不同,应该被辨识成重载,但是编译器报错:

        void method(List<Integer> a) {
        }
    
        void method(List<String> b) {
        }
        
    Error:java: name clash: method(java.util.List<java.lang.String>) and method(java.util.List<java.lang.Integer>) have the same erasure    
    

    错误信息是说两个方法的参数在擦除之后完全一致(have the same erasure),都是List,所以就不是重载,而是产生了冲突。

    6-4-3、泛型在父类子类继承时造成的一个影响
    6-4-3-1、问题提出

    首先创建一个简单的使用泛型的父类:

    public class GsuperClass<T> {
        private T t;
        public T getT() {return t;}
        public void setT(T t) {this.t = t;}
    }
    

    然后子类:

    public class GchildClass extends GsuperClass<String>{
        private String childString;
        @Override
        public String getT() {
            return this.childString;
        }
        @Override
        public void setT(String s) {
            this.childString = s;
        }
    }
    

    现在一个让人疑惑的问题是:子类中的getT和setT真的是重写?(Override)

    编译器认为这两个方法是重写,因为不加@Override注解的话会直接报警。

    但是,以set方法为例,父类中的setT(T t)经过类型擦除以后是setT(Object t);

    子类中的set方法参数是String类型,也就是说方法名相同但是参数不同,这难道不算是重载?(overloading)

    6-4-3-2、正常情况下的表现

    普通类的话,下面这样显然是合法的:

    public class CommonClass {
        public void setT (Object t){System.out.println("object");}
        public void setT (String s){System.out.println("String");}
        public static void main(String[] args) {
            CommonClass c = new CommonClass();
            c.setT(new Object());
            c.setT("123");
        }
    }
    
    结果:
    object
    String
    Process finished with exit code 0
    

    推及到继承上面,如果在一个普通的父类里面定义

    public void setT(Object t) {this.t = t;}
    

    在其子类里面定义

    public void setT(String t) {this.childString = t;}
    

    的话,也是完全行得通的,子类就拥有了两个setT方法(重载)。

    6-4-3-3、分析

    但是,在使用了泛型以后就完全不同了。如果尝试在 GchildClass 里面调用我认为有可能存在的重载方法时,编译直接报错:

    public class GchildClass extends GsuperClass<String>{
        private String childString;
        @Override
        public String getT() {
            return this.childString;
        }
        @Override
        public void setT(String t) {
            this.childString = t;
        }
        public static void main(String[] args) {
            GchildClass child = new GchildClass();
            child.setT("123");
            child.setT(new Object()); // ERROR:参数与方法类型不匹配
            System.out.println(child.getT());
        }
    }
    

    下面分析子类字节码,看看编译器和jvm到底干了什么见不得人的交易。

    public class GchildClass extends GsuperClass<java.lang.String> {
      public com.puhuijia.quartz.base.GchildClass();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method com/puhuijia/quartz/base/GsuperClass."<init>":()V
           4: return
    
      public java.lang.String getT();
        Code:
           0: aload_0
           1: getfield      #2                  // Field childString:Ljava/lang/String;
           4: areturn
    
      public void setT(java.lang.String);
        Code:
           0: aload_0
           1: aload_1
           2: putfield      #2                  // Field childString:Ljava/lang/String;
           5: return
    
      public void setT(java.lang.Object);
        Code:
           0: aload_0
           1: aload_1
           2: checkcast     #10                 // class java/lang/String
           5: invokevirtual #6                  // Method setT:(Ljava/lang/String;)V
           8: return
    
      public java.lang.Object getT();
        Code:
           0: aload_0
           1: invokevirtual #8                  // Method getT:()Ljava/lang/String;
           4: areturn
    }
    
    

    生成了两套get、set方法。

    而且参数是Object的调用了参数是String的方法。

    这里实际上使用了桥接模式,相当于jvm自己暗地里完成了对于setT(Object)的重写。

    6-4-3-4、彩蛋:关于重载的定义问题

    java编译器对于重载的定义不包括返回值。

    也就是说两个方法名、参数列表一致的方法,不管返回值是什么,都不可以同时存在,不视为重载;

    但是对于jvm来说,上例中存在两个 getT: String getT() 和 Object getT(),

    这显然不符合java的语法定义,但是却符合jvm标准。

    6-4-4、用泛型擦除来解释 “4-5、泛型类不能继承exception”

    如果以下代码可以通过

    public class GenericException<T> extends Exception {
    }
    

    那么就会出现这样的情况:

    try{
    }catch(GenericException<String> e1){
    }catch(GenericException<Integer> e2){
    }
    

    泛型擦除以后,两个catch就都会变成 GenericException<Object>,因此规定泛型类不能继承exception

    6-4、擦除导致的泛型不可变性

    对于泛型来说,其相同的容器类之间不存在任何的父类子类关系。

    也就是说:

    1. 不管 class A extends B ; 还是 class B extends A

    2. List<A>与List<B>之间不存在任何父类子类关系。

    这称之为不可变性。

    与不可变性相对应的概念是 协变、逆变:

    • 协变:如果 A 是 B 的父类,并且 A 的容器(比如 List< A>) 也是 B 的容器(List< B>)的父类,则称之为协变的(父子关系保持一致)

    • 逆变:如果 A 是 B 的父类,但是 A 的容器 是 B 的容器的子类,则称之为逆变

    Java 中数组是协变的,泛型是不可变的。

    6-4-1、用<?>来解决不可变性造成的问题
    class Fruit {}
    class Apple extends Fruit {}
    
    class Plate<T>{
        private T item;
        public Plate(T t){item=t;}
        public void set(T t){item=t;}
        public T get(){return item;}
    }    
    

    像下面这样使用水果盘子放苹果,是会引发编译错误的,因为根据上面的不可变性,容器之间不存在继承关系,无法向上溯型:

    Plate<Fruit> p=new Plate<Apple>(new Apple());   // ERROR
    

    解决方法如下:

    Plate<? extends Fruit> p=new Plate<Apple>(new Apple());
    

    (完)

    相关文章

      网友评论

          本文标题:泛型(Generic)总结(三)

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