是时候学习一波Lambda表达式了
是时候学习一波Lambda表达式了
Android N预览版的发布, 支持了一些java 8的新特性, lambda表达式算是最重要的新特性之一. 本篇文章将会探讨及使用Lambda表达式, 跟紧Google粑粑的脚步
什么是Lambda
首先Lambda并不是新鲜事物, 其为java8最重要的新特性之一. 我们Android开发者开始一直用java7, 直到AndroidN的发布终于能过使用Lambda.
要明白什么是Lambda, 先要知道什么是闭包(Closure).
闭包来源于函数式编程, 关于闭包的概念各类定义总是深(bu)奥(shuo)难(ren)懂(hua). 用人话来说就是:
"定义在函数内部的函数", 在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁
--来自阮一峰技术博客
而Lambda表达式是java对闭包这一特性的实现方式.
在java中实际使用时就是将函数参数中某类匿名内部类改写成不明觉屌的Lambda表达式.
等等, 这个某类匿名内部类是神马意思?
这类能用Lambda表达式替代的匿名内部类有两个条件: 必须是接口类型; 只有一个抽象方法.
Lambda的使用
语法
基本语法
(parameters) -> { expression or statements }
下面是一些例子:
// 无参数, 返回1+2的结果
() -> 1+2
// 接收一个参数(数字类型),返回其2倍的值
x -> 2 * x
// 接收2个参数(数字),返回表达式运算的结果
(x, y) -> x + y
// 多个语句要用大括号包裹, 并且返回值要用return指明
(x, y) -> {
int result = x + y;
System.out.print(result);
return result;
}
// 接收string 对象, 并在控制台打印
s -> System.out.print(s)
其中参数的类型可以不声明, 编译器会结合上下文智能推断, 比如这句
s -> System.out.print(s)
等价于
(String s) -> System.out.print(s)
注意: 无参数时()
不能省略
语法非常简单, 就是因为简单, 反而更让人摸不着头脑, 接下来开始介绍具体使用
java中使用Lambda
先看看我们常写的Runnable接口如何改写成Lambda形式
使用匿名内部类的写法
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello Lambda!");
}
}).start();
使用Lambda表达式
new Thread(() -> System.out.println("Hello Lambda!")).start();
改写过程一目了然, 就是原本写匿名内部类的地方, 改写成了
参数 -> 表达式或者代码库块
再来总结一下Lambda表达式的使用条件:
- 函数(可以是构造函数)的参数是接口
- 这个接口只包含一个抽象方法
这样就可以使用酷炫拽的Lambda表达式了
自定义接口使用Lambda
下面尝试自定义接口使用Lambda, 对使用方式理解更清晰
第一步, 创建一个Person类
public class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
第二步, 创建一个接口, 用来打印Person
public interface IPersonPrinter {
void printPerson(Person p);
}
第三步, 创建方法打印Person
private static void printPerson(Person p, IPersonPrinter personPrinter){
personPrinter.printPerson(p);
}
接下来要在main函数中调用第三步的方法, 先来看不使用Lambda表达式的代码
Person person = new Person("Smarx", 23);
printPerson(person, new IPersonPrinter() {
@Override
public void printPerson(Person p) {
System.out.println(p.toString());
}
});
很简单的操作却要使用这么多行代码, 只有一行代码是有效的. 而使用Lambda表达式后, 只需要一行代码:
printPerson(person, p -> System.out.println(p.toString()));
完整的调用代码如下:
public class LambdaDemo {
public static void main(String[] args) {
Person person = new Person("Smarx", 23);
printPerson(person, p -> System.out.println(p.toString()));
}
private static void printPerson(Person p, IPersonPrinter personPrinter){
personPrinter.printPerson(p);
}
}
如何使Android Studio支持Lambda
在Android N出现之前, 大家都是使用gradle-retrolambda插件支持的. 网上相关的文章很多, 如果需要可自行学习这个库的配置及使用.
下面介绍使用Android N支持Lambda表达式
首先确保你的jdk已经升级到了1.8, 然后在将工程根目录的build.gradle中的gradle版本改成最新版本, 目前最新的版本是2.1.0-alpha4
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0-alpha4'
}
module目录的build.gradle配置如下:
android {
compileSdkVersion 'android-N'
// buildTools必须用24以上
buildToolsVersion "24.0.0 rc3"
defaultConfig {
applicationId "com.github.smarxpan"
minSdkVersion 'N' //使用Android N最小版本也要是Android N
targetSdkVersion 'N'
versionCode 1
versionName "1.0"
// 使用jack(Java Android Compiler Kit)工具链
jackOptions{
enabled true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// 配置JDK为1.8
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
这样就能愉快的将我们常写的某些匿名内部类写成Lambda表达式了, such as:
findViewById(R.id.btn).setOnClickListener( view -> {
Toast.makeText(MainActivity.this, "Hello Lambda", Toast.LENGTH_SHORT).show();
});
Lambda和匿名内部类的区别
看起来Lambda表达式只是简化了匿名内部类的书写, 事实上Lambda并非匿名内部类的语法糖, Lambda的效率比匿名内部类要高.
以下内容主要学习自深入探索Java 8 Lambda表达式
, 我不过拾人牙慧, 不值一哂
匿名内部类形式
我们依旧使用前面自定义Lambda表达式的例子研究, 先来看匿名内部类的代码:
public class LambdaDemo {
public static void main(String[] args) {
Person person = new Person("Smarx", 23);
printPerson(person, new IPersonPrinter() {
public void printPerson(Person p) {
System.out.println(p.toString());
}
});
}
private static void printPerson(Person p, IPersonPrinter personPrinter){
personPrinter.printPerson(p);
}
}
进入这个文件所在的目录, 使用命令行编译
javac LambdaDemo.java
再使用javap命令查看字节码
javap -c -v LambdaDemo
可以看到匿名内部类生成的字节码如下:
12: aload_1
13: new #5 // class LambdaDemo$1
16: dup
17: invokespecial #6 // Method LambdaDemo$1."<init>":()V
20: invokestatic #7 // Method printPerson:(LPerson;LIPersonPrinter;)V
23: return
上述字节码的含义如下:
- 第13行,使用字节码操作new创建了类型LambdaDemo$1的一个对象,同时将新创建的对象的的引用压入栈中。
- 第16行,使用dup操作复制栈上的引用。
- 第17行,上面的复制的引用被指令invokespecial消耗使用,用来初始化匿名内部类实例。
- 第20行,调用本类的静态方法printPerson
Lambdas表达式和invokedynamic
将匿名内部类改写成Lambda
printPerson(person, p -> System.out.println(p.toString()));
重新编译后再查看字节码
12: aload_1
13: invokedynamic #5, 0 // InvokeDynamic #0:printPerson:()LIPersonPrinter;
18: invokestatic #6 // Method printPerson:(LPerson;LIPersonPrinter;)V
21: return
可以看到字节码与匿名内部类的版本并不相同, Lambda表达式转化成字节码实际上做了如下两步:
- 生成一个invokedynamic调用点,也叫做Lambda工厂。当调用时返回一个Lambda表达式转化成的函数式接口实例。
- 将Lambda表达式的方法体转换成方法供invokedynamic指令调用。
也就是说, Lambda表达式其实被翻译成了本类的一个静态方法, 比如我们上面的代码, 会被翻译成类似这样的方法:
static void lambda$1(String s){
System.out.println(p.toString());
}
需要注意的是,这里的$1并不是代表内部类,这里仅仅是为了展示编译后的代码而已。
需要注意的是编译器对于Lambda表达式的翻译策略并非固定的,因为这样invokedynamic可以使编译器在后期使用不同的翻译实现策略。比如,被捕获的变量可以放入数组中。如果Lambda表达式用到了类的实例的属性,其对应生成的方法可以是实例方法,而不是静态方法,这样可以避免传入多余的参数。
总结
经过上述的学习, 相信大家对Lambda表达式的使用已经有了清晰了了解. 目前Lambda的支持还未能向下兼容, 还处于预览版的状态, 相信Google很快会推出支持方案.