一、为什么要引入lamda表达式
众所周知,软件工程领域需求最大的不变之处就是变化。行为参数化就是应对频繁变化的软件需求的一种软件开发模式。我们可以先准备好一段代码块,不去执行它,而是作为参数传递给另一个方法,延迟执行。需求变化了,只需要传递代表另一个行为的参数即可。
想想java 8之前我们是怎么把代码传递给方法的?来看下面一段函数:
Collections.sort(inventory, new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});
为了对苹果进行排序,我们新建了对象(实现了Comparator接口),明确地实现了compare方法来描述排序的核心逻辑,是不是很啰嗦!!这里就有lamda表达式大显身手的地方了,它用一种更简单的方式来传递代码。上面的例子使用lambda表达式的版本如下:
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
接下来我们会详细讲解如何编写和使用lambda
二、Lambda表达式的基本语法
Lambda的基本语法是
(parameters) -> expression
或(请注意语句的 括号)
(parameters) -> { statements; }
Lambda表达式有三个部分,如图1所示。
- 参数列表--这里它用了Comparator中compare方法的参数,两个Apple
- 箭头--把参数列表与Lambda主体分 开。
- Lambda主体--描述比较两个Apple的重量。
lambda表达式有一些重要的特征如下:
- 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
- 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
- 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
- 可选的return关键字:如果主体只有一个表达式返回值则编译器会自动返回值,如果有大括号需要指定明表达式返回了一个数值。
我们再多看一些lambda表达式的例子,来熟悉它的基本语法。
// 如果主体只有一个表达式返回值则编译器会自动返回值
(String s) -> s.length()
// 不需要声明参数类型,编译器可以统一识别参数值
// 一个参数无需定义圆括号
s -> s.length()
这个Lambda表达式具有String类型的参数并返回一个int。以上两个lambda表达式等效。
// 主体有多条语句,需要大括号包起来
// 非void返回情况下,如果有大括号需要指明表达式返回了一个数值
(int x, int y) -> {
System.out.println("Result:");
return x + y;
}
上面的Lambda表达式具有两个int类型的参数而没有返回值(void返回)。主体可以包含多行语句,这里是两行。
() -> 42
上面的表达式没有入参,返回一个int
三、哪里以及如何使用Lambda表达式
先给出一个结论:可以在函数式接口上使用Lambda表达式,那么什么是函数式接口呢?
函数式接口是只定义一个抽象方法的接口。Java API中有一些常见的函数式接口,比如Comparator和Runnable。接口还可以拥有默认方法,哪怕有很多默认方法,只要接口只定义一个抽象方法,它就仍然是一个函数式接口。
public interface Comparator<T> {
int compare(T o1, T o2);
}
public interface Runnable {
public abstract void run();
}
那么问题来了,函数式接口和lambda表达式之间的关联是什么呢?lambda表达式是函数式接口一个具体实现的实例。如果方法使用函数式接口作为参数,那么就可以传递一个具体的lambda表达式了。看下面的例子:
public static void process(Runnable r){
r.run();
}
process方法接收一个Runnable接口具体实现的实例,在java8之前,我们可以使用匿名类按照如下的方式传递参数:
Runnable r1 = new Runnable() {
public void run() {
System.out.println("Hello World 1");
}
};
process(r1);
使用lamba表达式,我们可以按如下的方式传递参数:
Runnable r2 = () -> System.out.println("Hello World 2");
process(r2)
甚至不需要定义临时变量
process(() -> System.out.println("Hello World 3"));
可以看到,Lambda表达式可以作为参数传递给方法或存在变量中。
Java 8的库设计师帮我们在java.util.function包中引入了一些新的函数式接口,比如Predicate、Consumer和Function等。以Predicate为例,java.util.function.Predicate<T>接口定义了一个名 test的抽象方法,它接受泛型T对象,并返回一个boolean。
@FunctionalInterface
public interface Predicate<T>{
boolean test(T t);
}
在你需要表示一个涉及类型T的布尔表达式时,就可以使用这个接口,比如下面一段代码用于筛选出列表中的偶数。
List<Integer> list= Arrays.asList(1,2,3,4,5,6,7,8,9,10);
list.stream()
.filter(i -> i % 2 == 0)
.forEach(System.out::println);
其中,filter方法的参数就是一个Predicate接口类型,接受泛型类型T对象,返回boolean
Stream<T> filter(Predicate<? super T> predicate);
实际工作中可以根据需要选择匹配的函数式接口,甚至可以自己设计函数式接口
四、lambda表达式的优点和缺点
lambda表达式使我们的代码更加简洁、清晰和灵活。可以把lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
不幸的是,由于lambda表达式没有名字,它的栈跟踪可能很难分析。在下面这段简单的代码中,我们刻意地引入了一些错误:
public class Debugging{
public static void main(String[] args) {
List<Point> points = Arrays.asList(new Point(12, 2), null);
points.stream().map(p -> p.getX()).forEach(System.out::println);
}
}
运行这段代码会产生下面的栈跟踪:
Exception in thread "main" java.lang.NullPointerException
12
at java8.Debugging.lambda$main$0(Debugging.java:15)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
at java8.Debugging.main(Debugging.java:15)
错误发生在Lambda表达式内部。由于Lambda表达式没有名字,所以编译器只能为它们指定一个名字。这个例子中,它的名字是lambda0,看起来非常不直观 。如果你使用了大量的类,其中又包含多个Lambda表达式,这就成了一个非常头痛的问题。
如果方法引用指向的是同一个类中声明的方法,那么它的名称是可以在栈跟踪中显示的。比如下面这个例子:
public class Debugging {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.stream()
.map(Debugging::divideByZero)
.forEach(System.out::println);
}
public static int divideByZero(int n) {
return n / 0;
}
}
输出结果如下:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at java8.Debugging.divideByZero(Debugging.java:23)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
at java8.Debugging.main(Debugging.java:19)
下面介绍一个流操作中对流水线进行调试的方法--peek,它能将流水线中间变量的值输出到日志中,是非常有用的工具。
public class Debugging{
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(2, 3, 4, 5);
List<Integer> result =
numbers.stream()
.peek(x -> System.out.println("from stream: " + x))
.map(x -> x + 17)
.peek(x -> System.out.println("after map: " + x))
.filter(x -> x % 2 == 0)
.peek(x -> System.out.println("after filter: " + x))
.limit(3)
.peek(x -> System.out.println("after limit: " + x))
.collect(toList());
}
}
运行结果如下:
from stream: 2
after map: 19
from stream: 3
after map: 20
after filter: 20
after limit: 20
from stream: 4
after map: 21
from stream: 5
after map: 22
after filter: 22
after limit: 22
可以看出,peek方法可以帮助我们跟踪Stream流水线中的每个操作(比如map、filter、limit)产生的输出。