java-8-实战读书笔记-第八章-重构测试和调试

7次阅读

共计 9227 个字符,预计需要花费 24 分钟才能阅读完成。

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

1. 改善代码的可读性

Java 8 的新特性也可以帮助提升代码的可读性:

  • 使用 Java 8,你可以减少冗长的代码,让代码更易于理解
  • 通过方法引用和 Stream API,你的代码会变得更直观

这里我们会介绍三种简单的重构,利用 Lambda 表达式、方法引用以及 Stream改善程序代码的可读性:

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

2. 从匿名类到 Lambda 表达式的转换

  • 在匿名类中,this 代表的是类自身,但是在 Lambda 中,它代表的是包含类。其次,匿名类可以屏蔽包含类的变量,而 Lambda 表达式不
    能(它们会导致编译错误),譬如下面这段代码:

    int a = 10; 
    Runnable r1 = () -> { 
    int a = 2; // 类中已包含变量 a
    System.out.println(a); 
    };
  • 对于参数相同的函数式接口,调用时会造成都符合 Lambda 表达式的结果,不过 NetBeans 和 IntelliJ 都支持这种重构,它们能自动地帮你检查,避免发生这些问题。

3. 从 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)); 

为了实现这个方案,你还需要在 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));
  • 使用 Collectors 接口可以轻松得到和或者最大值,与采用 Lambada 表达式和底层的归约操作比起来,这种方式要直观得多.
int totalCalories = 
  menu.stream().map(Dish::getCalories) 
  .reduce(0, (c1, c2) -> c1 + c2);

int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));

4. 从命令式的数据处理切换到 Stream

原来:

List<String> dishNames = new ArrayList<>(); 
for(Dish dish: menu){if(dish.getCalories() > 300){dishNames.add(dish.getName()); 
 } 
}

替换成流式:

menu.parallelStream() 
 .filter(d -> d.getCalories() > 300) 
 .map(Dish::getName) 
 .collect(toList());

5. 增加代码的灵活性

(1)采用函数接口

用 Lambda 表达式带来的灵活性,它们分别是:有条件的延迟执行和环绕执行。

(2)有条件的延迟执行

如果你发现你需要频繁地从客户端代码去查询一个对象的状态,只是为了传递参数、调用该对象的一个方法(比如输出一条日志),那么可以考虑实现一个新的方法,以 Lambda 或者方法表达式作为参数,新方法在检查完该对象的状态之后才调用原来的方法。

(3)环绕执行

如果你发现虽然你的业务代码千差万别,但是它们拥有同样的准备和清理阶段,这时,你完全可以将这部分代码用 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); 
 } 
} 
public interface BufferedReaderProcessor{String process(BufferedReader b) throws IOException; 
}

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

1. 策略模式

策略模式代表了解决一类算法的通用解决方案,你可以在运行时选择使用哪种方案。
数字)。你可以从定义一个验证文本(以 String 的形式表示)的接口入手:

public interface ValidationStrategy {boolean execute(String s); 
} 

其次,你定义了该接口的一个或多个具体实现:

public class IsAllLowerCase implements ValidationStrategy {public boolean execute(String s){return s.matches("[a-z]+"); 
  } 
} 
public class IsNumeric implements ValidationStrategy {public boolean execute(String s){return s.matches("\\d+"); 
  } 
} 

之后,你就可以在你的程序中使用这些略有差异的验证策略了:

public class Validator{ 
  private final ValidationStrategy strategy; 
  public Validator(ValidationStrategy v){this.strategy = v;} 
  public boolean validate(String s){return strategy.execute(s); 
  } 
} 
Validator numericValidator = new Validator(new IsNumeric()); 
boolean b1 = numericValidator.validate("aaaa"); 
Validator lowerCaseValidator = new Validator(new IsAllLowerCase ()); 
boolean b2 = lowerCaseValidator.validate("bbbb"); 

如果使用 Lambda 表达式,则为:

Validator numericValidator = 
 new Validator((String s) -> s.matches("[a-z]+")); 
boolean b1 = numericValidator.validate("aaaa"); 
Validator lowerCaseValidator = 
 new Validator((String s) -> s.matches("\\d+")); 
boolean b2 = lowerCaseValidator.validate("bbbb");

2. 模板方法

如果你需要采用某个算法的框架,同时又希望有一定的灵活度,能对它的某些部分进行改进,那么采用模板方法设计模式是比较通用的方案。

abstract class OnlineBanking {public void processCustomer(int id){Customer c = Database.getCustomerWithId(id); 
 makeCustomerHappy(c); 
 } 
 abstract void makeCustomerHappy(Customer c); 
} 

processCustomer 方法搭建了在线银行算法的框架:获取客户提供的 ID,然后提供服务让用户满意。不同的支行可以通过继承 OnlineBanking 类,对该方法提供差异化的实现。
如果使用 Lambda 表达式:

public void processCustomer(int id, Consumer<Customer> makeCustomerHappy){Customer c = Database.getCustomerWithId(id); 
 makeCustomerHappy.accept(c); 
} 

