This is the 15th day of my participation in Gwen Challenge

This article is the second in jakewharton’s series on D8 and R8.

  • Android’s Java 9, 10, 11, and 12 Support
  • Originally written by jakewharton
  • Translator: Antway

The first article in this series explored Android’s support for Java 8. Although Android’s support for Java 8 language features and apis is not yet fully covered. But D8 already provides us with some of the core language features.

In the previous article, there was a lot of feedback that the Java 8 version was too old, and parts of the Java ecosystem started migrating to Java 11 (the first long-term support release since Java 8) after using Java 9 and 10. I’m glad the last post had reader feedback, because that’s what led to this post.

With the increasing frequency of Java releases, Android’s annual release schedule and delayed support for new language features and apis can feel uncomfortable. But are we really bothered by Java 8? Let’s take a look at what features are available in Java 8 and beyond and see how the Android toolchain works.

1. Java 9

In the last 2-3 years of release planning, Java 9 has included some language features, but they are not as important as Lambda and are more about cleaning up some edge apis.

1.1 the Try With the Resources

To simplify the code, use try-with-resources, which allows you to create a temporary variable such as try (Closeable bar = foo.bar()). But if a Closeable object is already defined, it is unnecessary to define a new variable. Therefore, this version allows you to omit the declaration of a new variable if you already have a valid final reference.

import java.io.*;

class Java9TryWithResources {
  String effectivelyFinalTry(BufferedReader r) throws IOException {
    try (r) {
      returnr.readLine(); }}}Copy the code

This feature is already fully supported in Java, so D8 is already supported when dex is packaged.

$ javac *.java

$ java -jar d8.jar \
    --lib $ANDROID_HOME/platforms/android-28/android.jar \
    --release \
    --output . \
    *.class

$ ls
Java9TryWithResources.java  Java9TryWithResources.class  classes.dex
Copy the code

Unlike the Lambda and static interface methods features in Java 8, Java 9 features are already supported on all versions of the Android API.

1.2 Anonymous Diamond

The diamond operation, introduced in Java 7, simplifies generic initialization and makes code more readable.

List<String> strings = new ArrayList<>();
Copy the code

The code above does not specify String when new ArrayList<>(). This removes useless declarations, but it cannot be used for anonymous inner classes. In Java 9, however, it can be used with anonymous inner classes to improve code readability.

import java.util.concurrent.*;

class Java9AnonymousDiamond {
  Callable<String> anonymousDiamond(a) {
    Callable<String> call = new Callable<>() {
      @Override public String call(a) {
        return "Hey"; }};returncall; }}Copy the code

Again, the above approach is implemented entirely in the Java compiler, so the generated bytecode behaves as if String were explicitly specified. We look at the compiled bytecode through Javap.

$ javac *.java

$ javap -c *.class
class Java9AnonymousDiamond {
  java.util.concurrent.Callable<java.lang.String> anonymousDiamond();
    Code:
       0: new           #7 // class Java9AnonymousDiamond$1
       3: dup
       4: aload_0
       5: invokespecial #8  // Method Java9AnonymousDiamond$1."<init>":(LJava9AnonymousDiamond;)V
       8: areturn
}

class Java9AnonymousDiamondThe $1 implements java.util.concurrent.Callable<java.lang.String> {
  final Java9AnonymousDiamond this$0;

  Java9AnonymousDiamondThe $1(Java9AnonymousDiamond);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #1 // Field this$0:LJava9AnonymousDiamond;
       5: aload_0
       6: invokespecial #2 // Method java/lang/Object."
      
       ":()V
      
       9: return

  public java.lang.String call();
    Code:
       0: ldc           #3 // String Hey
       2: areturn
}
Copy the code

Because there is nothing special about the bytecode, D8 can handle this feature without any problems.

$ java -jar d8.jar \
    --lib $ANDROID_HOME/platforms/android-28/android.jar \
    --release \
    --output . \
    *.class

$ ls
Java9AnonymousDiamond.java  Java9AnonymousDiamond.class  Java9AnonymousDiamondThe $1.class  classes.dex
Copy the code

