Android支持Java8新特性

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的新特性。

这几种方案的实现原理基本都是在编译时对字节码进行一次处理,大致如下图所示:

Google desugar bytecode transformations

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);
});

你可能感兴趣的:(Android支持Java8新特性)