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

news2025/1/12 1:50:23

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 流水线时,能将中间变量的值输出到日志中,是非常有用的工具。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/436300.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Java|基础篇】方法的定义使用、重载以及递归

文章目录 1.什么是方法2.方法的定义和使用返回值类型形参与实参方法执行过程 3.⭐方法的重载4.递归5. 总结 1.什么是方法 方法就是组织好的,可重复使用的具有某种功能的特定代码块 (类似于函数) 在我们平时写代码时,如果遇到会经常使用的一些功能相同的代码时,我们就可以把这段…

在线考试系统学员答题批改日志,实战练习

一、环境要求 sandbox-hdp 2.6.4 或同等版本自建的HadoopHiveSparkHBase 开发环境。 二、数据描述 这是一份来自于某在线考试系统的学员答题批改日志&#xff0c;日志中记录了日志生成时间,题目难度系数&#xff0c;题目所属的知识点 ID&#xff0c;做题的学生 ID&#xff0…

Oracle的学习心得和知识总结(二十一)|Oracle数据库可插拔数据库PDB的创建及删除

目录结构 注&#xff1a;提前言明 本文借鉴了以下博主、书籍或网站的内容&#xff0c;其列表如下&#xff1a; 1、参考书籍&#xff1a;《Oracle Database SQL Language Reference》 2、参考书籍&#xff1a;《PostgreSQL中文手册》 3、EDB Postgres Advanced Server User Gui…

华为OD机试(Java),5键键盘的输出

一、题目描述 有一个特殊的5键键盘&#xff0c;上面有a&#xff0c;ctrl-c&#xff0c;ctrl-x&#xff0c;ctrl-v&#xff0c;ctrl-a五个键。 a键在屏幕上输出一个字母a&#xff1b;ctrl-c将当前选择的字母复制到剪贴板&#xff1b;ctrl-x将当前选择的字母复制到剪贴板&#…

极氪要上市,吉利“基因改造”成功

文|智能相对论 作者| 落笔 当越来越多的国产新锐品牌入局新能源汽车市场&#xff0c;晚半拍的极氪却用较短的时间实现了主观逆袭。据统计&#xff0c;极氪001自问世以来便接连斩获消费者欢心&#xff0c;2022年极氪001更是实现了全年交付71941台的战绩&#xff0c;且持续蝉联…

ASP音乐网站的设计与实现

本文阐述了音乐网站的设计与实现&#xff0c;本系统采用ASPSQL Sever 2000作为开发工具。前台主要实现歌曲的分类显示及分类查询、歌曲的在线试听及下载、会员注册、登录以及最新音乐的新闻介绍&#xff0c;并且还为用户提供了在线交流的平台等功能&#xff1b;后台主要用于数据…

BGP路由优选实验

一&#xff0c;实验要求及其拓扑图 二&#xff1a;划分好IP的拓扑 三&#xff1a; 实验分析 1、使用 Preval 策略&#xff0c;确保R4通过R2到达192.168.10.0/24 1、抓取流量 [r4]ip ip-prefix PV permit 192.168.10.0 24 2、配置策略 [r4]route-policy PV permit node 10 [r4…

Web UI自动化测试框架

WebUI automation testing framework based on Selenium and unittest. 基于 selenium 和 unittest 的 Web UI自动化测试框架。 特点 提供更加简单API编写自动化测试。提供脚手架&#xff0c;快速生成自动化测试项目。自动生成HTML测试报告生成。自带断言方法&#xff0c;断言…

Linux常用环境配置

一、sqlite3 1、官网地址 SQLite Download Page 2、在Linux下载 右键复制源代码链接在linux用wget下载 wget https://www.sqlite.org/2023/sqlite-autoconf-3410200.tar.gz 3、解压 tar -xzvf sqlite-autoconf-3410200.tar.gz 4、编译 # 进入解压目录 cd sqlite-autoconf-34102…

App 版本更新插件介绍及使用指南

随着移动互联网的发展&#xff0c;APP 已经成为人们生活中必不可少的一部分&#xff0c;而版本更新也是 APP 发展过程中必不可少的环节。为了更好地实现 APP 版本更新和管理&#xff0c;我们推荐一个非常实用的插件&#xff1a;App 版本更新插件。 该插件支持强制更新、静默更…

