重构,测试和调试(了解)
文章目录
为改善可读性和灵活性重构代码
利用Lambda表达式,你可以写出更简洁、更灵活的代码。
用“更简洁”来描述Lambda表达式是因为相较于匿名类,Lambda表达式可以帮助我们用更紧凑的方式描述程序的行为。
改善代码的可读性
什么是可读性呢,通常来说:就是指别人理解这段代码的难易程度
改善可读性意味着你要确保你的代码能非常容易地被包括自己在内的所有人理解和维护。
为了确保你的代码能被其他人理解,有几个步骤可以尝试,比如确保你的代码附有良好的文档,并严格遵守编程规范。
跟之前的版本相比较,Java 8的新特性也可以帮助提升代码的可读性:
- 使用Java 8,你可以减少冗长的代码,让代码更易于理解
- 通过方法引用和Stream API,你的代码会变得更直观
这里会介绍三种简单的重构,利用Lambda表达式、方法引用以及Stream改善程序代码的可读性:
- 重构代码,用Lambda表达式取代匿名类
- 用方法引用重构Lambda表达式
- 用Stream API重构命令式的数据处理
从匿名类到Lambda表达式的转换
// 经典的runnable
Runnable r1 = new Runnable() {
public void run() {
sout("Hello world");
}
}
// 替换成为Lambda表达式之后
Runnable r1 = () -> sout("Hello world");
但是某些情况下,将匿名类转换为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(Runnable) 和 doSomething(Task)都匹配该类型
doSomething(() -> System.out.println("Danger danger!!"));
可以对Task尝试使用显式的类型转换来解决这种模棱两可的情况:
doSomething((Task)() -> System.out.println("Danger danger!!"));
从Lambda表达式到方法引用的转换
Lambda表达式非常适用于需要传递代码片段的场景。
但是为了改善代码的可读性,也请尽量使用方法引用。因为方法名往往能更直观地表达代码的意图。
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream()
.collect(groupingBy(dish -> {
if (dish.getCalories() <= 400)
returnCaloricLevel.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));
// 为了实现这个方案,你还需要在Dish类中添加getCaloricLevel方法:
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;
}
}
除此之外,我们还应该尽量考虑使用静态辅助方法,比如comparing、maxBy。
这些方法设计之初就考虑了会结合方法引用一起使用
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
// 下面这种明显表达式更好理解
inventory.sort(comparing(Apple::getWeight));
从命令式的数据处理切换到Stream
Stream API能更清晰地表达数据处理管道的意图。
除此之外,通过短路和延迟载入以及利用介绍的现代计算机的多核架构。
List<String> dishNames = new ArrayList<>();
// 对于卡路里大于300的加入到dishName中
for(Dish dish: menu){
if(dish.getCalories() > 300){
dishNames.add(dish.getName());
}
}
// 可以轻松的改成Lambda表达式
List<String> dishNames = menu.stream()
.filter(dish -> dish.getCalories() > 300) // 过滤出来卡路里大于300的
.map(Dish::getName()) // map拿到每一个的名字
.collect(Collectors.toList()); // 收集器收集
不幸的是,将命令式的代码结构转换为Stream API的形式是个困难的任务
因为你需要考虑控制流语句,比如break、continue、return,并选择使用恰当的流操作。
增加代码的灵活性(了解)
Lambda表达式有利于行为参数化。
你可以使用不同的Lambda表示不同的行为,并将它们作为参数传递给函数去处理执行。
比如,我们可以用多种方式为Predicate创建筛选条件,或者使用Comparator对多种对象进行比较。
采用函数接口
首先,你必须意识到,没有函数接口,你就无法使用Lambda表达式。因此,你需要在代码中引入函数接口。
听起来很合理,但是在什么情况下使用它们呢?这里我们介绍两种通用的模式,你可以依照这两种模式重构代码,利用Lambda表达式带来的灵活性,它们分别是:
- 有条件的延迟执行
- 环绕执行。
有条件的延迟执行
控制语句被混杂在业务逻辑代码之中。典型的情况包括进行安全
性检查以及日志输出。比如,
if (logger.isLoggable(Log.FINER)){
logger.finer("Problem: " + generateDiagnostic());
}
这段代码的问题:
- 日志器的状态(它支持哪些日志等级)通过isLoggable方法暴露给了客户端代码。
- 为什么要在每次输出一条日志之前都去查询日志器对象的状态?这只能搞砸你的代码。
更好的方案是使用log方法,该方法在输出日志消息之前,会在内部检查日志对象是否已经设置为恰当的日志等级:
logger.log(Level.FINER, "Problem: " + generateDiagnostic());
这种方式更好的原因是你不再需要在代码中插入那些条件判断,与此同时日志器的状态也不再被暴露出去。不过,这段代码依旧存在一个问题。日志消息的输出与否每次都需要判断,即使你已经传递了参数,不开启日志。
这就是Lambda表达式可以施展拳脚的地方。你需要做的仅仅是延迟消息构造,如此一来,日志就只会在某些特定的情况下才开启(以此为例,当日志器的级别设置为FINER时)。
你可以通过下面的方式对它进行调用:
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表达式
String oneLine = processFile((BufferedReader b) -> b.readLine());
String twoLines = processFile((BufferedReader b) -> b.readLine() + b.readLine());
public static String processFile(BufferedReaderProcessor p) throws IOException {
try(BufferedReader br = new BufferedReader(new FileReader("java8inaction/chap8/data.txt"))){
return p.process(br);
}
}
@FunctionalInterface
public interface BufferedReaderProcessor{
String process(BufferedReader b) throws IOException;
}