[译] Android 的 Java 8 支持

[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:conststringv0,"Hello"0002:invokeinterfacev1,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:invokestaticv1,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 0type:(Ljava/lang/String;)Vaccess: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.invokeAOSP source code for java.lang.invoke 的文档,我们可以注意到这个类在 Android Runtime 中不存在,这也是为什么脱糖在编译时要求最小版本的原因。VM 环境支持 invokedynamic 指令,但是 JDK 在编译 LambdaMetafactory 中却不可用。

6. Method References(方法引用)

除了 lambda 表达式,方法引用也是 Java 8 的语言特性,当 lambda 的实现是一个已经存在的方法,此时使用方法引用会很方便。

public static void main(String… args) {

  • sayHi(s -> System.out.println(s));
  • sayHi(System.out::println);
    }

这与 javacdexesD8 的编译是相同的,与 lambda 版本有一个显著的区别。在编译为 dalvik 字节码时,生成的 lambda 类的主体已更改。

[000268] -KaTeX parse error: Can't use function '$' in math mode at position 7: Lambda$̲1Osqr2Z9OSwjseX…Lambda$1Osqr2Z9OSwjseX_0FMQJcCG_uM;.f$0:Ljava/io/PrintStream;
0002: invoke-virtual {v0, v2}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V
0005: return-void

不是通过生成 Java8.lambda$main$0 方法然后调用 System.out.println 的方式实现,而是直接调用 System.out.println 方法。lambda 表达式调用类也不是一个静态单例,而是直接使用 PrintStream 类实例引用,即 System.out,它的调用如下。

[0002bc] Java8.main:([Ljava/lang/String;)V
0000: sget-object v1, Ljava/lang/System;.out:Ljava/io/PrintStream;
0003: new-instance v0, L-KaTeX parse error: Can't use function '$' in math mode at position 7: Lambda$̲1Osqr2Z9OSwjseX…Lambda 1 O s q r 2 Z 9 O S w j s e X 0 F M Q J c C G u M ; . < i n i t > : ( L j a v a / i o / P r i n t S t r e a m ; ) V 0008 : 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 1Osqr2Z9OSwjseX_0FMQJcCG_uM;.<init>:(Ljava/io/PrintStream;)V 0008: invoke-static {v0}, LJava8;.sayHi:(LJava8 1Osqr2Z9OSwjseX0FMQJcCGuM;.<init>:(Ljava/io/PrintStream;)V0008:invokestaticv0,LJava8;.sayHi:(LJava8Logger;)V

同样我们也可以在源码级层面进行模拟。

public static void main(String… args) {

  • sayHi(System.out::println);
  • sayHi(new -$$Lambda$1Osqr2Z9OSwjseX_0FMQJcCG_uM(System.out));
    }
    @@
    }

+class -$$Lambda$1Osqr2Z9OSwjseX_0FMQJcCG_uM implements Java8.Logger {

  • private final PrintStream ps;
  • -$$Lambda$1Osqr2Z9OSwjseX_0FMQJcCG_uM(PrintStream ps) {
  • this.ps = ps;
  • }
  • @Override public void log(String s) {
  • ps.println(s);
  • }
    +}

7. Interface Methods(接口中的方法)

Java 8 中新增了接口方法中的 defaultstatic 修饰符。接口中的 static 方法允许直接操作调用。接口中的 default 方法允许你为接口添加默认实现方法。

interface Logger {
void log(String s);

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

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

D8 中的脱糖都已经支持了这两个接口的新特性。通过上面的方法同样可以分析出脱糖是如何进行优化工作的,具体的分析就留给读者了。

8. Just Use Kotlin?

这个时候肯定有很多读者猜想 Kotlin 是否也具备这种能力。当然,Kotlin 同样提供了 lambda 和接口中的 staticdefault 方法。这些特性都被 kotlinc 以相同的方式实现。

Android 工具和 VM 的开发者肯定会 100% 支持 Kotlin 实现 Java 语言的新特性。因为每次的 Java 新版本都会在字节码构建和 VM 上带来新的优化体验。

在未来和可能 Kotlin 不会支持 Java 6Java 7Intellij 开发工具已经在在 2016 年 1 月迁移至 Java 8

9. Desugaring APIs

上面的分析中,我们一直关注的是 Java 语言新特性,其它还有一些主要的方面没有提及,比如新的 APIs。在 Java 8 转给你带来了很多新的 APIs,比如 streamOptionalCompletableFuture 以及新的 date/time API 等等。

回到上面的例子,我们使用新的 date/time API 来输出日志打印的时间。

import java.time.*;

class Java8 {
interface Logger {

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
…(img-MNtRUlPC-1715789818959)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 25
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值