I'm currently looking at the bytecode that jack
compiler generates for lambda expressions.
Let's take the following plain Java class as an example:
public class ForEach {
public static void main (String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("hello");
list.forEach(s -> System.out.println(s));
}
}
When using JDK 1.8's javac
, it generates the following bytecode:
public class ForEach
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
{
// .. redacted
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: ldc #4 // String hello
11: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
14: pop
15: aload_1
16: invokedynamic #6, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
21: invokevirtual #7 // Method java/util/ArrayList.forEach:(Ljava/util/function/Consumer;)V
24: return
private static void lambda$main$0(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
7: return
LineNumberTable:
line 7: 0
}
BootstrapMethods:
0: #27 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:
#28 (Ljava/lang/Object;)V
#29 invokestatic ForEach.lambda$main$0:(Ljava/lang/String;)V
#30 (Ljava/lang/String;)V
As per documentation, the translation of lambda expression to bytecode performed by generating invokedynamic call site
at line 16, and by converting the body of lambda expression into a static method.
Now, let's talk about jack
. Generally, when compiling lambda with minSdkVersion
that is less than 24, the jack
compiler generates an anonymous class - for backward compatibility with previous runtimes that are based on 1.6/1.7.
But what happen when compiling lambda with minSdkVersion
set to 24?
To answer this, I created same lambda expression (as in Java sample above) and compiled it with jack
in Android environment:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ArrayList<String> list = new ArrayList<>();
list.add("hello");
list.forEach(s -> System.out.println(s));
}
}
In this case I expected jack
to generate more performant/optimized code (something similar to JVM's invokedynamic call site
creating and lambda body helper method generating), since Android 7.0's runtime is based on 1.8.
I knew that invokedynamic
instruction operand is not supported by neither Dalvik and ART, but thought that there will be a more optimized mechanism - otherwise lambda expression support in Android is just a syntactic sugar..
Instead, the generated bytecode included same anonymous class:
Class #48 -
Class descriptor : 'Linfo/osom/java8demo/MainActivity;'
// .. redacted
Virtual methods -
#0 : (in Linfo/osom/java8demo/MainActivity;)
name : 'onCreate'
type : '(Landroid/os/Bundle;)V'
access : 0x0004 (PROTECTED)
[0014a0] info.osom.java8demo.MainActivity.onCreate:(Landroid/os/Bundle;)V
0000: invoke-super {v2, v3}, Landroid/app/Activity;.onCreate:(Landroid/os/Bundle;)V // method@0001
0003: const/high16 v0, #int 2130903040 // #7f03
0005: invoke-virtual {v2, v0}, Linfo/osom/java8demo/MainActivity;.setContentView:(I)V // method@0020
0008: new-instance v0, Ljava/util/ArrayList; // type@005b
000a: invoke-direct {v0}, Ljava/util/ArrayList;.<init>:()V // method@003f
000d: const-string/jumbo v1, "hello" // string@000000b9
0010: invoke-virtual {v0, v1}, Ljava/util/ArrayList;.add:(Ljava/lang/Object;)Z // method@0041
0013: new-instance v1, Linfo/osom/java8demo/MainActivity$-void_onCreate_android_os_Bundle_savedInstanceState_LambdaImpl0; // type@003c
0015: invoke-direct {v1}, Linfo/osom/java8demo/MainActivity$-void_onCreate_android_os_Bundle_savedInstanceState_LambdaImpl0;.<init>:()V // method@001b
0018: invoke-virtual {v0, v1}, Ljava/util/ArrayList;.forEach:(Ljava/util/function/Consumer;)V // method@0042
001b: return-void
// .. redacted
Why jack
compiler uses the anonymous class approach to represent lambda expression in bytecode instead of more efficient one, perhaps similar to JVM's invokedynamic
feature?