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

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay