“lambda 表达式”是一块代码,你可以将它传递出去,这样后面就可以执行一次或多次。很多情况下指定这样一个代码块是有用的:
- 传递给 Arrays.sort 一个比较方法
- 在单独的线程中运行任务
- 指定一个当按钮被点击时应该发生的行为
然而,java 是面向对象语言,其中的(几乎)所有一切都是对象。java 中没有函数类型。作为一种替代,函数被表达为对象,也就是实现了特定接口的类的实例。lambda 表达式给你一种便捷的语法来创建这样的实例。
android studio 中如何使用 lambda 参考: gradle-retrolambda
lambda 表达式语法
- 一个简单的表达式
String[] friends = {"peter", "paul", "mary"};
Arrays.sort(friends, (x, y) -> x.compareTo(y));
- 如果 lambda 表达式的表达体执行一个无法用一个表达式表示的计算,那么用编写方法的方式来写:即用 {} 包裹代码并明确写上 return 语句:
Arrays.sort(friends, (String x, String y) -> {
int d = x.length() - y.length();
if (d < 0) return -1;
else if (d > 0) return 1;
else return 0;
});
- 如果 lambda 表达式没有参数,提供空的小括号,就像没有参数的方法:
Runnable task = () -> { for(int i = 0; i < 10; i++) Log.d("MainActivity", i + "");};
- 如果 lambda 表达式的参数类型可以被推倒出来,则可以省略类型,如:
Comparator comparator = (x, y) -> x.compareTo(y);
- 如果某个方法只有一个参数,并且该参数类型是可以推导出来的,你甚至可以省略小括号。
- 永远不需要为 lambda 表达式指定返回类型。编译器会从表达式推断出类型,并且会检查返回类型是否与期望的类型先匹配。
函数式接口
java 有许多表达行为的接口,例如 Runnable 或 Comparetor 。 lambda 表达式兼容这些接口。无论何时,当你期望只有一个抽象方法的接口对象时,就可以提供一个 lambda 表达式。 这样的接口称为 函数式接口。如:
Arrays.sort(friends, (x, y) -> x.compareTo(y));
第二个参数接受一个实现了 Comparetor
在 java 中,对 lambda 表达式你只能做一件事:将其放入类型为函数式接口的变量中,这样它就被转换为该接口的实例
注意:不能将一个 lambda 表达式赋值给一个 Object 类型的变量,因为 Object 是类,不是函数式接口。
方法引用和构造函数引用
有时,你想要传递给其他代码块的操作已经有实现的方法了。有一种特殊的语法方法引用,它甚至比调用方法的 lambda 表达式更短。类似的便捷方法构造函数也有。
方法引用
如果你想不区分大小写地对字符串进行排序,就可以这样调用:
Arrays.sort(friends, (x, y) -> x.compareToIgnoreCase(y));
方法引用:
Arrays.sort(friends, String :: compareToIgnoreCase);
其它例子:
//删除列表所有的 null 值
list.removeIf(Objects :: isNull); 等同于 x -> Objects.isNull(x)
//打印列表
list.forEach(System.out :: println); 等同于 x -> System.out.println(x)
删除
表达式 String :: compareToIgnoreCase 是方法引用,它等同于 lambda 表达式 (x, y) -> x.compareToIgnoreCase(y)。
有3种使用方式,操作符 :: 将方法名称与类或对象名称分隔开:
- 类 :: 实例方法 –> 第一个参数变成方法的接受者,并且其它参数也传递给该方法。
- 类 :: 静态方法 –> 所有的参数传递给静态方法
- 对象 :: 实例方法 –> 在给定的对象上调用方法
构造函数引用
构造函数引用于方法引用类似,不同的是构造函数引用中的方法名都是 new。如:
int[] :: new //是一个含有一个参数的构造函数,该参数为数组的长度。它等同于 lambda 表达式 n -> new int[n]
使用 lambda 表达式
实现延迟执行
使用 lambda 表达式就在于延迟执行。毕竟,如果你想立即执行一段代码,无须将代码封装进 lambda ,你可以直接调用。延迟执行的原因很多,例如:
- 在另一个单独的线程运行代码
- 多次运行代码
- 在算法恰当时刻运行代码(例如排序中的比较操作)
- 当某些情况发生时运行代码(点击事件,数据到达等)
- 只有在有需要的时候才运行代码
常用函数式接口
- Runnable
- Supplier
- Function
- Consumer
- …
实现自己的函数式接口
- 声明接口
- 将接口对象传入函数的参数中
- 在函数里调用接口中的方法
- 调用函数,传入 lambda 表达式
lambda 表达式和变量作用域
lambda 表达式的作用域
lambda 表达式的方法体与嵌套代码块有着相同的作用域。因此,也使用同样的命名冲突和屏蔽规则。
如不允许局部变量与参数变量同名;方法内部不允许有同名的变量。
lambda 表达式中的关键字 this 代表的是创建 lambda 表达式的方法的 this 参数。
访问来自闭合作用域的变量
经常要在 lambda 表达式中访问来自闭合方法或类的变量。如下例子:
public static void repeatMessage(String text, int count){
Runnable r = () -> {
for (int i = 0; i < count; i++){
System.out.println(text);
}
};
new Thread(r).start();
}
//调用
repeatMessage("Hello", 1000);
lambda 表达式有三个部分:
- 代码块
- 参数
- 自由变量的值 —— 自由变量是指,既不是参数变量。也不是代码内部定义的变量。
上述示例中, lambda 表达式有两个自由变量, text 和 count。代表 lambda 表达式的数据结构必须存储这些变量的值(也就是 “hello” 和 “1000”)。我们说这些变量值以及被 lambda 表达式捕获了(通过具体的实现细节来完成捕获。例如,可以将 lambda 表达式转变为带有一个方法的对象,这样自由变量的值就可以复制到该对象的实例变量中)。
约束:在 lambda 表达式中,你只能引用那些值不会改变的变量。
如下会出错:
for (int i = 0; i < 100; i++){
new Thread(() -> System.out.println(i)).start();
}
也就是说捕获的变量应该是个 final 变量。
然而这样是可以的:
for (String s : friends){
new Thread(() -> System.out.println(s)).start();
}
每一个迭代器会创建一个新的 s 变量,并且从 数组 friends 中将下一个值赋给 s.
同样, lambda 表达式不能修改任何捕获的变量。
描述带有自由变量值的代码块的技术名词是闭包(closure), 在 java 中, lambda 表达式就是闭包