重构、测试和调试

Java8的新特性

第一节 为改善可读性和灵活性重构代码

学会了Lambda和流后我们可以在新项目中使用这些新特性,而一些老项目我们可能也想要用新的方式来重构代码,提高代码的可读性和灵活性。

用Lambda表达式可以相比匿名类更简洁,代码会更灵活,在需求有变更时,行为参数化的模式可以更有效的来应对。

为何说Java 8的新特性可以提高可读性?

  • 减少冗长的代码,代码更容易理解。
  • 通过方法引用和Stream API,代码会更直观。

如何通过Java 8的新特性提高可读性?

  • 重构代码,用Lambda表达式取代匿名类
  • 用方法引用重构Lambda表达式
  • 用Stream API重构命令式的数据处理

1.1 匿名类到Lambda表达式

用Lambda表达式代替匿名类时要注意二者的不同,如this和super在二者有不同的含义,对于匿名类this是类自身,而Lambda则表示包含类。匿名类可以屏蔽包含类变量,Lambda表达式则不行。对于重载的场景,Lambda表达式可能会有些歧义,多个函数可能都合法,可以通过显式的类型转换来解决此问题。

1.2 Lambda表达式到方法引用

方法名可以更直接的表达代码的意图,请尽量使用静态辅助方法。

1.3 命令式数据处理到Stream

所有使用迭代器这种处理模式来处理集合的代码都应该转换为Stream API的方式,流更能清楚的表达数据处理管道的意图,通过短路和延迟载入以及多核架构可以进行优化处理。

将命令式数据处理转换到Stream不是一件容易的事情,需要考虑控制流语句,选择恰当的流操作,不过已有一些工具可以辅助进行这一步转换。

1.4 增加代码灵活性

1.4.1 采用函数式接口

Lambda表达式的使用依赖于函数式接口,我们可以基于两种模式来重构代码:有条件的延迟执行环绕执行

1.4.2 有条件的延迟执行

控制语句经常会被混杂在业务逻辑代码之中,典型的场景就是安全性检查以及日志输出,如下述代码所示。

1
2
3
if(logger.isLoggable(Level.FINER)){
logger.finer("Problem: " + generateDiagnostic());
}

在上述代码中日志器的状态被isLoggable暴露给了客户端代码,在每次输出日志前都要查询日志器对象的状态,可以尝试下列改造。

1
logger.log(Level.FINER, "Problem: " + generateDiagnostic());

这样隐藏了日志器状态,也少去了条件判断,log方法会在内部检查日志对象是否已被设置为恰当的日志等级。但日志消息的输出与否每次仍需要进行判断,即使你已经传递了参数,不开启日志。

1
2
3
4
5
6
7
logger.log(Level.FINER, () -> "Problem: " + generateDiagnostic());

public void log(Level level, Supplier<String> msgSupplier) {
if (logger.isLoggable(level)) {
log(level, msgSupplier.get());
}
}

Java 8提供了log方法的重载版,提供了Supplier参数,主要思路就是延迟消息构造:如果你需要频繁的从客户端去查询一个对象的状态,只是为了传递参数、调用该对象的一个方法,那么就可以考虑实现一个新的方法,以Lambda或方法表达式作为参数,新方法在检查完该对象的状态后才调用原来的方法,这样的一层处理会使代码结构更清晰和易读,封装性更好。

1.4.3 环绕执行

环绕执行模式:如资源处理等场景往往需要打开一个资源,进行处理,然后关闭资源,准备和清理会环绕着执行处理的那些重要代码。业务代码虽然各不相同,但都有同样的准备和清理阶段。我们可以重用准备和清理阶段的逻辑,减少重复冗余的代码,如下所示,我们把打开和关闭文件看作重复操作,抽离出不同的处理方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
//通过函数式接口BufferedReaderProcesser,可以传递各种Lambda表达式对BufferedReader对象进行处理
String oneLine = processFile((BufferedReader b) -> b.readLine());
String twoLine = processFile((BufferedReader b) -> b.readLine() + b.readLine());

public static String processFile(BufferedReaderProcesser p)throws IOException{
try(BufferedReader br = new BufferedReader(new FileReader("d:/data.txt"))){
return p.process(br);
}
}

public interface BufferedReaderProcesser{
String process(BufferedReader b)throws IOException;
}

第二节 使用Lambda重构面向对象的设计模式

