Java-桥方法

1.引入

桥方法的来源是源自于Java的泛型,因为Java的泛型擦除而导致在多态的时候会出现问题而引入了桥方法

2.类型擦除

Java在语法中虽然存在泛型的概念,但是在虚拟机中却没有泛型的概念,虚拟机中所有的类型都是普通类。无论何时定义一个泛型类型,编译后类型会被都被自动转换成一个相应的原始类型。
但是要注意,泛型擦除其实只是擦除的字节码中code属性部分的泛型,classMetadata上还是会保留泛型信息。

比如这个类

public class Parent<T>
{
    public void sayHello(T value)
    {
        System.out.println("This is Parent Class, value is " + value);
    }
}

在编译后就变成了

public class Parent
{
    public void sayHello(Object value)
    {
        System.out.println("This is Parent Class, value is " + value);
    }
}

对类型变量进行替换的规则有两条:

  1. 若为无限定的类型,如,被替换为Object
  2. 若为限定类型,如<T extends Comparable & Serializable>,则用第一个限定的类型变量来替换,在这里被替换为Comparable

桥方法

类型擦除后,就产生了一个奇怪的现象。

假设有一个超类:

public class Parent<T>
{
    public void sayHello(T value)
    {
        System.out.println("This is Parent Class, value is " + value);
    }
}

以及一个子类:

public class Child extends Parent<String>
{
	@Override
    public void sayHello(String value)
    {
        System.out.println("This is Child class, value is " + value);
    }
}

那么在Parent类编译之后,方法上的泛型没有了,参数类型变成了Object类型,那就出现问题了,子类中的sayHello方法是重写了父类的方法,但是根据Java重写的要求,方法的参数类型必须是一致的,那这样不就出错了吗?子类是String但是父类是Object

结果是可以正常运行。
原因是编译器为了维护这种重写的原则,在Child类中自动生成了一个桥方法:

public void sayHello(Object value)
{
    sayHello((String) value);
}

可以看出,这个桥方法实际上就是对超类中sayHello(Obejct)的重写。这样做的原因是,当程序员在子类中写下以下这段代码的时候,本意是对超类中的同名方法进行重写,但因为超类发生了类型擦除,所以实际上并没有重写成功,因此加入了桥方法的机制来避免类型擦除与多态发生冲突。

public class Child extends Parent<String>
{
    public void sayHello(String value)
    {
        System.out.println("This is Child class, value is " + value);
    }
}

桥方法并不需要自己手动生成,一切都是编译器自动完成的。
查看Child最后生成字节码,可以发现其中多了一个方法,该方法就是桥方法,而且能看到一个访问修饰符ACC_BRUDGE,标识当前方法是一个桥方法
在这里插入图片描述而且不仅如此,使用Java反射也能获取到对应的方法。
Method类下存在该方法

/**
 * Returns {@code true} if this method is a bridge
 * method; returns {@code false} otherwise.
 *
 * @return true if and only if this method is a bridge
 * method as defined by the Java Language Specification.
 * @since 1.5
 */
public boolean isBridge() {
    return (getModifiers() & Modifier.BRIDGE) != 0;
}

返回值的情况

public class Parent<T> {
    public T get() {
        return null;
    }
}
public class Child extends Parent<String> {
    @Override
    public String get() {
        return null;
    }
}

这种情况下父类的返回值也会被擦除,也就变成了Object,这种情况下编译器依然会给我们生成一个桥方法,必须要生成桥方法,否则会出错。因为编译器在编译的时候会根据方法签名确定一个对象调用的方法的版本,就像下面这样
在这里插入图片描述在第9行的位置,我们看调用的方法的签名已经被确定了下来,只是运行时的时候还会根据多态的实际类型去判断具体调用哪个类型的方法。
如果子类中没有对应的桥方法,那么在运行时根据实际类型确定调用的方法的时候就会出现找不到指定的方法,所以子类必须生成一个和父类一致的桥方法。

所以Child变成了下面这个样子

public class Child extends Parent<String> {
    @Override
    public String get() {
        return null;
    }
    
    // 桥方法
    public Object get() {
    	// 你可能又有疑问了,这样调用get方法不会出问题吗?JVM如何知道调哪个方法
    	// 其实jvm是知道的,因为方法的签名不同,这里调用的方法的签名其返回值是String类型的,所以就会去调用上面的方法
        return get();
    }
}

java代码中,判断两个方法是否是同一个方法是根据方法的签名来决定的,方法的签名是不包含方法的返回值的,也就是“方法签名=方法名+参数”。那上面这个两个方法不就冲突了吗,没错,如果我们代码里面这样写确实是冲突了,但是Java虚拟机中的方法签名规则是不同的,其“方法签名=方法名+参数+返回值”,所以他是能正确区分两个方法的,所以字节码中是允许存在上面这种情况的。

当然这种情况不只是在使用泛型的时候会出现,当在重写方法时,指定了一个更加严格的返回值类型,虚拟机会帮我们生成桥方法。该例子中的 A.getValue() 和 B.getValue() 称为具有协变的返回类型。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值