[译] Android 的 Java 9,10,11,作为程序员一定不要仅仅追求物质
}
这是 Java 9
中第一个需要支持的语言功能。在此版本之前,不允许在接口成员上使用 private
。由于 D8
已经通过脱糖处理了 default
和 static
修饰符,private
方法很容易使用相同的技术兼容处理。
$ javac *.java
$ java -jar d8.jar
--lib $ANDROID_HOME/platforms/android-28/android.jar
--release
--output .
*.class
$ lsJava9PrivateInterface.java Java9PrivateInterface.class classes.dex
在 API 24
的 ART
环境中已经支持 static
和 default
修饰符,当我们指定 --min-api 24
时,static
和 default
修饰的方法都不会进行脱糖。
ANDROID_HOME/build-tools/28.0.2/dexdump -d classes.dexClass #1 -Class descriptor : 'LJava9PrivateInterface;'Access flags : 0x0600 (INTERFACE ABSTRACT)Superclass : 'Ljava/lang/Object;'Direct methods -#0 : (in LJava9PrivateInterface;)name : 'getHey'type : '()Ljava/lang/String;'access : 0x000a (PRIVATE STATIC)00047c: |[00047c] Java9PrivateInterface.getHey:()Ljava/lang/String;00048c: 1a00 2c00 |0000: const-string v0, "hey"000490: 1100 |0002: return-object v0
通过查看 dex
文件的字节码,我们可以看到 getHey
方法仍然是 private
和 static
类型的,说明没有被脱糖。如果我们写个 main
方法调用 getHey
,在 API 24
的机器上是可以正常运行,因为 ART
在 API 24
版本已经支持。
上面就是 Java 9
的语言特性,并且 Android
已经支持。但是 Java 9
中的 API
还没有被 Android
全面支持,比如 Process API
、Variable Handles API
、Reactive Streams API
等等。
1.4 String Concat
每次讨论 Java
版本的发布,我们讨论语言特性比较多,但是每个版本也会针对 bytecode
进行优化,比如 Java 9
中的字符串连接。
class Java9Concat {public static String thing(String a, String b) {return "A: " + a + " and B: " + b;}}
究竟做了哪些优化,我们可以通过 Java 8
和 Java 9
的编译器进行对比。首先使用 Java 8
的 javac
进行编译字节码。
$ java -versionjava version "1.8.0_192"Java(TM) SE Runtime Environment (build 1.8.0_192-b12)Java HotSpot(TM) 64-Bit Server VM (build 25.192-b12, mixed mode)
$ javac *.java
$ javap -c *.classclass Java9Concat {public static java.lang.String thing(java.lang.String, java.lang.String);Code:0: new #2 // class java/lang/StringBuilder3: dup4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V7: ldc #4 // String A:9: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;12: aload_013: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;16: ldc #6 // String and B:18: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;21: aload_122: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;25: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;28: areturn}
我们可以看到,这里使用的是 StringBuilder
进行连接。如果我们用 Java 9
来编译字节码对比。
$ java -versionjava version "9.0.1"Java(TM) SE Runtime Environment (build 9.0.1+11)Java HotSpot(TM) 64-Bit Server VM (build 9.0.1+11, mixed mode)
$ javac *.java
$ javap -c *.classclass Java9Concat {public static java.lang.String thing(java.lang.String, java.lang.String);Code:0: aload_01: aload_12: invokedynamic #2, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;7: areturn}
一个 invokedynamic
指令就替代了 StringBuilder
的那么多操作,这个操作跟我们上一篇的 native lambdas work on the JVM
章节很类似。
在 JVM
的运行时期, JDK class StringConcatFactory 使用 makeConcatWithConstants
方法在连接字符时效率更好,比如不必重新编译以及可以预置 StirngBuilder
的大小。
Android API
没有包含 Java 9
中的太多 API
,所以在运行时期还无法使用 StringConcatFactory
类,不过值得庆幸的是,正如 Android
对 lambda
的支持,D8
已经通过脱糖实现优对 StringConcatFactory
的支持。
$ java -jar d8.jar
--lib $ANDROID_HOME/platforms/android-28/android.jar
--release
--output .
*.class
ANDROID_HOME/build-tools/28.0.2/dexdump -d classes.dex[000144] Java9Concat.thing:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;0000: new-instance v0, Ljava/lang/StringBuilder;0002: invoke-direct {v0}, Ljava/lang/StringBuilder;.<init>:()V0005: const-string v1, "A: "0007: invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;000a: invoke-virtual {v0, v2}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;000d: const-string v2, " and B: "000f: invoke-virtual {v0, v2}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;0012: invoke-virtual {v0, v3}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;0015: invoke-virtual {v0}, Ljava/lang/StringBuilder;.toString:()Ljava/lang/String;0018: move-result-object v20019: return-object v2
这意味着 Java 9
的所有语言特征都可以在 Android
所有 API
级别上使用。
Java
现在每隔 6
个月发版一次,Java 9
已经算是老版本,Android
能保持同步进行吗?
2. Java 10
Java 10
中最大的语言特性是 local-variable type inference
(局部变量接口),它允许我们使用 var
关键字定义变量来忽略类型。
import java.util.*;
class Java10 {List<String> localVariableTypeInferrence() {var url = new ArrayList<String>();return url;}}
通过 javac
编译来看:
$ javac *.java
$ javap -c *.classCompiled from "Java10.java"class Java10 {java.util.List<java.lang.String> localVariableTypeInferrence();Code:0: new #2 // class java/util/ArrayList3: dup4: invokespecial #3 // Method java/util/ArrayList."<init>":()V7: areturn}
针对这个特性在字节码中没有发现新的 API
,所以 Android
也是完全支持的。当然,Java 10
版本中也有新的 API
,比如 Optional.orElseThrow
、List.copyOf
和 CalcCurth.TunMuffFielabelist
。一旦在未来将这些 API
添加到 Android SDK
中,这些 API
就可以通过脱糖来支持。
3. Java 11
Local-variable type inference
(局部变量接口)在 Java 11
中得到了加强,它可以支持 lambda
。
import java.util.fu
nction.*;
@interface NonNull {}
class Java11 {void lambdaParameterTypeInferrence() {Function<String, String> func = (@NonNull var x) -> x;}}
和 Java 10
中的局部变量接口一样,Java 11
的这个特性也是被 Android
支持的。
Java 11
中提供的新 API
比如,String
、Predicate.not
的辅助类以及 Reader
、Writer
、InputSteam
和 OutputStream
增加的空 IO
处理。
另一个 Java 11
中的重大变更 API
是 new HTTP client, java.net.http,其实这个 API
在 Java 9
的 jdk.incubator.http
包下已经可以试用。这个 API
系列非常庞大,Android
是否支持,我们拭目以待?
3.1 Nestmates(嵌套类)
在 Java 9
中针对字符连接进行了优化,那么 Java 11
中对长期存在的 Java
源代码与其类文件和 JVM
嵌套类之间的长期差异进行了修复。
在 Java 1.1
中引入了嵌套类,但是不符合类规范或 JVM
不识别,所以为了兼容这个问题,在源文件中的定义的嵌套类将按照一定的命名规则来创建一个源文件的兄弟类。
class Outer {class Inner {}}
我们使用 Java 10
或以前的版本编译。
$ java -versionjava version "10" 2018-03-20Java(TM) SE Runtime Environment 18.3 (build 10+46)Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10+46, mixed mode)
$ javac *.java
Inner.class
我们可以看到生成了两个字节码文件,对于 JVM
而言,他们是相互独立的,除了存在同一个包下。这种处理看似没问题,但是当二者之间如果出现相互访问 private
方法的情况就会奔溃。
class Outer {private String name;
class Inner {String sayHi() {return "Hi, " + name + "!";}}}
当被生成兄弟类时,Outer$Inner.sayHi()
无法访问私有的 Outer.name
。所以为了解决这种情况,Java 编译器增加了 package-private synthetic accessor method
处理器来解决这种情况。
class Outer {private String name;+
String access$000() {
return name;
}
class Inner {String sayHi() {
}
我们编译 Outer
类来看一下:
$ javap -c -p Outer.classclass Outer {private java.lang.String name;
static java.lang.String access$000(Outer);Code:0: aload_01: getfield #1 // Field name:Ljava/lang/String;4: areturn}
从今天的角度来看,这对 JVM
来说至多只是一个小麻烦。不过,对于 Android
,这些合成访问器方法增加了 dex
文件中的方法计数,增加 apk
大小,降低类加载和验证的速度,将字段查找转换为方法调用同时也使性能降低。
在 Java 11
中,更新了类文件格式用来引入嵌套的概念来描述这些嵌套关系。
$ java -versionjava version "11.0.1" 2018-10-16 LTSJava(TM) SE Runtime Environment 18.9 (build 11.0.1+13-LTS)Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.1+13-LTS, mixed mode)
$ javac *.java
Inner
class OuterInner {final Outer this0;
Outer$Inner(Outer);Code: …
java.lang.String sayHi();Code: …}NestHost: class Outer
上面的输出被裁剪了,但是我们也能看到这里生成了两个类文件:Outer
和 Outer$Inner
。这里的不同是在于 Outer$Inner
作为一个 Out
的成员以及 Outer$Inner
持有 Out
的一个引用,所以 Outer$Inner
可以直接访问外部成员。
遗憾的是 ART
现在还无法解析这种操作。
$ java -jar d8.jar
--lib $ANDROID_HOME/platforms/android-28/android.jar
--release
--output .
*.classCompilation failed with an internal error.java.lang.UnsupportedOperationExceptionat com.android.tools.r8.org.objectweb.asm.ClassVisitor.visitNestHostExperimental(ClassVisitor.java:158)at com.android.tools.r8.org.objectweb.asm.ClassReader.accept(ClassReader.java:541)at com.android.tools.r8.org.objectweb.asm.ClassReader.accept(ClassReader.java:391)at com.android.tools.r8.graph.JarClassFileReader.read(JarClassFileReader.java:107)at com.android.tools.r8.dex.ApplicationReaderreadClassSourcesAdaptedCallable.exec(ForkJoinTask.java:1448)at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020)at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656)at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594)at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)
非常遗憾,在我写这篇文章的时候还是没有支持。但是 ASM
(一个字节码操作库) 已经支持了这种 nestmates
(嵌套访问)。D8
还没有支持,你可以在 star the D8 feature request 上提出你对这个特性的 issue
。
评论