1. 桥接
最近在看源码,突然在源码中看到“BridgeMethodResolver.findBridgedMethod(specificMethod)”这样的代码瞬间懵逼了。后来通过我的疯狂百度,才知道这是一个获取“被桥接”方法的方法。这才顺腾摸瓜,仔细了解了一下“桥接方法”这个东东到底是什么东东。
1.1 什么是桥接
相信点进本文的胖友肯定都是被什么是桥接搞得有点懵。废话不多说,直接撸代码,来看下什么是桥接。
1.1.1 改变返回值生成桥接方法
首先看以下这段代码:
//定义了一个父类,其中方法返回一个number
public class Super0 {
public Number getNum0(){
return 1;
}
}
//super0的一个子类,子类重写了父类的getNum0方法
public class Sub0 extends Super0{
@Override
public Integer getNum0() {
return 1;
}
}
//这是一个测试方法,@Slf4j是由lombok提供的注解(可以忽略)
@Slf4j
public class TestMain {
public static void main(String[] args) {
//改变返回值类型生成桥接方法
Method[] ms0=Sub0.class.getDeclaredMethods();
for(Method m:ms0){
log.info("方法描述[{}],是否是桥接方法[{}]",m.toString(),m.isBridge());
}
}
}
- 定义了一个父类Super0,在父类中我们定义了一个getNum0的方法,该方法返回一个Number对象
- 然后定义了一个继承Super0的子类Sub0,Sub0重写了父类的getNum0方法。但它返回值类型改为了Number的子类Integer
- 在TestMain的main方法中获取Sub0类的方法并打印
运行结果如下:
可以看到Sub0中出现了两个方法,除了我们重写返回值为Integer的方法外,与此同时多了一个返回值为Number的同名方法,而该方法明显不是我们在类中定义的。这是怎么回事?
其实,多出来的这个方法在编译阶段由程序自动生成的,这个方法也就是我们通常所说的桥接方法(ps:至于为什么需要这样一个桥接方法,后面会讲到)。我们可以通过Method.isBridge()来判断方法是否为桥接方法。
那问题来了,什么时候会生成桥接方法?
我们将Sub0中的getNum0微调以下
public class Sub0 extends Super0{
//将返回值由Integer改为Number
@Override
public Number getNum0() {
return 1;
}
}
重新运行测试方法
可以看到当子类重写父类的方法返回值相同的时候,程序没有再生成桥接方法。
由此我们可以粗粗的得出一个结论“当子类重写父类的方法改变了其方法返回值的时候,程序会在编译时自动生成一个桥接方法”
1.1.2 使用泛型生成桥接方法
上一节讲了,子类重写时如果改变了返回值会生成桥接方法,那是不是只有这一种情况才会生成呢?请看以下这段代码:
//定义了一个带泛型的父类
public class Super1<T> {
private T num;
public Super1() {
}
public Super1(T num) {
this.num = num;
}
public T getNum1(){
return num;
}
}
//Sub1继承Super1
public class Sub1 extends Super1<String>{
public Sub1(String num) {
super(num);
}
@Override
public String getNum1() {
return super.getNum1();
}
}
@Slf4j
public class TestMain {
public static void main(String[] args) {
//泛型生成桥接方法
Method[] ms1=Sub1.class.getDeclaredMethods();
for(Method m:ms1){
log.info("方法描述[{}],是否是桥接方法[{}]",m.toString(),m.isBridge());
}
}
}
定义一个带泛型的父类Super1并定义一个返回该泛型参数的方法。Sub1继承Super1,getNum1返回一i个string。和上面一样,在main方法中打印Sub1的方法。
运行结果如图:
可以看到依然出现了桥接方法,且桥接方法返回值时一个Object对象。由此我们又得出一个结论“如果父类是一个带泛型的类,子类中也会出现桥接方法”
1.1.3 什么是桥接
以上两种方式都会生成桥接。我们将第一种情况下的Sub0的class文件进行反编译,如图:
可以看到Sub0中有两个getNum0()方法,其中方法2为桥接方法,但从反编译后看,方法2内部其实是直接调用的方法1中的逻辑。是不是很迷惑?
所以什么是桥接方法?从字面上理解,桥接方法是一种像“桥梁”一样的方法,他用于连接不同的“两端”。而这两端即是java和jvm
1.2 为什么需要桥接
java和jvm为什么需要通过桥接方法连接?桥接方法到底有什么意义?
请回想一下java中几个基础问题的答案。何为重写?何为重载?可以用不同返回值来进行方法重载吗?仔细一思考,我想大多数胖友都能回答出来。
- 重写:重写是子类对父类允许访问的方法的实现过程进行重新编写,或者说对原方法的一种复写
- 重载:重载是指我们可以定义一些名称相同的方法,通过定义不同的输入参数来区分这些方法,然后再调用时,JVM就会根据不同的参数样式,来选择合适的方法执行
- 从上面Super0和Sub0的列子即可证明,不能用方法返回值来进行方法重载(上面的例子明显是一种方法重写)
考虑了这几个问题后,我们在来想一下java中如何确定一个方法?也就是说如果方法有一个唯一id,这个id应该包含哪些内容?
由于java中返回值不同不能用于区分是否是方法重载,所以说方法的签名肯定不能包含返回值类型。所以一个方法的签名应该如下:
Java方法签名=方法名+参数类型
但是熟悉jvm的胖友都知道,jvm中区分一个方法不仅仅包含方法名,参数类型,也包含返回值类型。所以jvm中方法签名应该如下:
jvm方法签名=方法名+参数类型+返回值类型
由此带来几个问题:
- 如果子类重写了父类,但返回的类型为父类返回类型的子类。在java中被认定为重写,但在jvm中被认定为重载(如1.1.1中那样)
- 如果父类带泛型T,子类重写父类,在编译时由于泛型会被擦除(普通泛型被擦除为Object,带extends的泛型被擦除为其上界),这直接导致父类中方法签名发生改变,在java中定义的重写,在jvm中却无法识别。
- 所以为了解决jvm和java之间的差别,需要一个特殊的技巧来对java中这些“重写”进行改造成,以此来让jvm正确识别和处理。这个方式就是生成一个和父类方法签名一致的方法(返回参数类型,方法名,参数类型完全一致),并由该方法来调用具体的方法逻辑。
2 泛型和类型擦除
前面提到如果父类带泛型,泛型会被擦除。这又是怎么回事?我们首先来看以下这段代码
@Slf4j
public class TestMain {
public static void main(String[] args) {
Super1<String> super1=new Super1<>();
Super1<Integer> super2=new Super1<>();
log.info("class是否相同[{}]",super1.getClass()==super2.getClass());
}
}
其中Super1是我们上一节中定义的一个带泛型的类。我们实例化了两个Super1,但是其中泛型一个是String,一个是Integer。想一想这两个对象的class相等吗?
以下是运行结果:
可以看到这两个class是完全相同的,那就说明泛型不同并不等同于类不同。
泛型是我们平时用的比较多的一种java语言特性。但你知道泛型真正是什么吗?从字面意思来看,“泛型”即泛化的类型,换句话来说就是类型被参数化或者说是可以将类型当作参数传递给一个类或方法。
泛型是在jdk1.5后才出现的特性,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。来看下面这段代码:
@Slf4j
public class TestMain {
public static void main(String[] args) {
//类型擦除
Super1<String> super1=new Super1<>();
for(Method m:super1.getClass().getDeclaredMethods()){
log.info("方法描述[{}]",m.toString());
}
}
}
这里将super1的中的方法打印出来,注意这是泛型T是string类型的
可看到方法中的泛型都被擦除转化成了Object。从结果来看泛型确实旨在编译时才起作用。那是不是所有的泛型都会被擦除成Object呢?其实不是的,如果存在“T extends XX”这样的定义的话,泛型擦除后会变成对应的XX。
看到这里可能有聪明的胖友可能有疑问,既然泛型会被擦除成Object和其对应的上界,那我们为什么不直接使用父类来接受参数呢?这也是jdk1.5之前使用的方式。那是因为如果我们直接使用父类的话,在使用的时候不可避免的会出现需要强制转换类型的问题,同时还会出现一些在编译时期不会报错但运行时出现的类型匹配问题。而泛型能够提供我们在编译时期就能进行类型检查的安全机制。
3. 结语
花了一个上午写了这篇文章,因为人比较懒有些地方本可以加一些实例说明的但是最后还是没加。所有有问题的胖友可以评论区问我哈