新特性的出现往往会代替旧的编程模式,如Java 5时引入的for-each,因为其稳健性和简洁性,已在大部分场合代替了显式使用迭代器的方式。Java 7时推出的<>菱形操作符使创建实例时无需显式使用泛型,也推动了开发者们使用类型接口进行程序设计。

Lambda表达式为传统设计模式所面对的问题提供了更高效和简单的新解决方案,以下会简单整理几个常见的设计模式,以及Lambda表达式对于这些设计模式的实现优化。

2.1 策略模式

  • 一个代表某个算法的接口
  • 一个或多个此接口的具体实现
  • 一个或多个使用策略对象的客户

策略模式

我们通过传统的方式实现一个策略模式的实例——字符串校验器,用户根据需求实现策略对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* 策略接口-校验策略
*/
public interface ValidationStratery {
boolean execute(String s);
}

/**
* 策略实现-是否全部小写
*/
public class IsAllLowerCase implements ValidationStratery {
@Override
public boolean execute(String s) {
return s.matches("[a-z]+");
}
}

/**
* 策略实现-是否数字
*/
public class IsNumeric implements ValidationStratery {
@Override
public boolean execute(String s) {
return s.matches("\\d+");
}
}

/**
* 策略接口实现类-校验器
*/
public class Validator {
private final ValidationStratery stratery;

public Validator(ValidationStratery v){
this.stratery = v;
}

public boolean validate(String s){
return stratery.execute(s);
}
}

public class Test {
public static void main(String[] args) {
//多个使用策略对象的客户
Validator numberValidator = new Validator(new IsNumeric());
boolean b1 = numberValidator.validate("aaaa");
System.out.println("b1 : " + b1);//false
Validator lowerCaseValidator = new Validator(new IsAllLowerCase());
boolean b2 = lowerCaseValidator.validate("bbbb");
System.out.println("b2 : " + b2);//true
}
}

通过Lambda表达式改写,代码如下。我们有函数式接口ValidationStratery,无需通过类来构建策略实现,实现的代码块通过Lambda表达式来参数化。

1
2
3
4
5
6
Validator numberValidatorL = new Validator((String s) -> s.matches("\\d+"));
boolean b3 = numberValidatorL.validate("aaaa");
System.out.println("b3 : " + b3);//false
Validator lowerCaseValidatorL = new Validator((String s) -> s.matches("[a-z]+"));
boolean b4 = lowerCaseValidatorL.validate("bbbb");
System.out.println("b4 : " + b4);//true

2.2 模板方法

有时你会希望使用某个算法,但需要对其中某些代码行进行改进从而达到想要的效果,模板方法模式可以解决这一需求。通常会使用抽象类来来表示算法,需要修改的部分方法可以通过继承来实现。

如下列在线银行需求,processCustomer方法搭建了在线银行算法的框架,不同的支行可以通过继承OnlineBanking类,对该方法提供差异性的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 在线银行:用户输入账户,应用从数据库获取用户详细信息,最终完成一些让用户满意的操作,不同分行的满意方式会不同。
*/
abstract class OnlineBanking {
/**
* 获取客户提供的ID,然后使客户满意
* @param id 账户
*/
public void processCustomer(int id){
Customer c = DataBase.getCustomerWithId(id);
makeCustomerHappy(c);
}

abstract void makeCustomerHappy(Customer c);
}

引入Lambda表达式,我们就可以不用再继承OnlineBanking类,只须传递不同的实现即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 重载processCustomer,引入函数式接口参数
* @param id 账户
* @param makeCustomerHappy 不同实现
*/
public void processCustomer(int id, Consumer<Customer> makeCustomerHappy){
Customer c = DataBase.getCustomerWithId(id);
makeCustomerHappy.accept(c);
}

public static void main(String[] args) {
new OnlineBankingNotAbs().processCustomer(1,(Customer c) -> System.out.println("Hello " + c.getName()));
}

2.3 观察者模式

某些事件发生时(如状态转变),如果一个对象(主题)需要自动的通知多个对象(观察者),就会采用观察者模式。

观察者模式

假设我们要为Twitter实现一个定制化的通知系统,如果一些报社订阅了新闻,在新闻中包含他们关注的关键字时会得到特别通知,实现代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
* 观察者接口
*/
public interface Observer {
/**
* 当接收到一条新闻时,会调用此方法
* @param tweet 内容
*/
void notify(String tweet);
}

/**
* 观察者—纽约时报
*/
public class NYTimes implements Observer {
@Override
public void notify(String tweet) {
if(tweet != null && tweet.contains("money")){
System.out.println("Breaking news in NY! " + tweet);
}
}
}

