Android从KitKat(Android SDK 4.4, API Level 19)开始支持Java 7,但之后过了很长一段时间,Google却一直没有给出支持Java 8的计划(估计是和Oracle的Java API版权之争的原因),期间有大神推出了Retrolambda,Jack toolchain(Google出品)等替代方案来部分支持Java 8的新特性,满足开发者的需求。不过,今年Google在Android开发者博客中宣布将弃用Jack toolchain,改为在最新的Android Studio内置支持Java 8的新特性。
这几种方案的实现原理基本都是在编译时对字节码进行一次处理,大致如下图所示:
Android Studio 3.0+
近年来闹的沸沸扬扬的Google和Oracle的Java API版权之争随着Google的失败终于告一段落。当时我就猜测Google很有可能不会再全面支持Java的版本更新了,果不其然,在Google I/O 2017大会上官方正式宣布未来Kotlin将成为Android的第一官方语言,进一步证实了我的猜测。
不过现阶段Kotlin并未普及,即使Android Studio也需要在当前还未正式发布的3.0版本才开始默认支持Kotlin,3.0以下的版本仍需要配合扩展插件才能支持Kotlin。故在较长一段时间内,Java仍是Android的主力开发语言。
因此,Google也在Android Studio 3.0+里面内置支持了部分Java 8的新特性:
- Lambda表达式
- 方法引用
- 默认和静态接口方法
- 类型注解
- 重复注解
不过,可能是为了向前兼容,当IDE检测到工程中使用了Jack,Retrolambda或DexGuard时将不会激活IDE自带的Java 8特性支持。因此,要想使用Android Studio的扩展插件方案,需要删除原有的第三方插件方案,具体细节可参见手册:Use Java 8 language features
Retrolambda
Retrolambda是目前相对较为成熟的一套第三方插件方案,能够在Java 7,6,5上支持如下特性:
-
Java 8特性
- Lambda表达式
- 方法引用
- 默认和静态接口方法 可选特性,默认关闭,需要手动做向前兼容
-
Java 7特性(可在Java 6,5上支持部分Java 7特性)
- Try-with-resources statements
- Objects.requireNonNull
- Catching Multiple Exception
- Strings in switch Statements
引入Retrolambda
Retrolambda只是针对JDK的扩展,如果是在Android上使用,可以使用Gradle Retrolambda Plugin插件,该插件依赖于Retrolambda,可配合Gradle(依赖的Android Gradle Plugin最小版本为1.5.0,Gradle Plugin最小版本为2.5)自动构建Android工程。
Note: The minimum android gradle plugin is 1.5.0 and the minimum gradle plugin is 2.5.
需要在build.gradle中添加如下内容:
buildscript {
dependencies {
classpath 'me.tatarka:gradle-retrolambda:3.6.1'
}
}
apply plugin: 'me.tatarka.retrolambda'
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
配置Retrolambda
如需修改Retrolambda的配置项,可如下添加:
apply plugin: 'me.tatarka.retrolambda'
retrolambda {
javaVersion JavaVersion.VERSION_1_7
jvmArgs '-noverify'
defaultMethods false
incremental true
}
亦可如下添加:
apply plugin: 'me.tatarka.retrolambda'
android {
retrolambda {
javaVersion JavaVersion.VERSION_1_7
jvmArgs '-noverify'
defaultMethods false
incremental true
}
}
添加在这两个位置的效果是一样的。具体的配置参数说明可查看手册
Retrolambda的配置里需要重点关注的是javaVersion的配置
javaVersion Set the java version to compile to. The default is 6. Only 5, 6 or 7 are accepted.
Java 6和Java 7编译的区别
插件默认使用java 6来编译,这是有原因的。前面说到Retrolambda可在Java 5,6上支持部分Java 7特性,而Android是在Kitkat才开始支持Java 7的,因此,若APP的minSdkVersion小于19时,最好使用Java 6来编译,否则Retrolambda不会处理Java 7特性相关的代码,在老版本手机上运行时有可能会不生效(有些手机厂商会自己做一些处理,所以不是所有手机都会有问题,但也是大概率事件)而导致问题。同理,若APP的minSdkVersion大于等于19时,最好使用Java 7来编译,因为没必要再做转译处理。
我们以Objects.requireNonNull()
为例来看看Java 6和Java 7编译后的代码区别。
我们在HomeActivity.java
添加如下方法,然后分别使用Java 6和7来编译。
private String parseContent(@NonNull String content) {
return Objects.requireNonNull(content) + ".suffix";
}
编译后,可参考路径app -> build -> intermediates -> transforms -> retrolambda -> ... -> HomeActivity.class
找到对应java文件的字节码文件。
- JavaVersion.VERSION_1_7可看到没有做任何处理
private String parseContent(@NonNull String content) {
return (String)Objects.requireNonNull(content) + ".suffix";
}
- JavaVersion.VERSION_1_6可看到做了处理:用
object.getClass()
的方式来模拟Objects.requireNonNull()
中对空指针抛异常的处理
private String parseContent(@NonNull String content) {
StringBuilder var10000 = new StringBuilder();
content.getClass();
return var10000.append((String)content).append(".suffix").toString();
}
Java 7编译配置
在配置java 7来编译时,若只配置javaVersion JavaVersion.VERSION_1_7
,不配置jvmArgs '-noverify'
,编译时会报错:
Error:Execution failed for task ':app:transformClassesWithRetrolambdaForDebug'.
Process 'command '/Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/bin/java'' finished with non-zero exit value 1
这有可能是系统没有安装jdk 7导致的,不过只是猜测(在不指定javaVersion或只配置javaVersion JavaVersion.VERSION_1_6
时,系统没有安装jdk 6也不会有问题),没有搭建环境去验证。目前加上两个配置项后即可正常编译运行,暂时也没有遇到任何问题。
配置Proguard
需要在proguard文件中添加如下内容:
-dontwarn java.lang.invoke.*
-dontwarn **$$Lambda$*
处理Lint的不兼容报错
Lint不识别Lambda表达式
老版本的Android Gradle Plugin的Lint不兼容Java 8新特性的语法,像Lambda表达式这种Java 8新特性的语法会报错(不清楚具体是从哪个版本开始支持)。可引入android-retrolambda-lombok来解决。
Lint不识别try-with-resources
如果Android工程的minSdkVersion小于19(Android从API 19开始支持Java 7),Lint则会对像try-with-resources这种Java 7新特性的语法报错。可通过配置Lint规则来解决。
配置Lint规则有多种方式,可针对某一工程全局配置,可针对某一代码单独配置,也可系统全局配置。
除非特殊场景需要针对某一代码单独配置以外,一般推荐针对某一工程全局配置,不建议系统全局配置,毕竟有可能同时开发多个项目,会影响项目间的差异性配置。
Android工程全局配置
在Android工程的SRC同级目录下新建或修改lint.xml
文件,添加如下内容:
Note: 官方手册上说lint.xml放在Android工程的根目录下很容易让人误解,其实需要放在SRC同级目录下,如下所示:
Translator/
app/
src/
build.gradle
lint.xml
代码单独配置
通过注解@SuppressLint("xxx")
来忽略Lint报错,具体的注解类型,可通过命令lint --list
查看。
@SuppressLint("NewApi")
public final void setView(@NonNull T view) {
mView = Objects.requireNonNull(view);
}
关闭没必要的报警
引入Retrolambda后,会有一些新特性实现方式的建议性报警,可有可无,对于有点强迫症的人,可以配置关掉这些报警,免得看着难受。
报警类型
- Anonymous can be replaced with lambda或
@SuppressWarnings("Convert2Lambda")
mLoginBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(LoginActivity.class);
}
});
- Statement lambda can be replaced with expression lambda或
@SuppressWarnings("CodeBlock2Expr")
mLoginBtn.setOnClickListener(view -> {
startActivity(LoginActivity.class);
});
全局配置方法
通过Android Studio做项目或系统全局配置会方便些。进入如下配置页面,不勾选相关报警选择即可。
项目全局配置
Preferences -> Inspections系统全局配置
Other Settings -> Default Settings -> Inspections
代码单独配置方法
通过注解@SuppressWarnings("xxx")
来忽略报警,具体的注解类型可查看Android SuppressWarnings list,这个老兄总结的比较全。
@SuppressWarnings("Convert2Lambda")
mLoginBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(LoginActivity.class);
}
});
Lambda表达式
Lambda表达式(在Java中亦被人称为闭包或匿名函数)来源于函数式编程,用一种更为简介的语法来替代函数式接口(亦可称为SAM,Single Abstract Method,单个抽象方法类型。可简单理解为只有一个方法的接口)传统的内部类语法,解决常被人戏称的高度问题。所以像RxJava这类流式写法中,Lambda表达式能让代码显得更为简介。
// 内部类
mLoginBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Activity activity = HomeActivity.this;
Intent intent = new Intent(activity, LoginActivity.class);
activity.startActivity(intent);
}
});
// Lambda表达式
mLoginBtn.setOnClickListener(view -> {
Intent intent = new Intent(this, LoginActivity.class);
this.startActivity(intent);
});
Lambda表达式的用法和实现原理可以参考下面几篇文章,这里不再关注。
- 从java8 说起函数式编程
- State of the Lambda(译文)
- State of the Lambda: Libraries Edition
- Translation of Lambda Expressions
这里重点关注的是Lambda表达式和传统语法的差异点,毕竟这种语法糖是把双刃剑,虽然能让代码更为简介,但且降低了代码的可读性,而且用的不好有可能会导致问题。
this
内部类的this指向的是内部类对象的引用,不是指向外部类的引用。而Lambda表达式中的this是指向外部类的引用。
访问外部变量
内部类中访问的外部变量只能是final修饰的,而且编译器对这些规则要求很严格,如果没有final修饰也无法编译通过。
如果引入了Gradle Retrolambda Plugin,插件会在编译时为符合条件的外部变量自动加上final修饰符,此时访问外部变量的规则同Lambda表达式。
final String message = "test";
mLoginBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.d(TAG, message);
}
});
Lambda表达式中访问的外部变量不再强制要求final修饰,只需要满足“有效只读”的变量即可(编译器会根据上下文推导),可简单理解为不能在Lambda表达式中修改变量的值。
String message = "test";
mLoginBtn.setOnClickListener(view -> Log.d(TAG, message));
像下面这种改变了外部变量值的Lambda表达式则会编译报错。
String message = "test";
mLoginBtn.setOnClickListener(view -> {
message += "changed";
Log.d(TAG, message);
});