Apparently, another language feature, Android, is now available in all VERSIONS of the API.

1.3 Private Interface Methods

In the interface, static or default methods can be implemented repeatedly due to overwriting, and can be extracted as private functions if the methods are part of the class rather than the interface. Private methods decorated with private have been added to interfaces in Java 9.

interface Java9PrivateInterface {
  static String hey(a) {
    return getHey();
  }

  private static String getHey(a) {
    return "hey"; }}Copy the code

This is the first language feature to be supported in Java 9. Prior to this release, the use of private on interface members was disallowed. Since D8 already handles the default and static modifiers by desugmenting them, the private method can easily be handled using the same technical compatibility.

$ javac *.java

$ java -jar d8.jar \
    --lib $ANDROID_HOME/platforms/android-28/android.jar \
    --release \
    --output . \
    *.class

$ ls
Java9PrivateInterface.java  Java9PrivateInterface.class  classes.dex
Copy the code

Static and default modifiers are already supported in the ART environment of API 24. When we specify –min-api 24, both static and default modifiers will not be desuglified.

$ $ANDROID_HOME/build-tools/28.02./dexdump -d classes.dex
Class #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
Copy the code

By looking at the bytecode of the dex file, we can see that the getHey methods are still private and static, indicating that they have not been desugared. If we write a main method to call getHey, it will work on a machine running ON API 24, since ART is already supported in API 24.

These are the Java 9 language features that Android already supports. However, Java 9 apis are not fully supported by Android, such as the Process API, Variable Handles API, Reactive Streams API, and so on.

1.4 the String Concat

Each time we discuss a Java release, we talk a lot about language features, but each release will also be optimized for Bytecode, such as string concatenation in Java 9.

class Java9Concat {
  public static String thing(String a, String b) {
    return "A: " + a + " and B: "+ b; }}Copy the code

We can compare what optimizations have been made with the Java 8 and Java 9 compilers. The bytecode is first compiled using Java 8’s JavAC.

$ java -version
java version "1.8.0 comes with _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 *.class class Java9Concat { public static java.lang.String thing(java.lang.String,  java.lang.String); Code: 0: new#2 // class java/lang/StringBuilder
       3: dup
       4: invokespecial #3 // Method java/lang/StringBuilder."
      
       ":()V
      
       7: ldc           #4 // String A:
       9: invokevirtual #5  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      12: aload_0
      13: 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_1
      22: 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
}
Copy the code

As you can see, StringBuilder is used here to connect. If we compile bytecode comparison using Java 9.

$ java -version
java 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 *.class class Java9Concat { public static java.lang.String thing(java.lang.String,  java.lang.String); Code: 0: aload_0 1: aload_1 2: invokedynamic#2, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String; Ljava/lang/String;) Ljava/lang/String; 7: areturn }Copy the code

A single Invokedynamic instruction replaces so many operations on StringBuilder, which is similar to the native Lambdas work on the JVM section in our previous article.

At JVM runtime, the JDK Class StringConcatFactory method makeConcatWithConstants is more efficient at concatenating characters, such as the need to recompile and the ability to preset the size of StirngBuilder.

The Android API doesn’t contain many of the apis in Java 9, so the StringConcatFactory class isn’t available at runtime yet, but thankfully, just as Android supports lambda, D8 has optimized support for StringConcatFactory through desugaring.

$ 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] java9concate.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>:()V 0005: 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 v2 0019: return-object v2Copy the code

This means that all of Java 9’s language features are available at all Android API levels.

Java is now released every 6 months. Java 9 is an older version. Can Android keep up with that?

2. Java 10

The biggest language feature in Java 10 is the local-variable Type inference (Local Variable Interface), which allows us to define variables using the VAR keyword to ignore types.

import java.util.*;

class Java10 {
  List<String> localVariableTypeInferrence(a) {
    var url = new ArrayList<String>();
    returnurl; }}Copy the code

From javac compilation:

$ javac *.java