new OnlineBankingLambda().processCustomer(1337, (Customer c) -> 
 System.out.println("Hello" + c.getName());

3. 观察者模式

例子:好几家报纸机构,比如《纽约时报》《卫报》以及《世界报》都订阅了新闻,他们希望当接收的新闻中包含他们感兴趣的关键字时,能得到特别通知。

interface Observer {void notify(String tweet); 
}
class NYTimes implements Observer{public void notify(String tweet) {if(tweet != null && tweet.contains("money")){System.out.println("Breaking news in NY!" + tweet); 
 } 
 } 
} 
class Guardian implements Observer{public void notify(String tweet) {if(tweet != null && tweet.contains("queen")){System.out.println("Yet another news in London..." + tweet); 
 } 
 } 
} 
class LeMonde implements Observer{public void notify(String tweet) {if(tweet != null && tweet.contains("wine")){System.out.println("Today cheese, wine and news!" + tweet); 
 } 
 } 
}
interface Subject{void registerObserver(Observer o); 
 void notifyObservers(String tweet); 
}
class Feed implements Subject{private final List<Observer> observers = new ArrayList<>(); 
 public void registerObserver(Observer o) {this.observers.add(o); 
 } 
 public void notifyObservers(String tweet) {observers.forEach(o -> o.notify(tweet)); 
 } 
}
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!");

使用 Lambda 表达式 后,你无需显式地实例化三个观察者对象,直接传递 Lambda 表达式表示需要执行的行为即可:

f.registerObserver((String tweet) -> {if(tweet != null && tweet.contains("money")){System.out.println("Breaking news in NY!" + tweet); 
 } 
}); 
f.registerObserver((String tweet) -> {if(tweet != null && tweet.contains("queen")){System.out.println("Yet another news in London..." + tweet); 
 } 
});

4. 责任链模式

责任链模式是一种创建处理对象序列(比如操作序列)的通用方案。一个处理对象可能需要在完成一些工作之后,将结果传递给另一个对象,这个对象接着做一些工作,再转交给下一个处理对象,以此类推。通常,这种模式是通过定义一个代表处理对象的抽象类来实现的,在抽象类中会定义一个字段来记录后续对象。一旦对象完成它的工作,处理对象就会将它的工作转交给它的后继。

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); 
}
public class HeaderTextProcessing extends ProcessingObject<String> {public String handleWork(String text){return "From Raoul, Mario and Alan:" + text;} 
} 
public class SpellCheckerProcessing extends ProcessingObject<String> {public String handleWork(String text){return text.replaceAll("labda", "lambda"); 
 } 
}
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); 

使用 Lambda 表达式
你可以将处理对象作为函数的一个实例,或者更确切地说作为 UnaryOperator<String> 的一个实例。为了链接这些函数,你需要使用 andThen 方法对其进行构造。

UnaryOperator<String> headerProcessing = 
 (String text) -> "From Raoul, Mario and Alan:" + text;
UnaryOperator<String> spellCheckerProcessing = 
 (String text) -> text.replaceAll("labda", "lambda"); 
Function<String, String> pipeline = 
 headerProcessing.andThen(spellCheckerProcessing); 
String result = pipeline.apply("Aren't labdas really sexy?!!");

5. 工厂模式

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 表达式
第 3 章中,我们已经知道可以像引用方法一样引用构造函数。比如,下面就是一个引用贷款
(Loan)构造函数的示例:

构造器参数列表要与接口中抽象方法的参数列表一致! 因此,如果构造方法中有多个参数,需要自定义函数式接口。

Supplier<Product> loanSupplier = Loan::new; 
Loan loan = loanSupplier.get(); 

通过这种方式,你可以重构之前的代码,创建一个 Map,将产品名映射到对应的构造函数:

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); 
} 

现在,你可以像之前使用工厂设计模式那样,利用这个 Map 来实例化不同的产品。

public static Product createProduct(String name){Supplier<Product> p = map.get(name); 
 if(p != null) return p.get(); 
 throw new IllegalArgumentException("No such product" + name); 
}

三、测试 Lambda 表达式

  • 你可以借助某个字段访问 Lambda 函数
  • 要对使用 Lambda 表达式的方法进行测试
  • 一种策略是将 Lambda 表达式转换为方法引用,然后按照常规方式
  • 接受函数作为参数的方法或者返回一个函数的方法(所谓的“高阶函数”,higher-order function,我们在第 14 章会深入展开介绍)更难测试。如果一个方法接受 Lambda 表达式作为参数,你可以采用的一个方案是使用不同的 Lambda 表达式对它进行测试。

文中提到了List 的 equals 方法
ArrayList、Vector 两者都实现了 List 接口、继承 AbstractList 抽象类,其 equals 方法是在 AbstractList 类中定义的,源代码如下:

public boolean equals(Object o) {if (o == this)        
     return true;    
  // 判断是否是 List 列表,只要实现了 List 接口就是 List 列表
  if (!(o instanceof List))        
     return false;    
  // 遍历 list 所有元素
  ListIterator<E> e1 = listIterator();
  ListIterator e2 = ((List) o).listIterator();    
  while (e1.hasNext() && e2.hasNext()) {E o1 = e1.next();
      Object o2 = e2.next();        
      // 有不相等的就退出
      if (!(o1==null ? o2==null : o1.equals(o2)))            
          return false;
   }    
   // 长度是否相等
   return !(e1.hasNext() || e2.hasNext());

从源码可以看出,equals 方法并不关心 List 的具体实现类,只要是实现了 List 接口,并且所有元素相等、长度也相等 的话就表明两个 List 是相等的,所以例子中才会返回 true。

四、调试

1. 查看栈跟踪

由于 Lambda 表达式没有名字,它的栈跟踪可能很难分析, 编译器只能为它们指定一个名字, 如果你使用了大量的类,其中又包含多个 Lambda 表达式,这就成了一个非常头痛的问题, 这是 Java 编译器未来版本可以改进的一个方面。

2. 使用日志调试

这就是流操作方法 peek 大显身手的时候。peek的设计初衷就是在流的每个元素恢复运行之前,插入执行一个动作。但是它不像 forEach 那样恢复整个流的运行,而是在一个元素上完成操作之后,它只会将操作顺承到流水线中的下一个操作。

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)) 
// 输 出 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(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

正文完
 0