【java基础】类型擦除、桥方法、泛型代码和虚拟机

基础说明

虚拟机没有泛型类型对象一所有对象都属于普通类。在泛型实现的早期版本中,甚至能够将使用泛型的程序编译为在1.0虚拟机上运行的类文件!
由于泛型是在1.5才引入的,为了兼容,在java文件编译后是肯定看不见泛型的。也就是类型擦除,下面就来介绍一下类型擦除


类型擦除

无论何时定义一个泛型类型,都会自动提供一个相应的原始类型(raw type)。这个原始
类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除(erased),并替换为其限
定类型(或者,对于无限定的变量则替换为Object)。


无限定

下面先来看下面的代码

public class MyTool<T> {
    
    
    private T info;

    public T getInfo() {
    
    
        return info;
    }

    public void setInfo(T info) {
    
    
        this.info = info;
    }
}

这就是很简单的一个泛型类。现在,我通过反射来查看T是什么类型。

    public static void main(String[] args) throws NoSuchMethodException {
    
    
        // 得到getInfo方法
        Method getInfo = MyTool.class.getDeclaredMethod("getInfo");
        System.out.println("getInfo返回值类型为:"+getInfo.getReturnType().getName());
    }

上面运行结果如下
在这里插入图片描述
可以发现如果没有指定泛型,那么在编译过后T被替换为了Object
即使我们指定了泛型,T还是会被替换为Object

    public static void main(String[] args) throws NoSuchMethodException {
    
    
        MyTool<Comparable> myTool = new MyTool<>();
        // 得到getInfo方法
        Method getInfo = myTool.getClass().getDeclaredMethod("getInfo");
        System.out.println("getInfo返回值类型为:" + getInfo.getReturnType().getName());
    }

在这里插入图片描述


有限定

上面的泛型类没有限制,下面来看一下有限定的情况

public class Tool<T extends Serializable> {
    
    

    private T info;

    public T getInfo() {
    
    
        return info;
    }

    public void setInfo(T info) {
    
    
        this.info = info;
    }
}

还是使用反射来查看T类型

        // 得到getInfo方法
        Method getInfo = Tool.class.getDeclaredMethod("getInfo");
        System.out.println("getInfo返回值类型为:"+getInfo.getReturnType().getName());

运行结果如下

在这里插入图片描述

可以发现限定符替换了T。
上面是一个限定符的,如果有两个或者多个限定符呢

public class MulTool<T extends Comparator & Comparable & Serializable> {
    
    

    public T t;

    public T getInfo() {
    
    
        return t;
    }
}

还是使用上面的反射代码,输出如下

在这里插入图片描述

可以发现返回的是Comparator,下面来交换一下限定的位置,分别让Comparable 和Serializable成为第一个(自己交换即可)。交换后代码运行结果如下

在这里插入图片描述
在这里插入图片描述

通过上面的运行结果,我们就可以得出结论,使用了类型限定符,那么第一个限定就会替换T


转换泛型表达式

还是上利用MyTool代码举例

public class MyTool<T> {
    
    
    private T info;

    public T getInfo() {
    
    
        return info;
    }

    public void setInfo(T info) {
    
    
        this.info = info;
    }
}

我们经过上面的学习,知道会进行类型擦除,上面的MyTool的T会被替换为Object。那么getInfo返回值就是Object的类型,但是我们在实际调用getInfo方法时只要传入了类型,那么返回值就是我们传入的类型。看下面代码

    public static void main(String[] args) {
    
    
        MyTool<String> stringMyTool = new MyTool<>();
        stringMyTool.setInfo("xxx");
        // 得到所有方法
        Method[] declaredMethods = stringMyTool.getClass().getDeclaredMethods();
        for (Method declaredMethod : declaredMethods) {
    
    
            String methodName = declaredMethod.getName(); // 方法名
            String returnType = declaredMethod.getReturnType().getName(); // 返回类型
            System.out.println("方法名:" + methodName + "--返回类型:" + returnType);
        }
        // 返回的类型为String
        String info = stringMyTool.getInfo();
    }

运行结果如下

在这里插入图片描述

可以发现getInfo返回值确实为Object,但是我们 String info = stringMyTool.getInfo(); 这条语句并没有进行强转,这就说明编译器已经帮我们进行了强转。其实在调用stringMyTool.getInfo()编译器将其转换为了2条虚拟机指令

  • 对于MyTool.getInfo()的调用
  • 将返回值Object强转为String

上面是对方法返回值进行强转,其实对字段的访问也是一样的,如果将info字段修饰符改为public,也可以直接使用String进行接收

        String filed = stringMyTool.info;

