重构代码
从匿名类到Lambda表达式
回顾之前的例子:
Runnable r1 = new Runnable(){
@Override
public void run(){
System.out.println("Hello");
}
};
Runnable r2 = () -> System.out.println("Hello");
但是某些情况下,将匿名类转换为Lambda表达式可能是一个比较复杂的过程。
首先,匿名类和Lambda表达式中的this和super的含义是不同的。在匿名类中,this代表的是类自身,但是在Lambda中,它代表的是包含类。
其次,匿名类可以屏蔽包含类的变量,而Lambda表达式不能(它们会导致编译错误)。
匿名类编译正常:
int a = 10;
Runnable r2 = new Runnable(){
public void run(){
int a = 2;
System.out.println(a);
}
};
Lambda编译错误:
Runnable r1 = () -> {
int a = 2;
System.out.println(a);
};
在涉及重载的上下文里,将匿名类转换为Lambda表达式可能导致最终的代码更加晦涩。考虑下面的代码:
@FunctionalInterface
public interface Task {
void execute();
}
public class AnonymousLambda {
public static void doSomething(Runnable r){
r.run();
}
public static void doSomething(Task t){
t.execute();
}
public static void main(String[] args) {
//报错
doSomething(()-> System.out.println("helloworld"));
}
}
报错结果如下:Ambiguous method call,晦涩的方法调用
解决这种情况的可以使用下面的方式:
doSomething((Task)()-> System.out.println("helloworld"));
从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方法。变换之后,代码变得更简介,程序的意图也更加清晰了。
public enum CaloricLevel {
DIET,NORMAL,FAT;
}
public class Dish {
...
public CaloricLevel getCaloricLevel(){
if (this.getCalories() <= 400) return CaloricLevel.DIET;
else if (this.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
}
}
psvm:
Map<CaloricLevel, List<Dish>> dishesByCaloric=menu.stream().collect(groupingBy(Dish::getCaloricLevel));
除此之外,我们还应该尽量考虑使用静态辅助方法,比如comparing、maxBy。这些方法设计之初就考虑了会结合方法引用一起使用。
inventory.sort(
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
inventory.sort(comparing(Apple::getWeight));
此外,很多通用的归约操作,比如sum、maximum,都有内建的辅助方法可以和方法引用结合使用。
与其使用:
int totalCalories =
menu.stream().map(Dish::getCalories)
.reduce(0, (c1, c2) -> c1 + c2);
不如使用:
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
从命令式的数据处理切换到Stream
我们建议你将所有使用迭代器这种数据处理模式处理集合的代码都转换成Stream API的方式。为什么呢?Stream API能更清晰地表达数据处理管道的意图。除此之外,通过短路和延迟载入以及利用现代计算机的多核架构,我们可以对Stream进行优化。
比如考虑下面的代码:
List<String> dishNames = new ArrayList<>();
for(Dish dish: menu){
if(dish.getCalories() > 300){
dishNames.add(dish.getName());
}
}
替代方案是:
List<String> dishNames=menu.parallelStream()
.filter(d->d.getCalories()>300)
.map(Dish::getName)
.collect(toList());
不幸的是,将命令式的代码结构转换Stream API的形式是个困难的任务,因为你需要考虑控制流语句,比如break、continue、return,并选择使用恰当的流操作。好消息是已经有一些工具可以帮助我们完成这个任务。
增加代码的灵活性
有条件的延迟执行
我们经常看到这样的代码,控制语句被混杂在业务逻辑代码之中。典型的情况包括进行安全性检查以及日志输出。比如,下面的这段代码,它使用了Java语言内置的Logger类:
if (logger.isLoggable(Log.FINER)){
logger.finer("Problem: " + generateDiagnostic());
}
这段代码有什么问题吗?其实问题不少。
- 日志器的状态(它支持哪些日志等级)通过isLoggable方法暴露给了客户端代码。
- 为什么要在每次输出一条日志之前都去查询日志器对象的状态?这只能搞砸你的代码。
更好的方案是使用log方法,该方法在输出日志消息之前,会在内部检查日志对象是否已经设置为恰当的日志等级:
logger.log(Level.FINER, "Problem: " + generateDiagnostic());
这种方式更好的原因是你不再需要在代码中插入那些条件判断,与此同时日志器的状态也不再被暴露出去。不过,这段代码依旧存在一个问题。日志消息的输出与否每次都需要判断,即使你已经传递了参数,不开启日志。
这就是Lambda表达式可以施展拳脚的地方。你需要做的仅仅是延迟消息构造,如此一来,日志就只会在某些特定的情况下才开启(以此为例,当日志器的级别设置为FINER时)。显然,Java 8的API设计者们已经意识到这个问题,并由此引入了一个对log方法的重载版本,这个版本的log方法接受一个Supplier作为参数。这个替代版本的log方法的函数签名如下:
public void log(Level level, Supplier<String> msgSupplier)
可以进行如下的调用:
logger.log(Level.FINER, () -> "Problem: " + generateDiagnostic());
如果日志器的级别设置恰当,log方法会在内部执行作为参数传递进来的Lambda表达式。这里介绍的Log方法的内部实现如下:
public void log(Level level, Supplier<String> msgSupplier){
if(logger.isLoggable(level)){
log(level, msgSupplier.get());
}
}
以Lambda或者方法表达式作为参数,新方法在检查完该对象的状态之后才调用原来的方法。你的代码会因此而变得更易读(结构更清晰),封装性更好(对象的状态也不会暴露给客户端代码了)。
环绕执行
如果你发现虽然你的业务代码千差万别,但是它们拥有同样的准备和清理阶段,这时,你完全可以将这部分代码用Lambda实现。
回顾一下,在打开和关闭文件时使用了同样的逻辑,但在处理文件时可以使用不同的Lambda进行参数化。
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
public class BufferLambda {
public static String processFile(BufferedReaderProcessor b) throws IOException {
try (BufferedReader br =
new BufferedReader(new FileReader("data.txt"))) {
return b.process(br);
}
}
public static void main(String[] args) {
try {
processFile((BufferedReader b)->b.readLine()+b.readLine());
processFile(BufferedReader::readLine);
} catch (IOException e) {
e.printStackTrace();
}
}
}