1.前言
最近在看Mybatis-3的源码,碰到一个冷门的方法{@link Method#isBridge()},这是什么鬼。以前只听过有个叫做桥接的设计模式,现在怎么又出现一个桥接方法。经过一系列的查询,终于搞明白这个冷门的方法了。
2.概述
java的泛型自从jdk1.5问世以来,一直深受广大java程序员的喜爱,它免去了在实际开发过程中的类型强转,方便了开发。同时,也为接口的开发提供了不少便利。试想一下,如果写一个接口,返回一个List集合,如果没有泛型,使用接口的人一脸懵逼,他不知道怎么处理集合里的数据,而泛型的出现,使接口的开发更加安全、便捷。这里有几个问题我们要清楚一下。
2.1 class文件泛型的展现形式
java设计人员们为了保证向前兼容,他们在由java文件编译成class文件的过程中,会擦除泛型,那么在方法中我们传递的参数或者返回的类型该是什么样子的呢?我们先看一下下面的例子:
带有泛型的接口以及主方法:
public interface SurperInter<T> {
void print(T t);
}
public class BridgeTest {
public static void main(String[] args) {
//打印方法的个数
printMethodCount(SurperInter.class);
}
private static void printMethodCount(Class clazz){
Method[] methods = clazz.getMethods();
System.out.println(clazz.getName() + "方法个数==>" + methods.length);
Arrays.stream(methods).forEach(System.out::println);
System.out.println("===========================================");
}
}
输出结果:
cn.surpass.jvm.methodbridge.common.SurperInter方法个数==>1
public abstract void cn.surpass.jvm.methodbridge.common.SurperInter.print(java.lang.Object)
===========================================
看见了吧,他是以Object的形式展现出来的。
2.2 子类实现不指定泛型
那么我们如果子类不指定泛型,那么实现方法会是什么样子的呢?
public class ImpClassObject implements SurperInter {
@Override
public void print(Object o) {
}
}
他的实现方法传入参数是Object应该是,接下来,我们在打印一下这个实现类的方法列表,这里我们在打印类的方法时候,我们调用的是
Method[] methods = clazz.getDeclaredMethods();
cn.surpass.jvm.methodbridge.common.ImpClassObject方法个数==>1
public void cn.surpass.jvm.methodbridge.common.ImpClassObject.print(java.lang.Object)
咦,好像和我们想的一样,没什么变化。
2.3 执行范具体类型的实现类
那么我们如果子类指定泛型,如果还是指定Object作为传入参数,会是什么样子的呢?
好像这样不行了,意思是这个实现类或者定义成抽象方法,或者实现抽象方法print(T),换句话说,这个传入的Object参数他不认为是对于接口方法重写,那么我们不得不改成String,这样,我们的代码就不会报错了。
看看,正常了吧。
2.4 实现类的参数为接口参数的子类是否属于实现方法
他好像还是不认上面的方法就是下面的方法重写。
2.5 字节码层面的实现
上面的例子是通过java文件演示的,如果对于JVM执行的class文件呢,其实就变成了类似2.4出现的情况,接口在编译成class文件后会擦出泛型,编程2.4的内部接口,岂不是报错了,程序还怎么去执行?
3 揭开里面的玄机
究竟是怎么实现的呢?我们就正常写一个实现类,看看究竟是什么样子的。下面依次是接口、实现类和执行类。
public interface SurperInter<T> {
void print(T t);
}
public class ImpClass implements SurperInter<String> {
@Override
public void print(String s) {
System.out.println(s);
}
}
public class BridgeTest {
public static void main(String[] args) {
//打印方法的个数
printMethodCount(ImpClass.class);
}
private static void printMethodCount(Class clazz){
Method[] methods = clazz.getDeclaredMethods();
System.out.println(clazz.getName() + "方法个数==>" + methods.length);
Arrays.stream(methods).forEach(method -> {
System.out.println(method.toString());
System.out.println("是否桥接=>:" + method.isBridge());
});
}
}
接下来看看执行结果:
cn.surpass.jvm.methodbridge.common.ImpClass方法个数==>2
public void cn.surpass.jvm.methodbridge.common.ImpClass.print(java.lang.String)
是否桥接=>:false
public void cn.surpass.jvm.methodbridge.common.ImpClass.print(java.lang.Object)
是否桥接=>:true
到这里,我们突然恍然大悟了,原来,jvm在对子类进行编译的时候,生成了一个以Object为参数的方法。这样,就不会出现类似于2.4节出现的问题了。同时,我们看到,jvm为子类新创建的那个方法就是桥接方法。
到目前我们终于搞明白什么是桥接方法,但是好像还没有完。
3.1 新方法执行内容
我们想一下,类似于如下的代码:SurperInter surperInter = new ImpClass();他调用自己的方法应该是surperInter.print(Object obj),如果子类对于这个方法处理不慎,就会出现问题。那么子类究竟是怎么实现这个方法的呢?让我们继续往下看。我们通过javap -v 来看一下这个字节码文件的内容。这里,我只截取片段方便分析。
public void print(java.lang.String);
...
Code:
stack=2, locals=2, args_size=2
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_1
4: invokevirtual #3 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
7: return
...
public void print(java.lang.Object);
...
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #4 // class java/lang/String
5: invokevirtual #5 // Method print:(Ljava/lang/String;)V
8: return
...
对于方法public void print(java.lang.String);我们不做太多解释,无非就是执行System.out.println()方法。我们重点看一Object的方法实现。
1)首先我们通过args_size知道这个方法有两个参数(this和Object),存放在局部变量表中。
2)代码依次执行aload_0和aload_1将局部变量表中的this和Object一次压入操作数栈栈顶。
3)checkcast检查栈顶元素(object)是否可以强转成String.
4)invokevirtual #5 也即使调用public void print(java.lang.String);当然会使用栈顶两个参数this和Object.
现在我们明白了,其实绕了一大圈,创建的方法通过类型转换后最终还是调用了重写的方法。
3.2 对于ImpClass类的扩展
对于ImpClass,我们可不可以再写一个public void print(java.lang.Object)方法呢?应该是很容易猜到了,肯定是不可以的,否则对于3.1接口调用方法岂不有乱套了。
3.3 对于ImpClass的子类我们可以重写这个方法吗
3.5 泛型对于素有的接口都是Object吗
定义泛型有一个关键词extend.如果接口的泛型是集成一个类,那编译出来的是什么内容?
public interface SurperInter<T extends CharSequence> {
void print(T t);
}
public class BridgeTest {
public static void main(String[] args) {
//打印方法的个数
printMethodCount(SurperInter.class);
}
private static void printMethodCount(Class clazz){
Method[] methods = clazz.getDeclaredMethods();
System.out.println(clazz.getName() + "方法个数==>" + methods.length);
Arrays.stream(methods).forEach(method -> {
System.out.println(method.toString());
System.out.println("是否桥接=>:" + method.isBridge());
});
System.out.println("===========================================");
}
}
输出内容:
cn.surpass.jvm.methodbridge.extendobject.SurperInter方法个数==>1
public abstract void cn.surpass.jvm.methodbridge.extendobject.SurperInter.print(java.lang.CharSequence)
是否桥接=>:false
===========================================
4 总结
本来就像说一下桥接方法呢,结果引出来一系列泛型的知识,就当是共同学习吧。那么大家可以想一下,Mybatis为什么不去对桥接方法进行处理呢?