Lambda表达式和Method References在Android中的应用
CommentGoogle在发布了Android N Preview后支持了Java 8的一些新特性,其中就有:Lambda表达式、默认和静态接口方法、Stream、重复注解、Method References(方法引用)这些内容.
要使用这些新特性需要在build.gradle文件中的android节点下指定java版本:
1 | android { |
在android节点下的defaultConfig节点下开启jack工具链:
1 | android { |
最近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 | public interface TestListener { |
那么他的lambda表达式为:
1 | TestListener listener = () -> 2 + 3; |
表达式函数体适合小型 lambda 表达式,它消除了return关键字,使得语法更加简洁
2.语句块:语句块中的语句会被依次执行,与方法中的语句一样,return会把控制权交给匿名方法的调用者;
1 | TestListener listener1 = () -> { |
3. 函数式接口的名称并不是 lambda 表达式的一部分,lambda表达式的类型由其上下文推导而来,lambda 表达式在不同上下文里可以拥有不同的类型.
比如分别定义2个接口:
1 | public interface TestListener { |
那么他们的lambda表达式为:
1 | TestListener listener = () -> 2 + 3; |
可以看到他们的lambda为完全相同,编译器利用lambda表达式所在上下文所期待的类型(被称为目标类型)进行推导,得到2个不同接口的实例对象.当lambda作为方法的参数传入时,这种上下文推导也完全适用.
当lambda的参数只有一个时,该参数列表外面的括号可以被省略
1 | public interface TestListener3 { |
编译器通过判断lambda表达式的下面这些条件符合后才被赋给目标类型:
- 目标类型是一个函数式接口
- lambda表达式的参数和目标类型的方法参数在数量和类型上一一对应
- lambda表达式的返回值和目标类型的方法返回值相兼容
- lambda表达式内所抛出的异常和目标类型的方法throws的类型相兼容
这里可以看出为什么lambda表达式只能出现在目标类型为函数式接口的上下文中:一个lambda只能表示实现接口的一个方法,如果接口中存在多个方法,那用lambda表达式是无法创建出该接口的对象
其实这种通过目标类型进行类型推导在java中早已经有了,泛型方法的调用和”菱形”构造器调用就是通过目标类型来进行类型推导的.并且在java8中对目标类型进行类型推导进行了强化,一些在java8之前无法推导出的类型在java8中可以正常推导出来.
比如下面两个表达式在java8之前在编译时就会报错,但是在java8中可以通过:
1 | List<String> ls = Collections.checkedList(new ArrayList<>(), String.class); |
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 | int i = 0; |
方法引用(Method references) : 通过方法名直接调用方法(其实就是lambda表达式的一个简化写法).
1. 方法引用的格式:左边是容器(可以是类名,实例名),中间是”::”,右边是相应的方法名
1 | 如果是静态方法,则是: ClassName::methodName |
方法引用会自动将接口的方法中的参数传入被调用的方法中:
1 | Observable.create((Observable.OnSubscribe<String>) subscriber -> subscriber.onNext("123")) |
在这里使用了Rxjava1,tv是一个TextView控件对象,使用方法引用之后会自动将123设置给TextView.
后记:
这次对Lambda表达式和方法引用的一个笔记主要还是为了以后能看懂这些代码,是否真的使用到项目中还是看各公司的要求以及每个人的决定,虽然这些内容为减少代码的冗余是有很大好处,但对于代码的可读性来说确实是降低了不少.
RxJava对于不了解的人来说也是难以看懂,不过RxJava对于代码各方面的提升是不言而喻的,lambda表达式和方法引用带来的收益是否能弥补代码可读性的降低?还需多方面衡量来定论,这里也不再更多评论.
不过将以前的代码改写为lambda和方法引用的格式的投入还是很低的,现在Android studio的自动提示和自动调整功能已经能很完美的将内部类转换为lambda和方法引用.
另外java8除了上述这2个内容,默认方法和静态接口方法对于类库的维护还是提供了不少便利,更多的内容可以自行了解.