Hystrix传递ThreadLocal范围对象的问题(最为细致的分析)

场景 在springcloud微服务体系下&#xff0c;从网关层开始要在request请求头放置一些重要参数&#xff0c;比如traceId&#xff0c;并要求在fegin之间的调用时&#xff0c;也能够一直传递下去&#xff0c;由于实际项目使用中&#xff0c;都是fegin集成了hystrix一起配合使用的…

cloud-canal的部署使用

一&#xff0c;官网参考&#xff1a; https://www.clougence.com/ https://www.clougence.com/cc-doc/quick/quick_start 二&#xff0c;点击下载私有部署版 返回数据&#xff1a; 版本号: 2.5.0.7 MD5值: 18e2502xxxxxxx 下载地址: https://tgzdownload.clougence.com/lates…

华为OD机试(Java),分班

一、题目描述 幼儿园两个班的小朋友在排队时混在了一起&#xff0c;每位小朋友都知道自己是否与前面一位小朋友是否同班&#xff0c;请你帮忙把同班的小朋友找出来。 小朋友的编号为整数&#xff0c;与前一位小朋友同班用Y表示&#xff0c;不同班用N表示。 二、输入描述 输…

PYQT5学习笔记01——PYQT5初体验以及PYQT5程序基本结构分析

一、PYQT5初体验 我们首先用代码编写一个窗口&#xff0c;窗口里面有一个标签控件&#xff0c;标签内的文本是 Hello World&#xff0c;代码如下&#xff1a; # -*- coding: UTF-8 -*- # 导入需要的包 from PyQt5.Qt import * import sys# 创建应用程序对象 app QApplicatio…

【c++初阶】:

c入门 一.概念二.使用三.应用四.常引用五.引用与指针 一.概念 c语言中我们常用指针找地址&#xff0c;但在c中&#xff0c;忽略了指针&#xff08;当然也可以使用指针&#xff09;。常用引用这个概念。 二.使用 可以看到这里的b和c本质上都是a&#xff0c;只是不同的称呼罢了。…

手把手教你将项目部署到服务器!

一、导入centos7虚拟机&#xff1a; 打开VMWare&#xff0c;点击“打开虚拟机”&#xff0c;选择centos7.ova之后&#xff0c;选择存储路径&#xff1a; 点击导入&#xff1a; 选择“不再显示此消息”&#xff0c;点击“重试”按钮&#xff1a; 点击“编辑虚拟机设置”&#x…

【数据结构】二叉树OJ题

&#x1f63d;PREFACE &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐ 评论&#x1f4dd; &#x1f4e2;系列专栏&#xff1a;数据结构 &#x1f50a;本专栏主要更新的是数据结构部分知识点 &#x1f4aa;种一棵树最好是十年前其次是现在 目录 1.单值二叉树 2.相同的树 …

Hadoop之Hive

文章目录 一、Hive简介1.1 Hive 基本概念1.2 Hive架构图1.3 Hive数据模型 二、Hive安装配置2.1 内嵌模式2.2 配置元数据到mysql2.3本地模式2.4远程模式2.5 Hive JDBC Hiverserver22.5.1远程模式下使用Beeline CLI2.5.2 DataGrip图形化客户端 2.6 Hive常见属性配置 一、Hive简介…

Vue基础入门(上)

<script src"https://unpkg.com/vuenext"></script> 从面向dom编程到面向数据编程 输入显示列表 const appVue.createApp({data(){return{inputValue:,list:[]}},methods:{handleAddItem(){this.list.push(this.inputValue);this.inputValue;}},templ…

(一) nvidia jetson orin nvcsi tegra-capture-vi camera相关内容梳理 之 vi相关代码分析

背景:对于nvidia 的jetson orin 的camera,其内部是如何实现的尼?硬件方面的pipeline是怎么关联的,其内部有哪些camera相关的modules?对于这些modules,软件上又是怎么去实现?设备树如何去抽象这些modules?分析完后,给我们一个camera sensor,如何进行bring up?本文将会…