Java 8 Interface Methods for Android

- 8 mins

Java 8 language feature support using desugar bytecode transformations.

Java 8 language feature support using desugar bytecode transformations.


Recently, I enjoyed reading a blog post by Jake Wharton about how Android supports Java 8 features using D8.

The blog post goes through the following processes to understand how D8 works:

  1. Write Java code. (.java)
  2. Compile to ByteCode.(.class)
  3. Compile to Dalvik Executable. (.dex)
  4. Analysis of the generated files.

In the blog post, the above process allows us to understand what happens under the hood when some Java 8 features (Lambdas and APIs) are desugared using D8.

In this post, we will use the same process to understand how default methods and static methods in Java 8 interfaces are desugared using D8. To better understand this post, I heavily recommend reading Jake Warthon’s post first.

Compile Java 8 Code

We will try to analyse the following code :

class Java8 {

  interface Logger {
    void log(String s);

    default void log(String tag, String s) {
      log(tag + ": " + s);
    }

    static Logger systemOut() {
      return System.out::println;
    }
  }

  public static void main(String... args) {
    sayHi(s -> System.out.println(s));
    Logger.systemOut().log("hello from static");
  }

  private static void sayHi(Logger logger) {
    logger.log("Hello!");
    logger.log("hello from", "default");
  }
}

We compile the java code:

$ javac *.java
$ ls
Java8.java  Java8.class  Java8$Logger.class

Executing the above code gives the following output:

$ java Java8
Hello!
hello from: default
hello from static

Then we compile the bytecode to dex using D8:

$ $ANDROID_HOME/build-tools/28.0.2/d8 --release --lib $ANDROID_HOME/platforms/android-28/android.jar --output . *.class
$ ls
Java8.java  Java8.class  Java8$Logger.class  classes.dex

Our focus here is the default and static methods in the Logger interface.

Dex Analysis

To see how D8 desugared interface’s static and default methods, we will use dexdump:

$ $ANDROID_HOME/build-tools/28.0.2/dexdump -d classes.dex

We get a lot of output (the full output can be found here).

Default Methods

Firs, we find the following output:

Class #0            -
  Class descriptor  : 'LJava8$Logger-CC;'
  Access flags      : 0x1011 (PUBLIC FINAL SYNTHETIC)
  Superclass        : 'Ljava/lang/Object;'
  Interfaces        -
  Static fields     -
  Instance fields   -

A new class Java8$Logger-CC has been generated! (We know it’s generated because of the SYNTHETIC flag). This class has Object as superclass and doesn’t implement any interfaces and have no static or instance fields.

Now let’s check these class methods. The class has two methods, the first one is $default$log:

Direct methods    -
    #0              : (in LJava8$Logger-CC;)
      name          : '$default$log'
      type          : '(LJava8$Logger;Ljava/lang/String;Ljava/lang/String;)V'
      access        : 0x0009 (PUBLIC STATIC)

We can read that this method is a static method and takes as arguments a Logger plus the same arguments as our default method in our Logger interface! The content of the method is:

[000434] Java8.Logger-CC.$default$log:(LJava8$Logger;Ljava/lang/String;Ljava/lang/String;)V
|0000: new-instance v0, Ljava/lang/StringBuilder; // type@000c
|0002: invoke-direct {v0}, Ljava/lang/StringBuilder;.<init>:()V // method@0012
|0005: invoke-virtual {v0, v2}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; // method@0013
|0008: const-string v2, ": " // string@0001
|000a: invoke-virtual {v0, v2}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; // method@0013
|000d: invoke-virtual {v0, v3}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; // method@0013
|0010: invoke-virtual {v0}, Ljava/lang/StringBuilder;.toString:()Ljava/lang/String; // method@0014
|0013: move-result-object v2
|0014: invoke-interface {v1, v2}, LJava8$Logger;.log:(Ljava/lang/String;)V // method@0009
|0017: return-void

Even though the output looks complicated, the code here is actually simple and its logic is equivalent to the implementation of the default method in Logger interface! The equivalent Java code of the method can be the following :

public static void defaultLog(Logger logger, String tag, String s) {
    logger.log(tag + ":" + s);
}

We can conclude that the default method in interfaces are desugared to static methods in a newly generated utility class (Loger-CC).

Static Method

Based on what we already saw before, we can have a guess how static methods in interfaces are desugared! Let’s check! The generated class Loger-CC have a second static method! And without surprise, its name is systemOut:

#1            : (in LJava8$Logger-CC;)
  name        : 'systemOut'
  type        : '()LJava8$Logger;'
  access      : 0x0009 (PUBLIC STATIC)

The method systemOut in Logger-CC take no arguments and returns a Logger ! The body of the method is:

|[00040c] Java8.Logger-CC.systemOut:()LJava8$Logger;
|0000: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream; // field@0002
|0002: invoke-virtual {v0}, Ljava/lang/Object;.getClass:()Ljava/lang/Class; // method@0011
|0005: new-instance v1, L-$$Lambda$teOjDu261Kz9uXGt1wlPvIP5S04; // type@0001
|0007: invoke-direct {v1, v0}, L-$$Lambda$teOjDu261Kz9uXGt1wlPvIP5S04;.<init>:(Ljava/io/PrintStream;)V // method@0004
|000a: return-object v1

Without surprise, the code is equivalent to our implementation of the static method in systemOut in the interface Logger ($Lambda$teOjDu261Kz9uXGt1wlPvIP5S04 is a class that corresponds to the lambda System.out::println in our implementation).

The equivalent java code can be:

public static Logger systemOut() {
    return new Lambda(System.out); //System.out::println
}

Conclusion

The following Java code is an equivalent of our Java8 class above (PS: I changed some methods/classes names for clarity) :

import java.io.PrintStream;

class Java8Desugared {

  interface Logger {
    void log(String s);
    void log(String tag, String s);
  }

  public static final class LoggerCC {

    public static void defaultLog(Logger logger, String tag, String s) {
      logger.log(tag + ":" + s);
    }

    public static Logger systemOut() {
      return new LambdaSystemOut(System.out);
    }
  }

  public static final class LambdaSystemOut implements Logger {
    private PrintStream ps;

    public LambdaSystemOut(PrintStream ps) {
      this.ps = ps;
    }

    @Override public void log(String s) {
      ps.println(s);
    }

    @Override public void log(String tag, String s) {
      LoggerCC.defaultLog(this, tag, s);
    }
  }

  public static void main(String... args) {
    sayHi(LambdaSayHi.INSTANCE);
    LoggerCC.systemOut().log("hello from static");
  }

  private static void sayHi(Logger logger) {
    logger.log("Hello!");
    logger.log("hello from", "default");
  }

  public static final class LambdaSayHi implements Logger {
    static final LambdaSayHi INSTANCE = new LambdaSayHi();

    private LambdaSayHi() {}

    @Override public void log(String s) {
      lambdaContent(s);
    }

    @Override public void log(String tag, String s) {
      LoggerCC.defaultLog(this, tag, s);
    }
  }

  static void lambdaContent(String s) {
    System.out.println(s);
  }
}

Compiling the running the above code gives the same output as out Java8 class :

$ javac Java8Desugared.java
$ java Java8Desugared
Hello!
hello from:default
hello from static

Sources

Mouaad Aallam

Mouaad Aallam

Software Engineer

rss facebook twitter bsky github youtube mail spotify instagram linkedin google pinterest medium vimeo mastodon gitlab docker