$ javap -c *.class
Compiled from "Java10.java"
class Java10 {
  java.util.List<java.lang.String> localVariableTypeInferrence();
    Code:
       0: new           #2  // class java/util/ArrayList
       3: dup
       4: invokespecial #3  // Method java/util/ArrayList."<init>":()V
       7: areturn
}
Copy the code

No new APIS have been found in bytecode for this feature, so Android fully supports it. Java version 10, of course, there are also new API, such as Optional. OrElseThrow, List. The copyOf and CalcCurth TunMuffFielabelist. Once these apis are added to the Android SDK in the future, they can be supported by desugaring.

3. Java 11

Local-variable Type Inference (the Local variable Interface) was strengthened in Java 11 to support lambda.

import java.util.function.*;

@interface NonNull {}

class Java11 {
  void lambdaParameterTypeInferrence(a) {
    Function<String, String> func = (@NonNull varx) -> x; }}Copy the code

Like the local variable interface in Java 10, this feature in Java 11 is supported by Android.

New apis in Java 11 include helper classes for String, predicate. not, and added null I/O handling for Reader, Writer, InputSteam, and OutputStream.

Another major change to the API in Java 11 is the new HTTP client, java.net.http, which is already available for testing under the JDK.incubator. HTTP package in Java 9. It’s a huge family of apis, and we’ll see if Android supports it.

3.1 Nestmates (Nested Classes)

Optimized for character concatenation in Java 9, Java 11 fixes long-standing differences between Java source code and its class files and JVM nested classes.

Nested classes were introduced in Java 1.1, but do not conform to the class specification or are not recognized by the JVM, so to accommodate this problem, nested classes defined in the source file will be named according to certain rules to create a sibling of the source file.

class Outer {
  class Inner {}}Copy the code

We compiled using Java 10 or earlier.

$ java -version
java version "10" 2018-03-20
Java(TM) SE Runtime Environment 18.3 (build 10+46)
Java HotSpot(TM)64 - Bit Server 18.3 VM(build 10+46, mixed mode)

$ javac *.java

$ ls
Outer.java  Outer.class  Outer$Inner.class
Copy the code

We can see that two bytecode files are generated, which are independent of each other as far as the JVM is concerned, except that they exist under the same package. This may seem fine, but it will crash if the two access each other’s private methods.

class Outer {
  private String name;

  class Inner {
    String sayHi(a) {
      return "Hi, " + name + "!"; }}}Copy the code

