在 Android VM 上 使用 Java8-12
TLDR:“不是所有的Java8 Feature 都会被 D8 desugar”
在 D8
被引进作为替代 dx
的dex工具后,越来越多的人问我是不是可以在项目里用原生lambda来替换Retrolambda,或者问各种新特性在Android上的支持程度。
本文主要讨论在Android VM中能否用、以及如何用这些新语言特性。
Java8
TLDR:真正能Natively用的特性
- Api Level 24及以上能用 interface methods (default methods or static methods) 和 Method reference
- Api Level 26及以上能用 lambda
lambda
- Q: 在低Api Level上为什么不能 natively 用呢?
- A: 是因为 Native lambda 用到了 VM 指令 invoke-dynamic ,这个指令在低版本上不支持
-
Q: 在低Api Level上怎么用呢?
-
A1: 用 Retrolambda
-
A2: 或者用 D8 的 desugar: 将 lambda 函数体抽取出来,提成一个静态方法;创建一个内部类,构造函数的参数实现 context capture,内部类里生成一个实例方法调用外部类的静态方法,最后原来调用 lambda 的地方调用 new 内部类的实例方法
-
Q: 如果用了D8,且在D8的命令中指定 min-api 为26,是不是 lambda 就是 Native lambda 了呢?
-
A: 不是! 是因为,Native lambda 采用的是 BootstrapMethod (也就是在 bytecode 第一次执行的时候,用来定义行为的,native lambda 生成的 invoke-dynamic method 就在这时定义),BootstrapMethod 用到了 java/lang/invoke.* 一些类,可惜 Android runtime不提供这些类,所以还是不行,所以D8为了避免 runtime error ,还是 desugar 了 lambda
LocalDateTime
LocalDateTime 的新用法要小心,D8不会帮你 Desugar ,LocalDateTime.now()编写时不会报任何错误,但是跑起来就不一定了,在 26以下会报 ClassNotFound 错
em…. 那就用 ThreeTenBP 吧 😄
Interface methods
D8 会在编译后 Desugar 好,同样如果指定 –min-api 24 的话,D8 不会 desugar,使用 native interface methods
Java9
- Sad fact : Java9 目前还没有被引进 Android SDK.
- Fun fact : ART 在运行时用 bytecode instructions 支持了某些Java9特性,例如 private interface methods.
下面的新特性为了避免因为用得少而陌生,我先把例子写出来,然后再阐述是否可以在AndroidVM上使用。
Concise Try with Resources
如果你尝试在 Java 中读取一个流,然后关闭一个流,你应该会写很多 try catch
Java9 中可以这样写了:
import java.io.*;
class Java9TryWithResources {
String effectivelyFinalTry(BufferedReader r) throws IOException {
try (r) {
return r.readLine();
}
}
}
这个特性在 Java compiler 中实现,不需要特殊 VM 指令,所以 Android 里随便用
Anonymous Diamond
钻石语法 Java7 就有了,但是直到 Java9 才有匿名类的钻石语法,像这样:
import java.util.concurrent.*;
class Java9AnonymousDiamond {
Callable<String> anonymousDiamond() {
Callable<String> call = new Callable<>() {
@Override public String call() {
return "Hey";
}
};
return call;
}
}
同样,这个特性在 Java compiler 中实现,不需要特殊 VM 指令,所以 Android 里随便用
Private interface methods
Java8 引入了 default methods & static methods for interfaces, 但是如果想要复用代码且不暴露给外界,必然需要 private methods,所以 Java9 引入了这个特性。
上面说到,D8 在给 Java8 的 interface default methods & static methods 的 desugar 和 non desugar 的情况,这里的 private methods 是一样的。
上面说过的这些特性虽然在Android SDK里包括,但是如果你 javac 后,再 D8,adb push 到 sdcard 上,用 adb shell dalvikvm -cp /sdcard/path/to/classes.dex
执行的话,会得到正确执行结果。
String concatenation
Java9 之前的 String concat,都会在 bytecode 里转换成 StringBuilder,有多少次 concat 就有多少次append
Java9 的 concat 用了 invoke-dynamic 来委托给 StringConcatFactory
返回这些代码,也就是说,不管里怎么改 concat,实际上编译后的 bytecode 不会频繁发生改变,提高性能。
至此,我们知道 Java9 所有的 language feature 都被 D8 搞定了,虽然 Android SDK 没有引入任何 Java9的 class,但是仍然可以被 Google 魔改的 ART 和 D8 支持。
你只要用了 D8,且 -min-api 21 Java9 特性随便用,至于 Java8 么,呵呵,还要 -min-api 26 才能随便写,what a world!
Java10
TLDR:很多新特性都是在 Java compiler 中实现的,所以和 Runtime 的支持与否无关
Local variable type inference
var: 局部变量的类型推断
public class Main {
public static void main(String[] args) {
}
List<String> localVariableTypeInference() {
var url = new ArrayList<String>();
return url;
}
}
同样的,本地变量类型推断仍然是在 java compiler 里做的,所以 Android 里随便用
Java11
Type inference for lambda parameters
对 Java10 的类型推断的一个优化,看如下的代码实例,新旧用法对比:
public class Main {
public static void main(String[] args) {
}
void lambdaParameterTypeInference() {
// old ways
Function<String, String> normal = (String param) -> param + param;
// now
Function<String, String> function = (var param) -> param;
}
}
虽然这个特性看起来不起眼,因为大家平时用lambda的时候都会隐藏类型声明。但是如果是你想加一些 annotation,那就变的很方便:例如,上述代码如果需要在param上加@Nonnull注解,你不需要去翻阅代码去确认 param 的类型,直接把上述方法改为:
public class Main {
public static void main(String[] args) {
}
void lambdaParameterTypeInference() {
// old ways
Function<String, String> normal = (String param) -> param + param;
// now
Function<String, String> function = (@NonNull var param) -> param;
}
}
同样的,这个特性仍然是在 java compiler 里做的,所以 Android 里随便用
Nestmates
Java11 中Natively支持了嵌套类。 我们知道在以前在Java中编写嵌套的两个类,编译过后这两个类分别是两个class文件,VM里只是用命名规范来区别这两个类,例如如下Java类:
class Outer {
private String hello = "hello";
class Inner {
String helloWorld() {
return hello + "world!";
}
}
}
编译后就会变为:
$ javac *.java
$ ls
Outer.java Outer.class Outer$Inner.class
写过内部类的Handler的我们都知道,一旦内部类用了外部类的 method 或者 property 那么在编译时会 capture 外部类的实例,从而造成对外部类的强引用。 解决过OOM的人一定对此深恶痛绝,因为要解决这个问题要写很繁琐的 WeakReference。
在Java11中编译后的类会变为:
$ javac *.java
$ javap -v -p *.class
class Outer {
private java.lang.String name;
}
NestMembers:
Outer$Inner
class Outer$Inner {
final Outer this$0;
Outer$Inner(Outer);
…
java.lang.String sayHi();
…
}
NestHost: class Outer
注意到NestMembers
和NestHost
属性,在VM中不再是通过命名来区别嵌套类了。这样,在VM级别 Outer 和 Inner 通过可见性关键词就可以互相访问 package-private
and private
级别及以下的方法和属性。
很遗憾,ART 不支持这个特性,所以得靠 D8 来 desugar,但是貌似D8没有对class文件中的NestMembers
和NestHost
进行支持,所以目前Android中不能用。
Java12
虽然Java12仍在预览版,不过我们可以通过 EA-build 来了解具体有哪些特性,到写这篇文章时:expression switch
and string literals
expression switch:
switch (s) {
case "hello", "Hello" -> 0;
case "world" -> 1;
default -> s.length();
}
string literals:
String script = `function hello() {
print('"Hello World"');
}
hello();
`;
同样,上述特性都是 Java compiler 支持的,所以 Android 里随便用
Written on December 7, 2018
转载请联系我,微信号: michaelzhoujay