美文网首页
泛型(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)总结(三)

    六、泛型擦除 6-1、什么是泛型擦除 泛型这个概念,只存在于编译器中。而不存在于虚拟机(JVM)中。 意思是说,编...

  • 泛型(Generic)总结(一)

    一、泛型的引入 泛型这个概念的出现,根本目的是解决在“通用方法”中使用“通用类型”的问题。 泛型的本质是参数类型化...

  • 泛型(Generic)总结(二)

    三、类型参数与无界通配符 3-1、区别使用 首先要区分开两种不同的场景: 声明一个泛型类或泛型方法。这...

  • Java 中的泛型 (Generic)

    泛型 (Generic) 泛型 (Generic),即“参数化类型”,就是允许在定义类、接口、方法时使用类型形参,...

  • Generic泛型

    泛型:JDK1.5版本以后出现的新特性,用于解决安全问题,是一个类型安全机制。 好处:1.将运行时期出现问题Cla...

  • 泛型Generic

    用二位坐标定义一个平面上的点a(x,y): 精度不够,提高精度需要重新定义高精度的类: 上面定义的两个类的代码非常...

  • 泛型(Generic)

    泛型:JDK1.5版本以后出现的新特性,用于解决安全问题,是一个类型安全机制。 好处:1.将运行时期出现问题Cla...

  • 泛型Generic

    a. 在java泛型中,如果创建一个运用泛型的数组,完整的写法为: 即无法直接创建,只能创建Object类型,然后...

  • Generic泛型

    网址 https://www.cnblogs.com/dotnet261010/p/9034594.html De...

  • 泛型generic

    先看一段代码 上边的join方法的参数,有3种情况,都可以运行成功。 但是,当我们提出了新的需求,比如当first...

网友评论

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

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