Java泛型引发出来的桥接方法

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为什么不去对桥接方法进行处理呢?

发布了72 篇原创文章 · 获赞 24 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/oYinHeZhiGuang/article/details/103840416