DEV Community

Pramoth Suwanpech
Pramoth Suwanpech

Posted on

Java InvokeDynamic (part 4 Lambda expression)

ตอนนี้ใช้พื้นฐานจาก part ก่อนหน้านี้นะครับ
อ่าน part 3 invokedynamic bytecode จากที่นี่

ในตอนนี้นี้จะอธิบายในส่วนของ lambda express ซึ่งมีในจาวา 8 ขึ้นไป โดยตัว lambda expression นี้จะอิมพรีเมนต์แตกต่างกับ annonymouse innter class ออกไป

lambda expression ในจาวา 8 ส่วนหนึ่งจะใช้ฟีเจอร์ที่มีเข้ามาตั้งแต่ในจาวา 7 นั่นก็คือ invokedynamic ซึ่งถูกออกแบบมาให้รองรับกับพวกภาษาไดนามิคที่รันบน JVM อย่างพวก JRuby,Groovy เพื่อให้ resolve target method ที่จะถูกเรียกได้ตอนรันไทม์ (เดิมทีถ้าเราจะทำแบบนี้จะต้องใช้ Reflection)

ผมจะขอยกตัวอย่างจากโค๊ดให้ดูเลยนะครับ ให้โฟกัสที่ test() นะครับ

public class LambdaTest {

    public void test() {
        // 1. Not refer to outer scope
        // javac will create a synthetic static  method  name "lambda$test$0()"
        Runnable a = () -> System.out.println("Hello From Lambda. no capture variable");

        // 2. Refer/capture value from outer scope
        // javac will create a synthetic static method name "lambda$test$1(java.lang.String)V"
        String name = "Pramoth";
        Runnable b = () -> System.out.println("Hello From Lambda " + name + " with capture variable");
    }


    public static void main(String[] args) throws Throwable {
        // จำลอง invokedynamic ไปที่ bootstrap `lambda$test$0`
        Runnable r = mimic_call_lambda$test$0();
        // print "Hello From Lambda. no capture variable"
        r.run();

        // จำลอง invokedynamic ไปที่ bootstrap `lambda$test$1`
        r = mimic_call_lambda$test$1("Pramoth");
        // print "Hello From Lambda Pramoth with capture variable"
        r.run();

    }

    private static Runnable mimic_call_lambda$test$0() throws Throwable {
        // LambdaMetafactory จะทำการ implement Runnable.run ด้วยการทำ bytecode manipulation โดย ASM ดูการทำงานใน  java.lang.invoke.InnerClassLambdaMetafactory
        CallSite lambda$test$CallSite = LambdaMetafactory.metafactory(MethodHandles.lookup(),
                "run", // method ที่จะ  implement คือ Runnable.run()
                MethodType.methodType(Runnable.class), // method type ของ factory method จะรีเทริน Runnable อารมณ์คล้ายๆ Runnable Factory.create()
                MethodType.methodType(void.class), // อิมพลีเมนต์เมธอดจะรีเทร์น void `public void run()`
                MethodHandles.lookup().findStatic(LambdaTest.class, "lambda$test$0", MethodType.methodType(void.class)), // MethodHandle ที่เอาไว้เรียกเมธอดที่ javac สร้างมาเป็น body ของ lambda
                MethodType.methodType(void.class)); // อิมพลีเมนต์เมธอดจะรีเทร์น void `public void run()` อันนี้ที่เพราะสามารถรีเทริน more specific class ได้ กรณีนี้ ไม่ได้ใช้ก็ระบุ void.class เหมือนรีเทรินของอิมพลีเมนต์เมธอด
        // invoke โดยไม่มี capture vairable
        return (Runnable) lambda$test$CallSite.dynamicInvoker().invokeExact();
    }

