Google在发布了Android N Preview后支持了Java 8的一些新特性,其中就有:Lambda表达式、默认和静态接口方法、Stream、重复注解、Method References(方法引用)这些内容.

要使用这些新特性需要在build.gradle文件中的android节点下指定java版本:

1
2
3
4
5
6
7
android {
//...
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}
}

在android节点下的defaultConfig节点下开启jack工具链:

1
2
3
4
5
6
7
8
9
android {
//...
defaultConfig {
//....
jackOptions {
enabled true
}
}
}

最近Google在17年3月14日的消息Future of Java 8 Language Feature Support on Android中说到:未来会将Java 8语言特性直接添加到当前的javac和dx工具集中.

在Android没有支持Lambda之前,有一个插件:Retrolambda,可以在Android中进行Lambda表达式的使用,不过Retrolambda是Java6/7对Lambda表达式的非官方兼容方案,它的向后兼容性和稳定性是无法保障的.所以当时知道Retrolambda后并没有进行进一步具体的了解.

目前在Android Studio中使用了jack工具链之后,进行提示后可以轻易的转换成Lambda形式的代码,可以预见到将来有不少开源代码可能会是这种形式(比如rxjava2的不少案例代码就是使用lambda的形式了),所以继续逃避Lambda是没有意义了,不过还好Lambda表达式并不是那么难理解,这次主要就是对Lambda表达式和Method References进行一个笔记,对这些新特性不作任何评价,只是对其进行描述和举例方便理解.

一个lambda表达式对应一个接口,这个接口必须是函数式接口,lambda就是对这个接口的匿名内部类的简单表达.

1. 函数式接口(Functional interfaces):只拥有一个方法的接口称为函数式接口

java 8提供了一个@FunctionalInterface注解来表示函数式接口,这个注解是非必须的,只要接口只包含一个方法的接口,虚拟机会自动判断,java文档是建议最好在接口上使用注解@FunctionalInterface进行声明.

Java中的lambda无法单独出现,它需要一个函数式接口来盛放,lambda表达式方法体就是函数接口的实现

2. lambda 表达式的语法:由参数列表、箭头符号 -> 和函数体组成

函数体既可以是一个表达式,也可以是一个语句块:

1.表达式:表达式会被执行然后返回执行结果。

比如:定义一个接口,其中方法中的返回值为int:

1
2
3
public interface TestListener {
int onTestListener();
}

那么他的lambda表达式为:

1
TestListener listener = () -> 2 + 3;

表达式函数体适合小型 lambda 表达式,它消除了return关键字,使得语法更加简洁

2.语句块:语句块中的语句会被依次执行,与方法中的语句一样,return会把控制权交给匿名方法的调用者;

1
2
3
4
5
6
7
TestListener listener1 = () -> {
int a = 0;
for (int i = 0; i < 10; i++) {
a += i;
}
return a;
};

3. 函数式接口的名称并不是 lambda 表达式的一部分,lambda表达式的类型由其上下文推导而来,lambda 表达式在不同上下文里可以拥有不同的类型.

比如分别定义2个接口:

1
2
3
4
5
6
7
public interface TestListener {
int onTestListener();
}

public interface TestListener2 {
int onTestListener();
}

那么他们的lambda表达式为:

1
2
TestListener listener = () -> 2 + 3;
TestListener2 listener2 = () -> 2 + 3;

可以看到他们的lambda为完全相同,编译器利用lambda表达式所在上下文所期待的类型(被称为目标类型)进行推导,得到2个不同接口的实例对象.当lambda作为方法的参数传入时,这种上下文推导也完全适用.

当lambda的参数只有一个时,该参数列表外面的括号可以被省略

1
2
3
4
5
public interface TestListener3 {
int onTestListener3(int i);
}

TestListener3 listener3 = i -> 0;

编译器通过判断lambda表达式的下面这些条件符合后才被赋给目标类型:

  1. 目标类型是一个函数式接口
  2. lambda表达式的参数和目标类型的方法参数在数量和类型上一一对应
  3. lambda表达式的返回值和目标类型的方法返回值相兼容
  4. lambda表达式内所抛出的异常和目标类型的方法throws的类型相兼容