方法类型擦除(桥方法)

我们不说啥理论,直接看下面代码

public class Animal<T> {
    
    

    public void setX(T t) {
    
    
    }
}

这个一个泛型类,有一个set方法

public class Cat extends Animal<String> {
    
    

    @Override
    public void setX(String s) {
    
    
        
    }
}

这是Cat类,继承了Animal类,指定了泛型为String,并且重写了setX方法。

下面就是使用Cat

        Animal<String> animal = new Cat();
        animal.setX("hello world!!!");

大家看看这个代码,有没有发现问题?我们使用Animal来接收了一个Cat对象,这是正确的。但是animal.serX就不怎么对劲了。下面我来分析一下

  • 由于Animal会发生类型擦除,所以animal.setX实际会调用 Animal.setX(Object)
  • 由于animal引用的是一个Cat,所以会去寻找Cat.setX(Object)
  • 问题出现了,Cat根本没有setX(Object),只有setX(String)

可以发现,类型擦除和多态产生了冲突。为了解决这个问题,编译器会在Cat类中生成一个桥方法。在Cat中生成的桥方法如下

    public void setX(Object s){
    
    
        setX((String) s);
    }

其实就是生成了一个参数为Object类型的setX方法,这个方法会去调用参数为String类型的方法,就好像桥梁的作用一样,所以我们成为桥方法。

为了验证上面的说法,也就是编译器会给我们的代码生成一个桥方法,下面我就使用反射输出Cat的所有方法。

    public static void main(String[] args) {
    
    
        // 得到所有方法
        Method[] declaredMethods = Cat.class.getDeclaredMethods();
        for (Method declaredMethod : declaredMethods) {
    
    
            String methodName = declaredMethod.getName(); // 方法名
            // 参数类型集合
            List<String> types = Arrays.stream(declaredMethod.getParameterTypes())
                    .map(Class::getTypeName).collect(Collectors.toList());
            System.out.println("方法名:" + methodName + "--参数类型:" + types);
        }
    }

上面的代码输出如下

在这里插入图片描述

可以发现编译器确实给我们生成了一个setX方法,参数类型就是Object,这个方法就是一个桥方法。有了这个桥方法,多态和类型擦除的问题也就解决了。


关于重载的一些说明

通过上面的例子,大家应该对桥方法有了清晰的认识,有些思想活跃的人可能就会觉得不太对劲了。大家回想一下重载的定义,重载就是参数名相同,参数不同。

这确实没问题,下面我在Animal定义应该getT方法,然后在Cat里面重写这个方法

public class Animal<T> {
    
    

    private T t;

    public void setX(T t) {
    
    
    }

    public T getT() {
    
    
        return t;
    }
}
public class Cat extends Animal<String> {
    
    

    @Override
    public void setX(String s) {
    
    

    }

    @Override
    public String getT() {
    
    
        return "";
    }
}

根据上面的桥方法,大家想一下,是不是在Cat里面会生成应该 public Object getT()方法呢?我们还是通过的反射代码查看,代码和运行结果如下

    public static void main(String[] args) {
    
    
        // 得到所有方法
        Method[] declaredMethods = Cat.class.getDeclaredMethods();
        for (Method declaredMethod : declaredMethods) {
    
    
            String methodName = declaredMethod.getName(); // 方法名
            // 参数类型集合
            List<String> types = Arrays.stream(declaredMethod.getParameterTypes())
                    .map(Class::getTypeName).collect(Collectors.toList());
            // 得到返回类型
            String returnType = declaredMethod.getReturnType().getName();
            System.out.println("方法名:" + methodName + "\t\t参数类型:" + types + "\t\t返回类型:" + returnType);
        }
    }

在这里插入图片描述

可以发现,在Cat里面存在了2个同名的方法,并且参数相同,这已经违法了重载的定义,按理说程序应该直接报错,但是并没有,原因就是在虚拟机中,会由参数类型和返回类型共同指定一个方法,上面代码中参数为Object的getT方法就是一个桥方法。


总结

在最后,对于java泛型的转换,我们需要记住以下几点

  • 虚拟机中没有泛型,只有普通的类和方法
  • 所有的类型参数都会替换为它们的限定类型
  • 会通过合成桥方法来保持多态
  • 为保持类型安全性,必要时会插入强制类型转换

关于泛型的更多知识,参考以下内容

泛型程序设计基础
类型擦除、桥方法、泛型代码和虚拟机
泛型的限制及其继承规则
泛型的通配符(extends,super,?)

猜你喜欢

转载自blog.csdn.net/m0_51545690/article/details/129356404