随着Android N、Android Studio 2.2和Android Gradle Plugin 2.2的发布, 我们已经可以使用部分Java 8语言特性来编码。
文章更新历史:
-
2016/8/17 文章发布
伴随Android N发布的Java 8语言特性,部分支持在Android API 9及更高版本的系统中运行, 其余特性仅支持在Android N及更高版本中运行。
其中,支持Android API 9的语言特性包括:
-
Lambda表达式
-
Method引用
-
Type annotations
支持Android N的语言特性包括:
-
interface支持default method和static method
-
Repeatable annotations
在上面这些语言特性中,最实用的应该是 Lambda表达式 和 Method引用。
我们在设计API时,有时会要求调用者传入一个回调接口。这在支持函数的语言中,很容易实现。 但在Java语言中,由于method无法脱离class独立存在, 只能定义一个只有一个method的interface来实现回调(例如,Runnable)。
在Java 8之前,在调用这类API时,一般做法是实现一个匿名类来实现该interface。 显然,这里的代码是冗余的,我们仅需要这个匿名类的那个唯一method的实现即可。 因此,在Java 8语言中终于引入了被其它语言广泛使用Lambda表达式。
有了Lambda表达式,我们的代码会简化很多,不再需要实现很多匿名类。
例如,在没有Lambda表达式时,我们经常会写出这样的代码:
findViewById(R.id.lambda).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
new LambdaDemo().showUsage();
}
});而有了Lambda表达式,我们的代码会变成这样:
findViewById(R.id.lambda).setOnClickListener(
view -> new LambdaDemo().showUsage());关于Lambda表达式的语言细节,可以参见Java官方文档: https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
Lambda表达式的语法基本上是这样的:
(type1 param1, type2 param2, ...) -> {statements}
其中,参数类型、参数外面的圆括号和方法语句块外面的大括号在某些情况下都是可以省略的, 只要编译器在语法分析时不会产生错误和岐义即可。
这个语言特性看看例子就会了,很容易上手。例如:
executeTask(() -> System.out.println("Hello, world #1"));
executeTask(() -> {
System.out.print("Hello,");
System.out.println("world #2");
});
executeTask(a -> a * 2);
executeTask((int lhs, int rhs) -> lhs + rhs);
executeTask((long lhs, long rhs) -> lhs * rhs);
假如,我们有如下的API设计:
@FunctionalInterface // optional
public interface Transformer<F, T> {
T transform(F obj);
}
public static <F, T> List<T> transform(List<F> list, Transformer<F, T> transformer) {
ArrayList<T> result = new ArrayList<T>(list.size());
for (F obj : list) {
result.add(transformer.transform(obj));
}
return result;
}另外,我们也定义了一个Person类:
public static class Person {
public int age;
public String name;
public Person() {}
private Person(int age) {
this.age = age;
this.name = "P-" + age;
}
public static Person from(int age) {
return new Person(age);
}
public int compareByAge(Person other) {
return age - other.age;
}
@Override
public String toString() {
return "Person[name=" + name + ", age=" + age + "]";
}
}现在,我们需要根据一个代表age的int列表,生成一个Person列表。那么,代码可能是这样的:
private void showCode() {
List<Integer> ageList = Arrays.asList(1, 2, 3, 4, 5);
List<Person> personList = transform(ageList, age -> Person.from(age));
}
private void showCode2() {
List<Integer> ageList = Arrays.asList(1, 2, 3, 4, 5);
List<Person> personList = transform(ageList, age -> new Person(age));
}你会发现,虽然用了lambda表达式,代码实现也有点冗余, 因为做的事情仅仅是去调用了另外一个已经存在的method或者constructor。 “Method引用”就是为了解决这样的编码场景,它可以让我们更简单地去引用一些已存在方法。
有了Method引用,上面的代码就可以简化了:
private void showCode() {
List<Integer> ageList = Arrays.asList(1, 2, 3, 4, 5);
List<Person> personList = transform(ageList, Person::from);
}
private void showCode2() {
List<Integer> ageList = Arrays.asList(1, 2, 3, 4, 5);
List<Person> personList = transform(ageList, Person::new);
}Method引用共有4种类型,例举如下:
-
对static method的引用 (ContainingClass::staticMethodName)
-
对一个特定对象的instance method的引用 (containingObject::instanceMethodName)
-
对一个特定类型的任意对象的instance method的引用 (ContainingType::methodName)
-
对constructor的引用 (ClassName::new)
示例代码如下:
private void staticMethod(List<Integer> ageList) {
// ContainingClass::staticMethodName
List<Person> personList = transform(ageList, Person::from);
}
private Person createPerson(int age) {
return Person.from(age);
}
private void instanceMethod(List<Integer> ageList) {
// containingObject::instanceMethodName
List<Person> personList = transform(ageList, this::createPerson);
}
private void typeMethod(List<Person> personList) {
// ContainingType::methodName
Collections.sort(personList, Person::compareByAge);
}
private void constructor(List<Integer> ageList) {
// ClassName::new
List<Person> personList = transform(ageList, Person::new);
}在Java 8中,允许为interface添加有具体实现的method(即default method), 并且,如果仅新增了default method,那么修改后的interface可以和修改前的interface保待二进制兼容。 有了此特性,Library开发者就可以为interface增加default method, 而不需要Library使用者同步去修改interface的实现代码。
除了default method,还可以在interface中添加static method, 方便把跟该interface相关的static方法放在一起。
示例代码:
interface ItsAnInterface {
void fun();
default void foo() {
System.out.println("foo");
}
static void bar(ItsAnInterface a) {
a.fun();
a.foo();
}
}Repeatable annotations允许重复使用annotion。例如:
@Schedule(dayOfMonth="last")
@Schedule(dayOfWeek="Fri", hour="23")
public void doPeriodicCleanup() { ... }在Java 8之前,annotation仅能在声明出现的地方使用,而现在可以在类型出现的地方使用。 (话是这么说,但我还没明白其应用场景。。。)
除了对这些语言特性的支持,Android N也新增了一些API的支持(显然可能在Android N及更高版本中才可以使用)。
-
java.lang.FunctionalInterface
-
java.lang.annotation.Repeatable
-
java.lang.reflect.Method.isDefault()
-
跟Repeable annotations相关的反射API,例如AnnotatedElement.getAnnotationsByType(Class)
为了能够在Android开发中使用这些Java 8语言特性,需要使用 Jack工具链 。
之前,使用javac的编译过程是这样的:
javac (.java --> .class) --> dx (.class --> .dex)
现在使用Jack的编译过程变成了这样:
Jack (.java --> .jack --> .dex)
可以发现,使用Jack工具链之后,不再产生.class文件。这也导致那些基于.class文件的工具插件都会失效。
启用Jack工具链,需要在build.gradle文件中做如下修改:
android {
...
defaultConfig {
...
jackOptions {
enabled true
}
}
}