lambda 就是希腊字母 λ
代码块
lambda 表达式是一个可传递的代码块,可以在以后执行一次或多次。
先来看一下我们前面用到的代码块。
例如,向 Arrays.sort()
传入一个 Comparator 对象:
public LenghtComparator implements Comparator<String> {
public int compare(String first, String second) {
return first.length() - second.length();
}
}
...
Arrays.sort(Strings, new LenghtComparator());
compare 方法不是立即调用。实际上,在数组完成排序之前,sort 方法会一直调用 compare 方法,只要元素的顺序不正确就会调用 compare 方法重新排列元素。
总结一下,上述过程就是将一个代码块传递到某个对象。这个代码块将会在将来某个时间调用。
lambda 表达式的语法
lambda 表达式非常简单,就拿上面的例子转换成 lambda 表达式为:
(String first, String second) -> first.length() - second.lenght()
// like
(String first, String second) -> { return first.length() - second.lenght() }
因为 Java 是强类型语言,所以这里要指定他们的类型(String)。
如果可以推导出一个 lambda 表达式的参数类型,可以忽略其类型:
Comparator<String> comp = (first, second) // Same as (String first, String second)
-> first.length() - second.length();
这里编译器会自动推断出 first 和 second 是字符串,因为这个 lambda 表达式将赋值给一个字符串比较器
如果方法只有一个参数,而且这个参数的类型可以推导得出,那么甚至还可以省略小括号:
ActionListener listener = event ->
System.out.println("The time is " + new Date());
无需指定 lambda 表达式的返回类型。lambda表达式的返回类型总是会由上下文推导得出。
函数式接口
对于只有一个抽象方法的接口,需要这种接口的对象(实例)时,就可以提供一个 lambda 表达式。这种接口称为函数式接口(functional interface)。
讲解一下抽象方法的理解。一种方式:接口可能会重新声明 Object 类的方法,如 toString 或 clone,这些声明有可能会让方法不再是抽象的。另一种是,在 Java SE 8 中,接口本身就可以声明非抽象方法:默认方法。
比如 Arrays.sort()
第二个参数需要一个 Comparator 实例, Comparator 就是只有一个方法的接口,所以可以提供一个 lambda 表达式:
Arrays.sort(words, (first, second) -> first.length() - second.lenght())
在底层,Arrays.sort()
会接收实现了 Comparator
lambda 表达式可以转换为接口。具体的语法很简短。下面再来看一个例子:
Timer t = new Timer(1000, event -> {
System.out.println("At the tone, the tiem is " + new Date());
Toolkit.getDefaultToolkit().beep();
});
与使用实现了 ActionListener 接口的类相比,这个代码可读性要好得多。
不能把lambda表达式赋给类型为Object的变量,Object不是一个函数式接口。
Java API 在 java.util.function 包中定义了很多非常通用的函数式接口。其中一个接口 BiFunction
BiFunction<String, String, Integer> biFunction =
(first, second) -> first.length() - second.length();
不过,这对于排序并没有帮助。没有哪个 Arrays.sort 方法想要接收一个 BiFunction。
方法引用
有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。例如,假设你希望只要出现一个定时器事件就打印这个事件对象。当然,为此也可以调用:
Timer timer = new Timer(1000, event -> System.out.println(event));
但是,如果直接把 println 方法传递到 Timer 构造器就更好了。具体做法如下:
Timer timer = new Timer(1000, System.out::println);
表达式 System.out::println
是一个方法引用(method reference),它等价于 lambda 表达式 x->System.out.println(x)
。
这里的 x 是表示任意值,可以是 event -> System.out.println(event)),也可以是其他值
假设你想对字符串排序,而不考虑字母的大小写。可以传递以下方法表达式:
Arrays.sort(strings, String::compareToIgnoreCase);
从这些例子可以看出,要用 ::
操作符分隔方法名与对象或类名。主要有3种情况:
- Object::instanceMethod
- Class::staticMethod
- Class::instanceMethod
在前 2 种情况中,方法引用等价于提供方法参数的 lambda 表达式。前面已经提到,System.out::println
(第一种情况)等价于 x->System.out.println(x)
。类似地,Math::pow
(第二种情况)等价于 (x,y)->Math.pow(x,y)
。
但是对于第 3 钟情况,第一个参数会成为目标的方法。例如,String::compareToIgnoreCase
等同于 (x, y) -> x.compareToIgnoreCase(y)
。
可以在方法引用中使用 this 参数。例如,this::equals
等同于 x->this.equals(x)
。使用 super 也是合法的。例如:
class Greeter {
public void greet() {
System.out.println("Hello world!");
}
}
class TimedGreeter extends Greeter {
public void greet() {
Timer t = new Timer(1000, super::greet);
t.start();
}
}
构造器引用
构造器引用与方法引用很类似,只不过方法名为 new。例如,Person::new 是 Person 构造器的一个引用。哪一个构造器呢?这取决于上下文。
假设你有一个字符串列表。可以把它转换为一个 Person 对象数组,为此要在各个字符串上调用构造器,调用如下:
ArrayList<String> names = new ArrayList<String>(Arrays.asList(new String[]{"yikang", "kangkang"}));
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.collect(Collectors.toList());
map 方法会为各个列表元素调用 Person(String) 构造器。如果有多个 Person 构造器,编译器会选择有一个 String 参数的构造器,因为它从上下文推导出这是在对一个字符串调用构造器。
可以用数组类型建立构造器引用。例如,int[]::new
是一个构造器引用,它有一个参数:即数组的长度。这等价于 lambda 表达式 x->new int[x]
。
变量作用域
lambda 表达式有三部分:
- 一个代码块
- 参数
- 只有变量的值,指非参数而且不再代码中定义的变量。
例如:
public static void repeatMessage(String text, int dalay) {
ActionListener listener = event -> {
System.out.println(text);
Toolkit.getDefaultToolkit().beep();
};
new Timer(dalay, listener).start();
}
这个 lambda 表达式有一个自由变量 text。表示lambda表达式的数据结构必须存储自由变量的值。我们说 text 被 lambda 表达式捕获(captured)。
可以看到,lambda 表达式可以捕获外围作用域中变量的值。在 Java 中,要确保所捕获的值是明确定义的,这里有一个重要的限制。在 lambda 表达式中,只能引用值不会改变的变量。例如,下面的做法是不合法的:
public static void coutDown(int start, int dalay) {
ActionListener listener = event -> {
start--; // Error: Can't mutate cappture varibale
System.out.println(start);
};
new Timer(dalay, listener).start();
}
之所以有这个限制是有原因的。如果在 lambda 表达式中改变变量,并发执行多个动作时就会不安全。
另外如果在 lambda 表达式中引用变量,而这个变量可能在外部改变,这也是不合法的。例如,下面就是不合法的:
public static void repeat(String text, int count) {
for (int i = 1; i <= count; i++) {
ActionListener listener = event -> {
System.out.println(i + ": " + text);
};
new Timer(1000, listener).start();
}
}
这里有一条规则:lambda 表达式中捕获的变量必须实际上是最终变量(effectively final)。实际上的最终变量是指,这个变量初始化之后就不会再为它赋新值。在这里,text 总是指示同一个 String 对象,所以捕获这个变量是合法的。不过,i 的值会改变,因此不能捕获 i。
lambda 表达式的体与嵌套块有相同的作用域。这里同样适用命名冲突和遮蔽的有关规则。在 lambda 表达式中声明与一个局部变量同名的参数或局部变量是不合法的。
Paht first = Paths.get("/usr/bin");
Comparator<String> comp = (first, second) -> first.length() - second.length();
在方法中,不能有两个同名的局部变量,因此,lambda 表达式中同样也不能有同名的局部变量。
在一个 lambda 表达式中使用 this 关键字时,是指创建这个 lambda 表达式的方法的 this 参数。例如,考虑下面的代码:
public class Application() {
public void init() {
ActionListener listener = event -> {
System.out.println(this.toString();
...
}
...
}
}
表达式this.toString()会调用Application对象的toString方法,而不是ActionListener实例的方法。
处理 lambda 表达式
使用 lambda 表达式的重点是延迟执行(deferred execution)。之所以希望以后再执行代码,这有很多原因,如:
- 在一个单独的线程中运行代码;
- 多次运行代码;
- 在算法的适当位置运行代码(例如,排序中的比较操作);
- 发生某种情况时执行代码(如,点击了一个按钮,数据到达,等等);
- 只在必要时才运行代码。
比如,你现在想要重复一个动作 n 次,将这个动作和重复次数传递到一个 repeat 方法:
repeat(10, () -> System.out.println("Hello World!"));
要接受这个 lambda 表达式,需要选择(偶尔可能需要提供)一个函数式接口。在这里,我们可以使用 Runnable 接口:
public static void repeat(int n, Runnable action) {
for (int i = 0; i < n; i++) action.run();
}
调用 action.run()
会执行上述的 lambda 表达式:() -> System.out.println("Hello World!")
这里列出常用的函数式接口:
| 函数式接口 | 参数类型 | 返回类型 | 抽象方法名 | 描述 | 其他方法 | | —- | —- | —- | —- | —- | —- |
| Runnable | 无 | void | run | 作为无参数或返回值的动作进行 | |
|
Supplier
|
Consumer
|
BiConsumer
|
Function
|
BiFunction
|
UnaryOperator
|
BinaryOperator
|
Predicate
|
BiPredicate
现在来改进一下上述例子,每次打印的时候,打印出是那一次迭代。这样的函数式接口需要包含一个方法,这个方法你有一个 int 参数而且返回类型为 void。处理 int 值得标准接口如下:
public interface IntComsumer {
void accept(int value);
}
这样就可以写出 repeact 得改进版本:
public static void repeat(int n, IntConsumer action) {
for (int i = 0; i < n; i++) action.accept(i);
}
...
repeact(10, i -> System.out.println("Countdown: " + (9 - i)));
下面列出基本类型的函数式接口:
| 函数式接口 | 参数类型 | 返回类型 | 抽象方法名 | | —- | —- | —- | —- |
| BooleanSupplier | none | boolean | getAsBoolean |
| P_Supplier | none | _p | getAsP |
| P_Consumer | _p | void | accept |
|
ObjP_Consumer
|
P_Functino
| P_To_Q_Function | _p | q | applyAsQ |
|
ToP_Function
|
ToP_BiFunction
| P_UnaryOperator | _p | p | applyAsP |
| P_BinaryOperator | _p, p | p | applyAsP |
| P_Predicate | _p | boolean | test |
p, q 为 int, long, double; P, Q 为 Int, Long, Double
如果设计你自己的接口,其中只有一个抽象方法,可以用 @FunctionalInterface
注解来标记这个接口。这样做有两个优点。如果你无意中增加了另一个非抽象方法,编译器会产生一个错误消息。另外 javadoc 页里会指出你的接口是一个函数式接口。当然不是必须使用注解,根据定义,任何有一个抽象方法的接口都是函数式接口。不过使用 @FunctionalInterface
注解确实是一个很好的做法。
再谈 Comparator
Comparator 接口包含很多方便的静态方法来创建比较器。这些方法可以用于 lambda 表达式或方法引用。
静态 comparing 方法是一个「键提取器」函数,它将类型 T 映射为一个可比较的类型。对要比较的对象应用这个函数,然后对返回的键完成比较。
例如,假设有一个 Person 对象数组,可以如下按名字对这些对象排序:
Arrays.sort(people, Comparator.comparing(Person::getName));
上述方法比手动实现一个 Comparator 相比,要容易得多。另外,代码也更为清晰,因为显然我们都希望按人名来进行比较。
可以把比较器与 thenComparing 方法串起来。例如:
Arrays.sort(people, Comparator.comparing(Person::getLastName)
.thenComparing(Person:getFirstName));
如果两个人的姓相同,就会使用第二个比较器。
comparing 有很多变体形式,可以该方法提取的键指定一个比较器,例如,可以根据人名长度完成排序:
Arrays.sort(people, Comparator.comparing(Person::getLastName),
(s, t) -> Integer.compare(s.length(), t.length()));
comparing 还有变体形式,可以避免 int, long, double 值的装箱。比如,这样可以完成前一个操作:
Arrays.sort(people, Comparator.comparingInt(p -> p.getName().lenght()));
如果键函数会返回 null 值,就要用到 nullsFirst 和 nullLast 适配器。这些静态方法会修改现有的比较器,从而在遇到null值时不会抛出异常,而是将这个值标记为小于或大于正常值。