这里可以看出为什么lambda表达式只能出现在目标类型为函数式接口的上下文中:一个lambda只能表示实现接口的一个方法,如果接口中存在多个方法,那用lambda表达式是无法创建出该接口的对象

其实这种通过目标类型进行类型推导在java中早已经有了,泛型方法的调用和”菱形”构造器调用就是通过目标类型来进行类型推导的.并且在java8中对目标类型进行类型推导进行了强化,一些在java8之前无法推导出的类型在java8中可以正常推导出来.

比如下面两个表达式在java8之前在编译时就会报错,但是在java8中可以通过:

1
2
3
List<String> ls = Collections.checkedList(new ArrayList<>(), String.class);

Set<Integer> si = flag ? Collections.singleton(23) : Collections.emptySet();

5. lambda表达式不会从超类中继承任何变量名,也不会引入一个新的作用域,lambda表达式基于词法作用域,lambda表达式函数体里面的变量和它外部环境的变量具有相同的语义.

this关键字在lambda表达式内部和外部拥有相同的语义,对this的引用,以及通过this对未限定字段的引用和未限定方法的调用在本质上都属于使用final局部变量,包含此类引用的lambda表达式相当于捕获了this实例,在其它情况下,lambda表达式对象不会保留任何对 this 的引用.

普通的内部类实例会一直保留一个对其外部类实例的强引用,对于lambda表达式来说:那些没有捕获外部类成员的lambda表达式不会保留对外部类实例的引用.这可以防止在一些情况下造成的内存泄漏.

在同一个方法中lambda表达式中声明的变量与在外部声明的变量处于相同的作用域,但是在lambda中还是无法改变外部声明的局部变量的值,在外部也无法使用lambda中声明的变量.lambda表达式并没有对这些内容给出解决方案.(官方的解释是lambda表达式的目的并不是为了解决这些内容,只是为了解决内部类无用代码过多的问题…)

另外在java8之前,在内部类中如果捕获的变量没有被声明为final就会产生一个编译错误,在java8中放宽了这个限制,现在编译器会去检查这个变量是否是一个只读的变量,当是一个只读变量时就不会产生编译错误.(其实就是加上final之后也不会编译报错)

1
2
3
4
5
6
7
8
9
10
11
int i = 0;
binding.tv.setOnTouchListener((v, event) ->
{
//int i = 0; //这里再声明i会报错
//i++; //无法改变外部局部变量的值
int n = i + 1; //使用外部局部变量没有强行让其被final修饰,但是当i的值被修改后就会报错
int j = 0;
return false;
}
);
int k = j + 1; //外部无法使用lambda中声明的变量

方法引用(Method references) : 通过方法名直接调用方法(其实就是lambda表达式的一个简化写法).

1. 方法引用的格式:左边是容器(可以是类名,实例名),中间是”::”,右边是相应的方法名

1
2
3
4
5
如果是静态方法,则是:    ClassName::methodName
如果是实例方法,则是: Instance::methodName
超类上的实例方法引用: super::methodName
构造函数,则是: ClassName::new
数组构造方法引用: TypeName[]::new

方法引用会自动将接口的方法中的参数传入被调用的方法中:

1
2
3
4
Observable.create((Observable.OnSubscribe<String>) subscriber -> subscriber.onNext("123"))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(tv::setText);

在这里使用了Rxjava1,tv是一个TextView控件对象,使用方法引用之后会自动将123设置给TextView.

后记:

这次对Lambda表达式和方法引用的一个笔记主要还是为了以后能看懂这些代码,是否真的使用到项目中还是看各公司的要求以及每个人的决定,虽然这些内容为减少代码的冗余是有很大好处,但对于代码的可读性来说确实是降低了不少.

RxJava对于不了解的人来说也是难以看懂,不过RxJava对于代码各方面的提升是不言而喻的,lambda表达式和方法引用带来的收益是否能弥补代码可读性的降低?还需多方面衡量来定论,这里也不再更多评论.

不过将以前的代码改写为lambda和方法引用的格式的投入还是很低的,现在Android studio的自动提示和自动调整功能已经能很完美的将内部类转换为lambda和方法引用.

另外java8除了上述这2个内容,默认方法和静态接口方法对于类库的维护还是提供了不少便利,更多的内容可以自行了解.