Outer$Inner. SayHi () cannot access the private Outer. Name when the sibling class is generated. So to solve this situation, the Java compiler adds a package-private Synthetic Accessor Method processor to solve this situation.

 class Outer {
   private String name;
+
+  String access$000() {+returnname; +}class Inner {
     String sayHi(a) {-return "Hi, " + name + "!";
+      return "Hi, " + access$000() + "!";
     }
Copy the code

Let’s compile class Outer to see:

$ javap -c -p Outer.class
class Outer {
  private java.lang.String name;

  static java.lang.String accessThe $000(Outer);
    Code:
       0: aload_0
       1: getfield      #1 // Field name:Ljava/lang/String;
       4: areturn
}
Copy the code

From today’s point of view, this is at best a minor nuisance for the JVM. However, for Android, these synthetic accessor methods increase method counts in dex files, increase APK size, slow class loading and validation, and reduce performance by converting field lookups into method calls.

In Java 11, the class file format was updated to introduce the concept of nesting to describe these nesting relationships.

$ java -version
java version "11.0.1"2018-10-16 LTS Java(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 $javap -v -p *. Class Outer {private java.lang.String name; } NestMembers: Outer$Inner

class Outer$Inner {
  final Outer this$0;

  Outer$Inner(Outer); Code:... java.lang.String sayHi(); Code:... } NestHost: class OuterCopy the code

The output above is clipped, but we can also see that two class files are generated: Outer and Outer$Inner. The difference here is that Outer$Inner is a member of Out and holds a reference to Out, so Outer$Inner can access the external member directly.

Unfortunately, ART is not yet able to parse this operation.

$ java -jar d8.jar \
    --lib $ANDROID_HOME/platforms/android-28/android.jar \
    --release \
    --output . \
    *.class
Compilation failed with an internal error.
java.lang.UnsupportedOperationException
  at 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.ApplicationReader$ClassReader.lambda$readClassSourcesThe $1(ApplicationReader.java:231)
  at java.base/java.util.concurrent.ForkJoinTask$AdaptedCallable.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)
Copy the code

Unfortunately, there is still no support as I write this article. But ASM, a bytecode manipulation library, already supports such NestMates (nested access). D8 is not supported yet, you can make your issue about this feature on the Star the D8 feature Request.

Java 11 is not yet available on Android because NEstMates (nested access) is not yet implemented on D8.

4. Java 12

With the March 2019 release date approaching, Java 12 is getting closer and closer. The language features and apis have been in development for several months. With an early access build, we can download and try out these features today.

In the current EA build (No. 20), two new language features are available: Expression Switch (switch expressions) and String literals (string text).

class Java12 {
  static int letterCount(String s) {
    return switch (s) {
      case "one"."two" -> 3;
      case "three" -> 5;
      default -> s.length();
    };
  }

  public static void main(String... args) { System.out.println(` __ ______ ______ ______ ______ ______ ______ /\ \ /\ ___\ /\__ _\ /\__ _\ /\ ___\ /\ == \ /\ ___\ There comes \ \ \ \ \ __ \ \ _ / \ \ / \ / _ / \ \ / \ \ \ \ \ (< \ \ ___ \ \ \ \ \ \ \ \ \ _ _____ _____ \ \ \ _ \ \ \ \ \ \ _ \ \ _ \ _____ \ / \ \ \ / / / / / _____ _____ _____ \ _ \ / _ / / / / / / _ _____ / / _ \ / _____ / `); System.out.println("three: " + letterCount("three")); }}Copy the code

These two features are fully implemented as part of the Java compiler without any new bytecode or API.

$ java -version
openjdk version "12-ea" 2019-03-19
OpenJDK Runtime Environment (build 12-ea+20)
OpenJDK 64-Bit Server VM (build 12-ea+20, mixed mode, sharing)

$ javac *.java

$ java -jar d8.jar \
    --lib $ANDROID_HOME/platforms/android-28/android.jar \
    --release \
    --output . \
    *.class

$ ls
Java12.java  Java12.class  classes.dex
Copy the code

We can package this class and run it on a device.

$ADB push classes.dex /sdcard classes.dex: 1 file pushed. 0.6 MB/s (1792 bytesin0.003 s) $adb shell dalvikvm - cp/sdcard/classes. Dex Java12 __.................. / \ \ / \ ___ \ / \ __ _ \ \ __ _ \ / \ ___ \ / \ = = \ \ ___ there comes \ \ \ \ \ \ __ \ \ _ / \ \ / \ / _ / \ \ / \ \ \ \ \ (< \ \ ___ \ \ \ \ \ \ \ _____ _____  \ \_\ \ \_\ \ \_____\ \ \_\ \_\ \/\_____\ \/_____/ \/_____/ \/_/ \/_/ \/_____/ \/_/ /_/ \/_____/ three: 5Copy the code

Return statements written in switch expressions, and directly implemented in strings using newlines (previously possible with the \ N escape character).

As with all releases, there are new apis in Java 12, and again, if you need to add them to the Android SDK, you need to use desugaring.

Hopefully, by the time Java 12 is actually released, D8 will implement NestMates (nested access) for Java 11. Otherwise, the pain of being stuck on Java 10 will increase a lot!

5. To summarize

As the Java ecosystem moves forward with new releases, Java 8 language features are becoming less problematic, and it’s reassuring that all language features between Java 8 and Java 12 are already available on Android.

However, the final recommendation remains the same as in the previous article. Android’s support for apis and VM features in new versions of Java is critical. Without integrating the new apis into the SDK, they cannot (easily) be used by desugaring. If virtual machine functionality is not integrated into ART D8, there is a huge burden on all API levels, not just backward compatibility.

The next article will show how D8 solves bugs caused by specific versions and specific vendors in the VM.