/**
* 观察者—卫报
*/
public class Guardian implements Observer {
@Override
public void notify(String tweet) {
if(tweet != null && tweet.contains("queen")){
System.out.println("Yet another news in London... " + tweet);
}
}
}

/**
* 观察者—世界报
*/
public class Lemonde implements Observer {
@Override
public void notify(String tweet) {
if(tweet != null && tweet.contains("wine")){
System.out.println("Today cheese, wine and news! " + tweet);
}
}
}

/**
* 主题—通知接口
*/
public interface Subject {
/**
* 注册观察者
* @param o 观察者
*/
void registerObserver(Observer o);

/**
* 通知观察者
* @param tweet 内容
*/
void notifyObservers(String tweet);
}

/**
* 主题实现—通知器
*/
public class Feed implements Subject {
//观察者集合
private final List<Observer> observers = new ArrayList<>();

@Override
public void registerObserver(Observer o) {
this.observers.add(o);
}

@Override
public void notifyObservers(String tweet) {
observers.forEach(o -> o.notify(tweet));
}
}

public class Test {
public static void main(String[] args) {
Feed f = new Feed();
f.registerObserver(new NYTimes());
f.registerObserver(new Guardian());
f.registerObserver(new Lemonde());
f.notifyObservers("The queen said her favourite book is Java 8 in Action!");
//Yet another news in London... The queen said her favourite book is Java 8 in Action!
}
}

如果引入Lambda表达式,我们就没必要去构建每个观察者的实现类了,无需显式的实例化观察者对象,直接传递Lambda表达式来表示需要执行的行为即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Feed f1 = new Feed();
f1.registerObserver((String tweet) -> {
if(tweet != null && tweet.contains("money")){
System.out.println("Breaking news in NY! " + tweet);
}
});
f1.registerObserver((String tweet) -> {
if(tweet != null && tweet.contains("queen")){
System.out.println("Yet another news in London... " + tweet);
}
});
f1.registerObserver((String tweet) -> {
if(tweet != null && tweet.contains("wine")){
System.out.println("Today cheese, wine and news! " + tweet);
}
});
f1.notifyObservers("The queen said her favourite book is Java 8 in Action!");

如果代码块很复杂,甚至还包含状态的话就不应该再使用Lambda表达式来代替类了。

2.4 责任链模式

责任链模式则是一种创建处理对象序列的通用方案。一个处理对象可能需要在完成一些工作后,将结果传递给另一个对象,这个对象接着做一些工作,再转交给下个对象,以此类推。

通常此模式通过定义一个代表处理对象的抽象类来实现,successor记录后续对象,一旦对象完成工作,就会将工作转交给后继。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public abstract class ProcessingObject<T> {
protected ProcessingObject<T> successor;

public void setSuccessor(ProcessingObject<T> successor){
this.successor = successor;
}

public T handle(T input){
T r = handleWork(input);
if(successor != null){
return successor.handle(r);
}
return r;
}

abstract protected T handleWork(T input);
}

责任链模式

这种实现就是模板方法设计模式,我们尝试继承并实现抽象方法,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class HeaderTextProcessing extends ProcessingObject<String> {
@Override
protected String handleWork(String input) {
return "From Raoul, Mario, and Alan: " + input;
}
}
public class SpellCheckerProcessing extends ProcessingObject<String> {
@Override
protected String handleWork(String input) {
return input.replaceAll("labda","lambda");
}
}
public class Test {
public static void main(String[] args) {
ProcessingObject<String> p1 = new HeaderTextProcessing();
ProcessingObject<String> p2 = new SpellCheckerProcessing();

//将两个处理链接起来
p1.setSuccessor(p2);

String result = p1.handle("Aren`t labdas really sexy?!!");
System.out.println(result);//From Raoul, Mario, and Alan: Aren`t lambdas really sexy?!!
}
}

如果引入Lambda表达式,我们可以不用构建不同的处理对象,而是将对象作为UnaryOperator的一个实例,并通过andThen进行连接。

1
2
3
4
5
6
UnaryOperator<String> headerTextProcessing = (String input) -> "From Raoul, Mario, and Alan: " + input;
UnaryOperator<String> spellCheckerProcessing = (String input) -> input.replaceAll("labda","lambda");
//将两个方法结合起来
Function<String, String> pipeline = headerTextProcessing.andThen(spellCheckerProcessing);
String result1 = pipeline.apply("Aren`t labdas really sexy?!!");
System.out.println(result1);//From Raoul, Mario, and Alan: Aren`t lambdas really sexy?!!

