Lambda
表达式重构代码Lambda
表达式对面向对象的设计模式的影响Lambda
表达式的测试Lambda
表达式和Stream API
的代码利用Lambda
表达式,你可以写出更简洁、更灵活的代码。用“更简洁”来描述Lambda
表达式是因为相较于匿名类,Lambda
表达式可以帮助我们用更紧凑的方式描述程序的行为。
采用Lambda
表达式之后,你的代码会变得更加灵活,因为Lambda
表达式鼓励大家使用行为参数化的方式。在这种方式下,应对需求的变化时,你的代码可以依据传入的参数动态选择和执行相应的行为。
改善代码的可读性到底意味着什么,我们很难定义什么是好的可读性,因为这可能非常主观。通常的理解是,“别人理解这段代码的难易程度”。改善可读性意味着你要确保你的代码能非常容易地被包括自己在内的所有人理解和维护。为了确保你的代码能被其他人理解,有几个步骤可以尝试,比如确保你的代码附有良好的文档,并严格遵守编程规范。
跟之前的版本相比较,Java 8
的新特性也可以帮助提升代码的可读性:
Java 8
,你可以减少冗长的代码,让代码更易于理解Stream API
,你的代码会变得更直观这里会介绍三种简单的重构,利用Lambda
表达式、方法引用以及Stream
改善程序代码的可读性:
Lambda
表达式取代匿名类Lambda
表达式Stream API
重构命令式的数据处理你值得尝试的第一种重构,也是简单的方式,是将实现单一抽象方法的匿名类转换为Lambda表达式。为什么呢?前面几章的介绍应该足以说服你,因为匿名类是极其繁琐且容易出错的。采用Lambda表达式之后,你的代码会更简洁,可读性更好。比如,第3章的例子就是一个创建Runnable对象的匿名类,这段代码及其对应的Lambda表达式实现如下:
Runnable r1 = new Runnable() {//传统的方式,使用匿名类
public void run() {
System.out.println("Hello");
);
Runnable r2 = () -> System.out.println("Hello");//新的方式,使用Lambda表达式
但是某些情况下,将匿名类转换为Lambda
表达式可能是一个比较复杂的过程。首先,匿名类和Lambda
表达式中的this
和super
的含义是不同的。在匿名类中,this
代表的是类自身,但是在Lambda
中,它代表的是包含类。其次,匿名类可以屏蔽包含类的变量,而Lambda
表达式不能(它们会导致编译错误),譬如下面这段代码:
int a = 10;
Runnable r1 =() -> {
int a = 2; //编译错误
System.out.println(a);
};
Runnable r2 = new Runnable() {
public void run() {
int a = 2; //一切正常
System.out.println(a);
};
最后,在涉及重载的上下文里,将匿名类转换为Lambda
表达式可能导致最终的代码更加晦涩。实际上,匿名类的类型是在初始化时确定的,而Lambda
的类型取决于它的上下文。通过下面这个例子,可以了解问题是如何发生的。假设用与Runnable
同样的签名声明了一个函数接口,称之为Task()
:
interface Task {
public void execute();
}
public static void doSomething(Runnable r) { r.run();}
public static void doSomething(Task a) { a.execute();}
现在,再传递一个匿名类实现的Task
,不会碰到任何问题:
doSomething(new Task() {
public void execute() {
System.out.println("Danger danger!!");
}
});
但是将这种匿名类转换为Lambda
表达式时,就导致了一种晦涩的方法调用,因为Runnable
和Task
都是合法的目标类型:
doSomething(() -> System.out.println("Danger danger!!")); //doSomething(Runnable)和doSomething(Task)都匹配该类型
可以对Task
尝试使用显式的类型转换来解决这种模棱两可的情况:
doSomething((Task)() -> System.out.println("Danger danger!!"));
但是不要因此而放弃对Lambda
的尝试。好消息是,目前大多数的集成开发环境,比如NetBeans
和IntelliJ
都支持这种重构,它们能自动地帮你检查,避免发生这些问题。
Lambda
表达式非常适用于需要传递代码片段的场景。不过,为了改善代码的可读性,也请尽量使用方法引用。因为方法名往往能更直观地表达代码的意图。比如,下面这段代码,它的功能是按照食物的热量级别对菜肴进行分类:
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream()
.collect(
groupingBy (dish ->(
if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getcalories() <= 700)return CaloricLevel.NORMAL;else return CaloricLevel.FAT;
}));
可以将Lambda
表达式的内容抽取到一个单独的方法中,将其作为参数传递给groupingBy
方法。变换之后,代码变得更加简洁,程序的意图也更加清晰了:
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect(groupingBy(Dish::getcaloricLevel)); //将Lambda表达式抽取到一个方法内
为了实现这个方案,还需要在Dish
类中添加getCaloricLevel
方法:
public class Dish {
public CaloricLevel getCaloricLevel() {
if (this.getCalories() <= 400) return CaloricLeve1.DIET;
else if (this.getCalories() <= 700)return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
}
}
除此之外,还应该尽量考虑使用静态辅助方法,比如comparing
、maxBy
。这些方法设计之初就考虑了会结合方法引用一起使用。通过示例,优化过的代码更清晰地表达了它的设计意图:
inventory.sort(
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight ()));//你需要考虑如何实 现比较算法
inventory.sort(comparing(Apple::getWeight));//读起来就像问题描述,非常清晰
此外,很多通用的归约操作,比如sum
、maximum
,都有内建的辅助方法可以和方法引用结合使用。比如,在示例代码中,使用Collectors
接口可以轻松得到和或者最大值,与采用Lambada
表达式和底层的归约操作比起来,这种方式要直观得多。与其编写:
int totalcalories = menu.stream().map(Dish::getCalories).reduce(0, (c1, c2) -> c1 + c2);
不如尝试使用内置的集合类,它能更清晰地表达问题陈述是什么。下面的代码中,使用了集合类summingInt
(方法的名词很直观地解释了它的功能):
int totalcalories = menu.stream().collect(summingInt(Dish::getcalories));