    private static Runnable mimic_call_lambda$test$1(String captureVariable) throws Throwable {
        CallSite lambda$test$CallSite = LambdaMetafactory.metafactory(MethodHandles.lookup(),
                "run",
                MethodType.methodType(Runnable.class, String.class), // method type ของ factory method จะรีเทริน Runnable แต่ว่าเคสนี้มีการ capture variable ด้วย ดังนั้นจึงต้องระบุ parameter ด้วย อารมณ์คล้ายๆ Runnable Factory.create(String)
                MethodType.methodType(void.class),
                MethodHandles.lookup().findStatic(LambdaTest.class, "lambda$test$1", MethodType.methodType(void.class, String.class)),
                MethodType.methodType(void.class));
        // invoke พร้อมกับ capture vairable
        return (Runnable) lambda$test$CallSite.dynamicInvoker().invokeExact(captureVariable);
    }
}

javac จะเปลี่ยน body ของ lambda expression ไปเป็น target method ที่มีชื่อ

  • private static R lambda$(method)$x ในกรณีอยู่ใน method
  • private static R lambda$new$x ในกรณีอยู่ใน instance initializer block หรือตรง initial ค่าของ instance variable
  • private static R lambda$static$x ในกรณีอยู่ใน static initializer block หรือตรง initial ค่าของ class variable โดย x จะเป็น 0 1 2 .... และ R คือ return type

เมธอดที่สร้างขึ้นจะมีแอททริบิว ACC_SYNTHETIC ซึ่งหมายถึง compiler เป็นผู้สร้างให้

จากนั้น javac จะสร้าง CONST_MethodHandle_info ใน bytecode เพื่อให้ invokedynamic เรียกใช้งาน โดยในภาษาจะมี api ที่เป็น generic bootstrap method ที่ใช้สำหรับอิมพลีเมนต์ functional interface อยู่ ซึ่งก็คือคลาส java.lang.invoke.LambdaMetafactory

เมื่อ invokedynamic ทำงานจะไปเรียก bootstrap method java.lang.invoke.LambdaMetafactory.metafactory() เพื่อสร้าง CallSite ที่เมื่อเรียก invoke*() จะรีเทริน implementation ของ functional interface นั้นๆ ซึ่งตัว implementation นี้จะอิมพลีเมนต์ functional method ด้วยการไปเรียก MethodHandle ของ target method อีกที (อธิบายไว้ในโคีดนะครับ)

อันนี้เป็น internal representation นะครับ ไม่ควรเอาไปเรียกใช้ตรงๆอย่างในโค๊ด อันนี้แค่เดโม

โดยผมยกตัวอย่างไว้ 2 กรณี

  • กรณีที่ lambda expression ไม่ได้อ้างถึงตัวแปรใน outer scope (ตัวอย่างการเรียกใช้งานในเมธอด mimic_call_lambda$test$0()) target method ที่ได้จะออกมาเป็น
private static void lambda$test$0();
  • กรณีที่ lambda expression อ้างถึงตัวแปรใน outer scope (ตัวอย่างการเรียกใช้งานในเมธอด mimic_call_lambda$test$1(String captureVariable)) target method ที่ได้จะออกมาเป็น
 private static void lambda$test$1(java.lang.String);

จะสังเกตว่าจะมีการรับค่าที่ capture มาจาก outer scope มาด้วย

ตัวอย่าง bytecode ที่ javac สร้างมาให้นะครับ

private static void lambda$test$1();
    descriptor: ()V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #18                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #26                 // String Hello From Lambda. no capture variable
         5: invokevirtual #25                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 15: 0

  private static void lambda$new$0();
    descriptor: ()V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #18                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #27                 // String dd
         5: invokevirtual #25                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 9: 0
}

จบแล้วครับสำหรับ invokedynamic หรือ Indy!

สำหรับใครที่อยากรู้ว่า javac อิมพรีเมนต์ annonymous inner class อย่าไร รวมถึงวิธีการที่มัน cature variable จาก outer scope
อ่านได้จากที่นี่

source code

Top comments (0)