2.5 工厂模式

工厂模式可以隐藏实例化的逻辑而完成对象的创建。我们不会再暴露构造函数或配置给客户,也使客户创建产品时更加容易。

1
2
3
4
5
6
7
8
9
10
11
12
public class ProductFactory {
public static Product createProduct(String name){
switch (name){
case "loan" : return new Loan();//贷款
case "stock" : return new Stock();//股票
case "bond" : return new Bond();//债券
default: throw new RuntimeException("No such product " + name);
}
}
}

Product p = ProductFactory.createProduct("loan");

如果引入Lambda表达式,我们可以首先引用构造函数构建Map,通过函数式接口Supplier来传递构造器引用。但如果我们需要多个参数来构建产品时,这种方案的扩展性不是很好,你需要提供不同的函数接口,而无法采用统一使用一个简单接口的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static Product createProductL(String name){
Supplier<Product> p = map.get(name);
if(p != null) return p.get();
throw new IllegalArgumentException("No such product " + name);
}

final static Map<String, Supplier<Product>> map = new HashMap<>();
static {
map.put("loan",Loan::new);
map.put("stock",Stock::new);
map.put("bond",Bond::new);
}

Product p1 = ProductFactory.createProductL("loan");

第三节 测试Lambda表达式

Lambda表达式都是匿名函数无函数名,测试上会有问题,无法直接通过函数名的方式来调用。我们可以通过某个字段来访问Lambda函数,然后测试表达式所生成函数接口实例的行为。

1
2
3
4
5
6
7
8
9
10
@Test
public void testMoveRightBy()throws Exception{
Point p1 = new Point(5, 5);
Point p2 = p1.moveRightBy(10);

assertEquals(15, p2.getX());
assertEquals(5, p2.getY());
}

public final static Comparator<Point> compareByXAndThenY = Comparator.comparing(Point::getX).thenComparing(Point::getY);

Lambda表达式的初衷是封装一些逻辑给另外一个方法调用,所以不应该声明Lambda表达式为public,它们只是具体的实现细节,而是应该对使用Lambda表达式的方法进行测试。

有些时候我们会遇到一些比较复杂的Lambda表达式,包含了大量的业务逻辑,如需要处理复杂情况的定价算法。Lambda表达式可以转换为方法引用。

第四节 调试

当代码出现异常需要调试时,我们有两项武器:(1)查看栈跟踪 (2)输出日志

4.1 查看栈跟踪

首先需要调查程序在何处发生异常,为何会发生异常?程序的每次方法调用都会产生相应的调用信息,包括程序中方法调用的位置、该方法调用使用的参数、被调用方法的本地变量,这些信息都保存在栈帧上。程序失败时,你会获得它的栈跟踪,通过一个又一个的栈帧,通过这些信息你可以获取到程序失败时的方法调用列表。

因为Lambda表达式没有名字,其栈跟踪会很难分析,如以下代码运行时发生异常,因为没有函数名所以编译器只能为它们指定一个名字,即使替换为方法引用,依然会有类似很难分析的栈跟踪。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
at categories.java.a7stream.test.Debugging.lambda$main$0(Debugging.java:9)
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 categories.java.a7stream.test.Debugging.main(Debugging.java:9)

4.2 使用日志调试

对流水线进行调试,可以使用forEach把流操作的结果日志输出或记录到日志文件。不过一旦调用forEach,整个流就会恢复运行,这时就是流操作peek大显身手的时候,peek的设计初衷就是在流的每个元素恢复运行之前,插入执行一个动作,且不像forEach那样恢复整个流的运行,而是在一个元素上完成操作之后,它只会讲操作顺承到流水线中的下一个操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
List<Integer> numbers = Arrays.asList(2,3,4,5);
numbers.stream()
.map(x -> x + 17)
.filter(x -> x % 2 == 0)
.limit(3)
.forEach(System.out::println);
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)) //输出map操作的结果
.filter(x -> x % 2 == 0)
.peek(x -> System.out.println("after filter: " + x)) //输出经过filter操作之后,剩下的元素个数
.limit(3)
.peek(x -> System.out.println("after limit: " + x)) //输出经过limit操作之后,剩下的元素个数
.collect(Collectors.toList());

使用peek查看Stream流水线中的数据流的值

打印结果如下,forEach只能打印出流水线操作之后的结果,而peek则可以了解流水线操作中每一步的输出结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
20
22
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

参考:

🔗 《Java8实战》