方法调用6——Java对动态类型的支持

1. JDK1.7(JSR-292)对动态类型的直接支持

invokevirtual invokespecial invokestatic invokeinterface的第一个参数都是被调用方法的符号引用(CONSTANT_Methodref_info或者CONSTANT_InterfaceMethodref_info),符号引用在编译期确定了接收者的类型,而动态语言只有在运行期才能确定接收者类型。

对此JDK1.7(JSR-292)提供了invokedynamic指令以及java.lang.invoke包。

2.java.lang.invoke包

主要目的是在之前单纯依靠符号引用来确定调用的目标方法这种方式之外,提供了一种新的动态确定目标方法的机制,称为MethodHandle。

MethodHandler类似于C/C++的函数指针。

之前的Java没有办法单独地把一个函数作为参数进行传递。普遍的做法是设计一个带有方法的接口,以实现了这个接口的对象作为参数。

在拥有了MethodHandle之后,Java拥有了类似于函数指针的工具。

public class MethodHandleTest {
    static class ClassA {
        public void println(String s) {
            System.out.println(s);
        }
    }

    public static void main(String[] args) throws Throwable {
        Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
        getPrintlnMH(obj).invokeExact("hello world");
    }

    private static MethodHandle getPrintlnMH(Object receiver) throws Throwable {
        MethodType mt = MethodType.methodType(void.class, String.class);
        return MethodHandles.lookup().findVirtual(receiver.getClass(), "println", mt).bindTo(receiver);
    }
}

MethodType代表方法类型:方法返回值和参数
MethodHandles.lookup():在指定类中查找符合给定方法名称、方法类型并且符合调用权限的方法句柄。
虚方法的第一个参数是隐式的,代表该方法的接收者,也即this指向的对象,该参数之前是放在参数列表中进行传播,现在提供了bindTo()方法。

getPrintlnMH()模拟了invokevirtual指令执行过程,只不过其分派逻辑并非固化在Class文件的字节码上,而是通过一个具体方法来实现。
该方法的返回值MethodHandle对象可以视为对最终调用的一个“引用”。

3.反射机制与MethodHandle机制的区别

  • 都是在模拟方法调用,反射是在模拟Java代码层次的方法调用,而MethodHandle模拟字节码层次的方法调用。MethodHandles.lookup中的三个方法——findStatic findVirtual findSpecial正是为了对应于invokestatic invokevirtual&invokeinterface invokespecial这几条字节码指令的执行权限校验行为,而这些底层细节在调用反射API时不需要关心(MethodHandle权限检查在句柄创建阶段完成,在实际调用过程中,JVM并不会检查MethodHandle权限。如果多次被调用,相较于反射调用,会省下重复权限检查的开销)
  • java.lang.reflect.Method(重量级)远比java.lang.invoke.MethodHandle(轻量级)所包含的信息多。前者是方法在Java侧的全面映像,包含了方法的签名、描述符以及方法属性表中各种属性,还包含执行权限等运行期信息。 而后者仅仅包含与执行该方法相关的信息。
  • MethodHandle可以采用类似于字节码优化技术的思路,反射不行。
  • 反射只是为了Java语言服务,MethodHandle是服务于所有JVM上的语言。

4.invokedynamic指令

在某种程度上,invokedynamic与MethodHandle机制的作用是一样的,都是为了解决4条invoke指令方法分派规则固化在虚拟机中的问题,把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码中,让用户有更高的自由度。

MethodHandle采用上层Java代码和API实现,invokedynamic用字节码和Class中其他属性和常量来完成。

invokedynamic指令位置称为“动态调用点”(Dynamic Call Site),指令的第一个参数不再是方法的符号引用,而是CONSTANT_InvokeDynamic_info,包含三项信息:引导方法(放在BootstrapMethods属性中)、方法类型和名称。

CONSTANT_InvokeDynamic_info {
u1 tag;
u2 bootstrap_method_attr_index; //指向引导方法
u2 name_and_type_index; //方法名和方法描述符
}

引导方法返回值是java.lang.invoke.CallSite对象,代表真正要执行的目标方法调用。

interface Strategy {
    String approach(String msg);
}

interface StrategyDev {
    String approach(String msg);
}

class Soft implements Strategy {
    @Override
    public String approach(String msg) {
        return msg.toLowerCase() + "?";
    }
}

