}
}
通过 javac
指令编译为字节码后,然后通过 dx
工具编译打包为 dex
文件,但是出错了。
$ javac *.java
$ ls
Java8.java Java8.class Java8$Logger.class
$ $ANDROID_HOME/build-tools/28.0.2/dx --dex --output . *.class
Uncaught translation error: com.android.dx.cf.code.SimException:
ERROR in Java8.main:([Ljava/lang/String;)V:
invalid opcode ba - invokedynamic requires --min-sdk-version >= 26
(currently 13)
1 error; aborting
这是因为 lambda
使用了 invokedynamic
字节码指令,invokedynamic
是在 Java 7
中引入的。上面的错误信息提示,Android
支持这种字节码的最低版本是 26
。与此同时 Android
使用 desugaring
(脱糖)兼容所有 API
版本上使用 lambda
表达式。
2. Desugaring(脱糖)的历史
脱糖工具的发展史非常出彩,但是它的核心目标却是一致的:让所有的 Java
语言新特性都能运行在所有设备上。
Retrolambda 是最初支持 lambda
表达式的第三方工具库,它通过在编译时利用 JVM
指令将 lambda
转换为内部类来实现。然而生成的类会使方法数激增,但是随着时间的推移,使用该工具的成本降低到了合理的水平。
然后,Android
工具团队宣布了一个新的编译器,它将提供 Java 8
语言特性的支持,以及更好的性能。该工具是建立在 Eclipse Java
编译器上的,而不是 Dalvik Java
字节码之上的。虽然处理 Java 8
效率很高,但是它的体验很差以及无法与别的工具兼容。
最终新的编译器被舍弃,同时在 Android Gradle plugin 中引入了谷歌定制的字节码构建系统,因为脱糖是增量式的,所以脱糖的输出效率仍然不是很理想,与此同时,正在进行的工作有了更好的方案。
D8 编译工具问世了。D8
编译工具用来替代老的 dx
工具,同时在 D8
中集成了脱糖,以此取代脱糖作为一个独立的字节码转换模块的方式。D8
相比较 dx
有很大的提升,带来了更有效率的字节码转换。同时在 Android Gradle Plugin 3.1
中作为默认 dex
编译器,然后在 3.2
版本中 D8
又集成了脱糖。
3. D8
通过 D8
工具编译上面的例子成功了。
$ java -jar d8.jar
–lib $ANDROID_HOME/platforms/android-28/android.jar
–release
–output .
*.class
$ ls
Java8.java Java8.class Java8$Logger.class classes.dex
同时我们可以通过 Android
提供的 dexdump
工具来查看 dex
文件内容,看看 D8
是如何脱糖的,由于 dexdump
会产生很多代码,我们只截取一部分。
$ $ANDROID_HOME/build-tools/28.0.2/dexdump -d classes.dex
[0002d8] Java8.main:([Ljava/lang/String;)V
0000: sget-object v0, LJava8$1;.INSTANCE:LJava8
1
;
0002
:
i
n
v
o
k
e
−
s
t
a
t
i
c
v
0
,
L
J
a
v
a
8
;
.
s
a
y
H
i
:
(
L
J
a
v
a
8
1; 0002: invoke-static {v0}, LJava8;.sayHi:(LJava8
1;0002:invoke−staticv0,LJava8;.sayHi:(LJava8Logger;)V
0005: return-void
[0002a8] Java8.sayHi:(LJava8
L
o
g
g
e
r
;
)
V
0000
:
c
o
n
s
t
−
s
t
r
i
n
g
v
0
,
"
H
e
l
l
o
"
0002
:
i
n
v
o
k
e
−
i
n
t
e
r
f
a
c
e
v
1
,
v
0
,
L
J
a
v
a
8
Logger;)V 0000: const-string v0, "Hello" 0002: invoke-interface {v1, v0}, LJava8
Logger;)V0000:const−stringv0,"Hello"0002:invoke−interfacev1,v0,LJava8Logger;.log:(Ljava/lang/String;)V
0005: return-void
…
在 main
方法中,对应 0000
位置创建了一个 Java8$1
类对象 INSTANCE
实例,但是我们的源文件中并不包含这个类,所以猜测这个类是由脱糖产生的。同时 main
方法的字节码中也没有包含任何 lambda
的实现,所以很可能是在 Java8$1
中实现的。在 0002
位置,INSTANCE
调用了 sayHi
方法,同时可以看到 sayHi
方法的参数是 LJava8$Logger
,所以基本可以确定 Java8$1
类实现了 lambda
中的接口。我们可以输出字节码进行验证。
Class #2 -
Class descriptor : ‘LJava8KaTeX parse error: Expected 'EOF', got '#' at position 118: …faces - #̲0 …Logger;’
SYNTHETIC
字节码标签代表着这个类是由系统产生,通过 Interfaces
可以看到 LJava8$1
类实现了 LJava8$Logger
接口。
现在 LJava8$1
的实现已经替代了 lambda
,我们可以通过查看 sayHi
方法的字节码实现。
…
[00026c] Java8
1.
l
o
g
:
(
L
j
a
v
a
/
l
a
n
g
/
S
t
r
i
n
g
;
)
V
0000
:
i
n
v
o
k
e
−
s
t
a
t
i
c
v
1
,
L
J
a
v
a
8
;
.
l
a
m
b
d
a
1.log:(Ljava/lang/String;)V 0000: invoke-static {v1}, LJava8;.lambda
1.log:(Ljava/lang/String;)V0000:invoke−staticv1,LJava8;.lambdamain$0:(Ljava/lang/String;)V
0003: return-void
…
在 sayHi
的字节码实现中,它调用了 Java8
类中的静态方法 lambda$main$0
,但是我们并没有在类中定义这个方法,所以我们只能查看下 Java8
类对应的字节码。
…
#1 : (in LJava8;)
name : 'lambda$main
0
′
t
y
p
e
:
′
(
L
j
a
v
a
/
l
a
n
g
/
S
t
r
i
n
g
;
)
V
′
a
c
c
e
s
s
:
0
x
1008
(
S
T
A
T
I
C
S
Y
N
T
H
E
T
I
C
)
[
0002
a
0
]
J
a
v
a
8.
l
a
m
b
d
a
0' type : '(Ljava/lang/String;)V' access : 0x1008 (STATIC SYNTHETIC) [0002a0] Java8.lambda
0′type:′(Ljava/lang/String;)V′access:0x1008(STATICSYNTHETIC)[0002a0]Java8.lambdamain$0:(Ljava/lang/String;)V
0000: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream;
0002: invoke-virtual {v0, v1}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V
0005: return-void
在这里我们通过 SYNTHETIC
标签可以确定 lambda$main$0
方法是由系统自动生成的,并且看到了 lambda
实现的方法体 System.out.println
。
通过上面的流程分析,我们可以推测出:lambda
的实现保持在原来的主类中,并且是私有的,别的类无法直接访问。
4. Source Transformation(源码模拟实现)
为了更好的理解脱糖是如何工作的,我们可以在源码的层面模拟实现,注意这里的模拟仅仅是为了加深理解,脱糖实际工作比这个要复杂的多。
class Java8 {
interface Logger {
void log(String s);
}
public static void main(String… args) {
sayHi(s -> System.out.println(s));
}
private static void sayHi(Logger logger) {
logger.log(“Hello!”);
}
}
第一步将 lambda
表达式移到同级的包私有方法。
public static void main(String… args) {
- sayHi(s -> System.out.println(s));
- sayHi(s -> lambda$main$0(s));
} - static void lambda$main$0(String s) {
- System.out.println(s);
- }
第二步生成一个内部类实现 Logger
接口,并且它的方法体调用刚才实现的 lambda
方法。
public static void main(String… args) {
- sayHi(s -> lambda$main$0(s));
- sayHi(new Java8$1());
}
@@
}
+class Java8$1 implements Java8.Logger {
- @Override public void log(String s) {
- Java8.lambda$main$0(s);
- }
+}
最后,因为 lambda
方法并没有依赖外部的任何类,所以我们在 Java8$1
内部创建一个单例对象来避免每次调用 lambda
方法都生成一个新对象。
public static void main(String… args) {
- sayHi(new Java8$1());
- sayHi(Java8$1.INSTANCE);
}
@@
class Java8$1 implements Java8.Logger { - static final Java8$1 INSTANCE = new Java8$1();
@Override public void log(String s) {
最终我们经过脱糖生成的文件适用与所有 APIs
。
class Java8 {
interface Logger {
void log(String s);
}
public static void main(String… args) {
sayHi(Java8$1.INSTANCE);
}
static void lambda$main$0(String s) {
System.out.println(s);
}
private static void sayHi(Logger logger) {
logger.log(“Hello!”);
}
}
class Java8$1 implements Java8.Logger {
static final Java8$1 INSTANCE = new Java8$1();
@Override public void log(String s) {
Java8.lambda$main$0(s);
}
}
实际上你在查看 lambda
表达式生成的 Dalvik
字节码时可能看到不是类似 Java8$1
的名称,而是像这样的 -$$Lambda$Java8$QkyWJ8jlAksLjYziID4cZLvHwoY
名称,这是由于命名规范不恰当引起的。
5. Native Lambdas
在上面我们通过 dx
工具编译 dex
文件时,错误信息提示我们最低的支持版本是 API 26。
$ $ANDROID_HOME/build-tools/28.0.2/dx --dex --output . *.class
Uncaught translation error: com.android.dx.cf.code.SimException:
ERROR in Java8.main:([Ljava/lang/String;)V:
invalid opcode ba - invokedynamic requires --min-sdk-version >= 26
(currently 13)
1 error; aborting
所以如果我们在使用 D8
的时候指定 --min-api 26
版本,应该就不会报错了。
$ java -jar d8.jar
–lib $ANDROID_HOME/platforms/android-28/android.jar
–release
–min-api 26
–output .
*.class
同样为了查看 D8
如何工作,我们还是查看 Java8
类的字节码。
$ javap -v Java8.class
class Java8 {
public static void main(java.lang.String…);
Code:
0: invokedynamic #2, 0 // InvokeDynamic #0:log:()LJava8KaTeX parse error: Expected 'EOF', got '#' at position 26: … invokestatic #̲3 // Metho…Logger;)V
8: return
}
…
为了阅读方便我只截取了部分代码,但是我们同样可以在 main
方法中看到这里使用了 InvokeDynamic
指令,在 Code
表的 0
位置上,我们可以看到第二个参数是 0
,对应着 bootstrap method(引导方法)
。 bootstrap method
(引导方法)是当字节码第一次执行时首先被执行的一小段代码。
…
BootstrapMethods:
0: #27 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(
Ljava/lang/invoke/MethodHandlesKaTeX parse error: Expected 'EOF', got '#' at position 194: …hod arguments: #̲28 (Ljava/lang/…main$0:(Ljava/lang/String;)V
#28 (Ljava/lang/String;)V
在上面的代码中,bootstrap method
(引导方法)对应的是 java.lang.invoke.LambdaMetafactory 类中的 metafactory
方法。LambdaMetafactory
类在运行时为 lambda
表达式生成匿名类,而 D8
是在编译时生成。
如果我们查看 Android documentation for java.lang.invoke 和 AOSP source code for java.lang.invoke 的文档,我们可以注意到这个类在 Android Runtime
中不存在,这也是为什么脱糖在编译时要求最小版本的原因。VM
环境支持 invokedynamic
指令,但是 JDK 在编译 LambdaMetafactory 中却不可用。
最后
现在其实从大厂招聘需求可见,在招聘要求上有高并发经验优先,包括很多朋友之前都是做传统行业或者外包项目,一直在小公司,技术搞的比较简单,没有怎么搞过分布式系统,但是现在互联网公司一般都是做分布式系统。
所以说,如果你想进大厂,想脱离传统行业,这些技术知识都是你必备的,下面自己手打了一份Java并发体系思维导图,希望对你有所帮助。
` 指令,但是 JDK 在编译 LambdaMetafactory 中却不可用。
最后
现在其实从大厂招聘需求可见,在招聘要求上有高并发经验优先,包括很多朋友之前都是做传统行业或者外包项目,一直在小公司,技术搞的比较简单,没有怎么搞过分布式系统,但是现在互联网公司一般都是做分布式系统。
所以说,如果你想进大厂,想脱离传统行业,这些技术知识都是你必备的,下面自己手打了一份Java并发体系思维导图,希望对你有所帮助。
[外链图片转存中…(img-fMS7mFvp-1714483765902)]