您现在的位置是:首页 >技术教程 >《Java8实战》第9章 重构、测试和调试网站首页技术教程

《Java8实战》第9章 重构、测试和调试

boy1007 2023-05-24 12:00:02
简介《Java8实战》第9章 重构、测试和调试

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

Lambda 表达式可以帮助我们用更紧凑的方式描述程序的行为。

9.1.1 改善代码的可读性

可读性非常主观,但是通俗的理解就是“别人理解这段代码的难易程度”。
改善可读性意味着你要确保你的代码能非常容易地被包括自己在内的所有人理解和维护。
使用 Java 8,你可以减少冗长的代码,让代码更易于理解。
使用lambda的三个简单的重构点:

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

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

传统方式的匿名内部类
Runnable r1 = new Runnable(){ 
 public void run(){ 
   System.out.println("Hello"); 
 } 
}; 

新的方式
Runnable r2 = () -> System.out.println("Hello"); 

但是在某些情况下,将匿名类转换为 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 的类型取决于它的上下文,这样可能会使代码更加晦涩。
比如这样:

interface Task{ 
 public void execute(); 
} 
public static void doSomething(Runnable r){ r.run(); } 
public static void doSomething(Task a){ a.execute(); }
如果使用的是匿名内部类,那么一看就知道使用了什么哪个参数类型
doSomething(new Task() { 
 public void execute() { 
   System.out.println("Danger danger!!"); 
 } 
});
如果使用lambda的话,你就分不清究竟使用的是哪个类型了
doSomething(() -> System.out.println("Danger danger!!"));
不过也可以使用显式的类型来调用
doSomething((Task)() -> System.out.println("Danger danger!!")); 

9.1.3 从 Lambda 表达式到方法引用的转换

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

可以修改成
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = 
 menu.stream().collect(groupingBy(Dish::getCaloricLevel)); 

把原来的判断代码封装到getCaloricLevel。
很多求和统计可以直接使用函数添加。

9.1.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()); 

将命令式的代码结构转换为 Stream API 的形式是个困难的任务,因为你需要考虑控制流语句,比如 break、continue 和 return,并选择使用恰当的流操作。不过已经有一些工具,比如 LambdaFicator

9.1.5 增加代码的灵活性

  1. 采用函数接口

没有函数接口,就无法使用 Lambda 表达式。因此,你需要在代码中引入函数接口。

  1. 有条件的延迟执行

输出日志的时候,先进行日志级别的判断

if (logger.isLoggable(Log.FINER)){ 
 logger.finer("Problem: " + generateDiagnostic()); 
} 

上面代码问题:

  • 日志器的状态(它支持哪些日志等级)通过 isLoggable 方法暴露给了客户端代码。
  • 为什么要在每次输出一条日志之前都去查询日志器对象的状态?这只能搞砸你的代码

更好的方案是使用 log 方法,该方法在输出日志消息之前,会在内部检查日志对象是否已经设置为恰当的日志等级:
logger.log(Level.FINER, "Problem: " + generateDiagnostic());
但是这样子还是需要去判断日志的等级。
java8引入了一个对 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 或者方法引用作为参数,新方法在检查完该对象的状态之后才调用原来的方法。你的代码会因此而变得更易读(结构更清晰),封装性更好(对象的状态也不会暴露给客户端代码了)。

  1. 环绕执行

第3章讲过,就是前后的代码都是相同的,但是中间的代码不同,使用这种模式,可以减少代码的冗余。

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("ModernJavaInAction/chap9/data.txt"))) { 
   return p.process(br);// 将 BufferedReaderProcessor作为执行参数传入
 } 
} 
public interface BufferedReaderProcessor {
// 使用 Lambda 表达式的函数接口,该接口能够抛出一个 IOException
 String process(BufferedReader b) throws IOException; 
} 

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

9.2.1 策略模式

之前就了解过,根据苹果的重量或者颜色来筛选。
策略模式包含三部分内容:

  • 一个代表某个算法的接口(Strategy 接口)
  • 一个或多个该接口的具体实现,它们代表了算法的多种实现(比如,实体类ConcreteStrategyA或者 ConcreteStrategyB)
  • 一个或多个使用策略对象的客户

image.png

普通情况下,就是定义一个接口,方法,然后就写几个实现类去实现。
使用lambda表达式就可以直接传递行为

9.2.2 模板方法

模板方法模式在你“希望使用这个算法,但是需要对其中的某些行进行改进,才能达到希望的效果”时是非常有用的。
不同分行的在线银行应用让客户满意的方式可能略有不同,比如给客户的账户发放红利,或者仅仅是少发送一些推广文件。

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

使用lambda表达式
这里我们向 processCustomer 方法引入了第二个参数,它是一个 Consumer类型的参数,与前文定义的 makeCustomerHappy 的特征保持一致:

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

9.2.3 观察者模式

某些事件发生时(比如状态转变),如果一个对象(通常称之为主题)需要自动地通知其他多个对象(称为观察者),就会采用该方案。
可以先
使用 Lambda 表达式
Observer 接口的所有实现类都提供了一个方法:notify。新闻到达时,它们都只是对同一段代码封装执行。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 more news from London... " + tweet); 
 } 
});

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

image.png

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 表达式
这个模式看起来像是在链接(也就是构造)函数。你可以将处理对象作为 Function<String, String>的一个实例,或者更确切地说作为UnaryOperator的一个实例。为了链接这些函数,你需要使用 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?!!"); 

9.2.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 表达式
Supplier 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); 
}

9.3 测试 Lambda 表达式

9.4 调试

因为 Lambda 表达式没有名字,涉及 Lambda 表达式的栈跟踪可能非常难理解。这是 Java 编译器未来版本可以改进的一个方面。
日志调试可以使用peek
image.png

List<Integer> result = 
 numbers.stream() 
// 输出来自数据源的当前元素值
 .peek(x -> System.out.println("from stream: " + x)) 
 .map(x -> x + 17) 
// 输出 map 操作的结果
 .peek(x -> System.out.println("after map: " + x)) 
 .filter(x -> x % 2 == 0) 
// 输出经过 filter 操作之后,剩下的元素个数
 .peek(x -> System.out.println("after filter: " + x)) 
 .limit(3) 
// 输出经过 limit 操作之后,剩下的元素个数
 .peek(x -> System.out.println("after limit: " + x)) 
 .collect(toList()); 

9.5 小结

  • Lambda 表达式能提升代码的可读性和灵活性。
  • 如果你的代码中使用了匿名类,那么尽量用 Lambda 表达式替换它们,但是要注意二者间语义的微妙差别,比如关键字 this,以及变量隐藏。
  • 跟 Lambda 表达式比起来,方法引用的可读性更好。
  • 尽量使用 Stream API 替换迭代式的集合处理。
  • Lambda 表达式有助于避免使用面向对象设计模式时容易出现的僵化的模板代码,典型的比如策略模式、模板方法、观察者模式、责任链模式,以及工厂模式。
  • 即使采用了 Lambda 表达式,也同样可以进行单元测试,但是通常你应该关注使用了Lambda 表达式的方法的行为。
  • 尽量将复杂的 Lambda 表达式抽象到普通方法中。
  • Lambda 表达式会让栈跟踪的分析变得更为复杂。
  • 流提供的 peek 方法在分析 Stream 流水线时,能将中间变量的值输出到日志中,是非常有用的工具。
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。