class Unrelated {
    static String twice(String msg) {
        return msg + " " + msg;
    }
    String third(String msg) {
        return msg + " " + msg + " " + msg;
    }
}

public class Strategize {
    Strategy strategy;
    String msg;
    Strategize(String msg) {
        strategy = new Soft(); // [1]
        this.msg = msg;
    }
    void communicate() {
        System.out.println(strategy.approach(msg));
    }
    void changeStrategy(Strategy strategy) {
        this.strategy = strategy;
    }
    public static void main(String[] args) {
        Strategy strategy1 = Unrelated::twice;//static

        System.out.println(strategy1.approach("Hello there"));

    }
}

字节码:

Constant pool:
   #9 = InvokeDynamic      #0:#49         // #0:approach:()Lcom/enjoy/learn/core/lambdastream/test/test4/Strategy;
  #49 = NameAndType        #57:#63        // approach:()Lcom/enjoy/learn/core/lambdastream/test/test4/Strategy;
  #57 = Utf8               approach
  #63 = Utf8               ()Lcom/enjoy/learn/core/lambdastream/test/test4/Strategy;



  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: invokedynamic #9,  0              // InvokeDynamic #0:approach:()Lcom/enjoy/learn/core/lambdastream/test/test4/Strategy;
         5: astore_1
         6: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
         9: aload_1
        10: ldc           #10                 // String Hello there
        12: invokeinterface #7,  2            // InterfaceMethod com/enjoy/learn/core/lambdastream/test/test4/Strategy.approach:(Ljava/lang/String;)Ljava/lang/String;
        17: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: return
      LineNumberTable:
        line 41: 0
        line 45: 6
        line 47: 20
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      21     0  args   [Ljava/lang/String;
            6      15     1 strategy1   Lcom/enjoy/learn/core/lambdastream/test/test4/Strategy;
}
SourceFile: "Strategize.java"
InnerClasses:
  public static final #71= #70 of #76;    // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #46 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #47 (Ljava/lang/String;)Ljava/lang/String;
      #48 REF_invokeStatic com/enjoy/learn/core/lambdastream/test/test4/Unrelated.twice:(Ljava/lang/String;)Ljava/lang/String;
      #47 (Ljava/lang/String;)Ljava/lang/String;

5.掌控方法分配规则

在Java程序中,可以通过“super”关键字很方便地调用到父类中的方法,但如果要访问祖类的方法呢?
1)使用MethodHandle实现

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

class GrandFather {
    void thinking() {
        System.out.println("I am grandfather");
    }
}

class Father extends GrandFather {
    @Override
    void thinking() {
        System.out.println("I am father");
    }
}

public class Son extends Father{
    @Override
    void thinking() {
        try {
            MethodType mt = MethodType.methodType(void.class);
            MethodHandle mh = MethodHandles.lookup().findVirtual(GrandFather.class,
                    "thinking", mt).bindTo(new GrandFather());
            mh.invoke();
        } catch (Throwable e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {
        new Son().thinking();
    }
}

2)方法引用(invokedynamic)实现

import java.util.function.Function;

class GrandFather {
    void thinking() {
        System.out.println("I am grandfather");
    }
}

class Father extends GrandFather {
    @Override
    void thinking() {
        System.out.println("I am father");
    }
}

public class Son extends Father{
    @Override
    void thinking() {
        Runnable function = new GrandFather()::thinking;
        function.run();
    }

    public static void main(String[] args) {
        new Son().thinking();
    }
}

字节码:

  void thinking();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class com/enjoy/learn/core/oop/method/GrandFather
         3: dup
         4: invokespecial #3                  // Method com/enjoy/learn/core/oop/method/GrandFather."<init>":()V
         7: dup
         8: invokevirtual #4                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
        11: pop
        12: invokedynamic #5,  0              // InvokeDynamic #0:run:(Lcom/enjoy/learn/core/oop/method/GrandFather;)Ljava/lang/Runnable;
        17: astore_1
        18: aload_1
        19: invokeinterface #6,  1            // InterfaceMethod java/lang/Runnable.run:()V
        24: return
      LineNumberTable:
        line 32: 0
        line 33: 18
        line 44: 24
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      25     0  this   Lcom/enjoy/learn/core/oop/method/Son;
           18       7     1 function   Ljava/lang/Runnable;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342