《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 增加代码的灵活性
- 采用函数接口
没有函数接口,就无法使用 Lambda 表达式。因此,你需要在代码中引入函数接口。
- 有条件的延迟执行
输出日志的时候,先进行日志级别的判断
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 或者方法引用作为参数,新方法在检查完该对象的状态之后才调用原来的方法。你的代码会因此而变得更易读(结构更清晰),封装性更好(对象的状态也不会暴露给客户端代码了)。
- 环绕执行
第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 表达式的函数接口,该接口能够抛出一个 IOExceptionString process(BufferedReader b) throws IOException;
}
9.2 使用 Lambda 重构面向对象的设计模式
9.2.1 策略模式
之前就了解过,根据苹果的重量或者颜色来筛选。
策略模式包含三部分内容:
- 一个代表某个算法的接口(Strategy 接口)
- 一个或多个该接口的具体实现,它们代表了算法的多种实现(比如,实体类ConcreteStrategyA或者 ConcreteStrategyB)
- 一个或多个使用策略对象的客户
普通情况下,就是定义一个接口,方法,然后就写几个实现类去实现。
使用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);
}
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
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 流水线时,能将中间变量的值输出到日志中,是非常有用的工具。
《Java8实战》第9章 重构、测试和调试相关推荐
- 《Java8实战》笔记(08):重构、测试和调试
重构.测试和调试 为改善可读性和灵活性重构代码 利用Lambda表达式,你可以写出更简洁.更灵活的代码.用"更简洁"来描述Lambda表达式是因为相较于匿名类,Lambda表达式可 ...
- 《Java8实战》-第六章读书笔记(用流收集数据-01)
用流收集数据 我们在前一章中学到,流可以用类似于数据库的操作帮助你处理集合.你可以把Java 8的流看作花哨又懒惰的数据集迭代器.它们支持两种类型的操作:中间操作(如 filter 或 map )和终 ...
- 《Java8实战》读书笔记10:组合式异步编程 CompletableFuture
<Java8实战>读书笔记10:组合式异步编程 CompletableFuture 第11章 CompletableFuture:组合式异步编程 11.1 Future 接口 (只是个引子 ...
- [201604]Java8实战(陆明刚 劳佳 译)
==[201604]Java8实战(陆明刚 劳佳 译)== 第一部分 基础知识 第 1 章 为什么要关心 Java 8 1.1 Java 怎么还在变 1.1.1 Java 在编程语言生态系统中的位置 ...
- 《Java8实战》笔记汇总
<Java8实战>笔记(01):为什么要关心Java8 <Java8实战>笔记(02):通过行为参数传递代码 <Java8实战>笔记(03):Lambda表达式 & ...
- 《Java8实战》读书笔记06:Parallel Stream 并行流
<Java8实战>读书笔记06:Parallel Stream 并行流 第7章 并行数据处理与性能 7.1 并行流 7.1.1 将顺序流转换为并行流 7.1.2 测量流性能 7.1.3 正 ...
- Java8实战学习笔记(四)——高效 Java 8 编程(一)
一.重构.测试和调试 (一).为改善可读性和灵活性重构代码 用更紧凑的方式描述程序的行为 -- Lambda表达式 将一个既有的方法作为参数传递给另一个方法 -- 方法引用 如何运用前几章介绍的Lam ...
- [原创].NET 业务框架开发实战之六 DAL的重构
.NET 业务框架开发实战之六 DAL的重构 前言:其实这个系列还是之前的".NET 分布式架构开发实战 ",之所以改了名字,主要是因为文章的标题带来了不少的歧义:系列文章中本打算 ...
- [菜鸟SpringCloud实战入门]第九章:服务网关Zuul体验
前言 欢迎来到菜鸟SpringCloud实战入门系列(SpringCloudForNoob),该系列通过层层递进的实战视角,来一步步学习和理解SpringCloud. 本系列适合有一定Java以及Sp ...
- Java8实战 阅读二周目感想
Java8实战是我目前看过的写的水平最高的一本书,由浅入深,深入浅出,九浅一深. 之前大略的过了一遍,但是对于前几章的内容一直有点雾里看花的感觉. 又读了一遍,感觉有点新的感想. 一.其中1.2.1中 ...
最新文章
- 自己动手写简单的web应用服务器(4)—利用socket实现文件的下载
- 【POJ3126 Prime Path】【POJ 3087 Shuffle'm Up】【UVA 11624 Fire!】【POJ 3984 迷宫问题】
- oracle 日期格式转换 ‘ddMONyyyy’ 'ddMMMyyyy'
- 我为什么要这么功利?
- php在菜单栏里加子菜单,WordPress后台添加子菜单add_submenu_page()
- Qt学习之路(11): MainWindow
- 正则表达式替换和不包含指定字符串
- HANA学习笔记1-搭建HANA学习环境
- 【POJ1276】【多重背包】凑货币问题
- oracle数据库英语,Oracle的数据库管理功能的学习英语
- python验证软件签名
- 南京工业大学计算机科学与技术学院保研外校,南京工业大学计算机科学与技术学院2018年招收推荐免试研究生章程...
- 企业网中nextcloud与iRedmail邮件系统的配合
- linux环境使用c语言获取当前目录下有哪些文件,并打印它们的名字
- 华为服务器用户名密码忘记,电脑坏了,腾讯云华为云忘记宝塔面板登陆名和密码怎么办?...
- 爱因斯坦:三篇著名演讲
- 我的ElasticSearch认证工程师之路
- 计算机收藏夹位于哪个磁盘,电脑浏览器收藏夹保存在哪里
- socket中结构与函数
- Cmd批处理替换文件
热门文章
- 使用sourceTree添加git远端
- 在MATLAB中创建函数
- C#中什么是类,类和对象的关系,类的访问修饰符?
- iOS 监听耳机状态
- android 抓包 修改数据,微信跳一跳怎么用抓包修改分数_改数据
- WPS如何快速输入随机姓名
- 容器编排-Docker Compose
- 补码一位乘--布斯公式
- 基于SPI协议下的OLED显示
- python version-32 required_python version 3.6 required,which was not fount in the regis-站长资讯中心...