本章内容
如何
使用Lambda
表达式重构代码
Lambda
表达式对面向对象的设计模式
的影响
Lambda
表达式的测试
如何调试使用Lambda
表达式和Stream API
的代码
为了确保你的代码能被其他人理解,有几个步骤可以尝试,比如确保你的代码附有良好的文档,并严格遵守编程规范。
- 跟之前的版本相比较,Java 8的新特性也可以帮助提升代码的可读性:
- 使用Java 8,你可以减少冗长的代码,让代码更易于理解
- 通过
方法引用
和Stream API
,你的代码会变得更直观。这里我们会介绍三种
简单的重构,利用Lambda表达式
、方法引用
以及Stream
改善程序代码的。
- 可读性:
- 重构代码,用
Lambda
表达式取代匿名类
- 用
方法引用
重构Lambda
表达式- 用
Stream API
重构命令式的数据处理
注意:我们重构的目的是让代码更简洁直观,这也是我们要不要把匿名类
换成Lambda
的依据。(如果重构后,导致逻辑更绕,阅读成本增加就没必要了。)
Lambda
可用来简化匿名类
的废话代码。(比如:核心代码就一句话,为了这一句,以前非得让我实例化一个匿名类再重写方法,太多无用功。)
Runnable r1 = new Runnable(){
public void run(){
System.out.println("Hello 匿名类");
}
};
new Thread(r1).start();
Runnable r2 = () -> System.out.println("Hello Lambda");
new Thread(r2).start();
Lambda
是表达式
,匿名类
是类
。对此保持清醒的认识,关于作用域
、this
的问题就不言自明了。Lambda
的类型
根据上下文决定。如果出现重载
,可以显示转换
类型。例子:interface Task{
public void execute();
}
public static void doSomething(Runnable r){ r.run(); }
public static void doSomething(Task a){ a.execute(); }
// 这样 doSomething 就知道自己是谁了
doSomething((Task)() -> System.out.println("Danger danger!!"));
这是一段Lambda
实现的分组逻辑。将groupingBy
内的业务代码提取出来改为方法引用
可以让代码简洁
、意图清晰
。
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;
})
);
在Dish
类中添加一个方法getCaloricLevel
来实现groupingBy
中的逻辑。
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;
}
}
有了Dish::getCaloricLevel
原来的代码就可以简化为如下效果:(取菜单流》按热量级别分组》收集到Map。代码简洁意图清晰,简直像读小说)
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream()
.collect(groupingBy(Dish::getCaloricLevel));
JDK提供了大量现成工具:java.util.stream.Collectors 收集器、java.util.Comparator 比较器。《Java8实战》读书笔记04、《Java8实战》读书笔记05 有相关介绍。
命令式
(传统)迭代方式:
List<String> dishNames = new ArrayList<>();
for(Dish dish: menu){
if(dish.getCalories() > 300){
dishNames.add(dish.getName());
}
}
Stream API
隐式迭代:
menu.parallelStream()
.filter(d -> d.getCalories() > 300)
.map(Dish::getName)
.collect(toList());
哪个好阅读一目了然。但目前还有个问题就是不支持:break、continue、return
书上给了个连接说是辅助工具,但是无法访问 http://refactoring.info/tools/LambdaFicator/。
个人认为换个思路:break、continue
可以通过 filter、limit
等配合实现。return
我只想到能抛异常模拟。
本节介绍了具体的方法:
将参数类型声明为函数接口
,这样就能能给它传Lambda
了。然而何时需要这样做呢?引出了两个概念:条件的延迟执行
和环绕执行
。(本质就是TemplateMethod模板方法的思想)
客户代码原本是每次输出日志,先判断,再打印。
if (logger.isLoggable(Log.FINER)){
logger.finer("Problem: " + generateDiagnostic());
}
提取为一个log
函数后,每次调用,传入条件 level
、行为 msgSupplier
。条件满足,就执行。
public void log(Level level, Supplier<String> msgSupplier){
if(logger.isLoggable(level)){
log(level, msgSupplier.get());
}
}
logger.log(Level.FINER, () -> "Problem: " + generateDiagnostic());
processFile
可以接收一个Lambda
,并在创建BufferedReader
后用这个Lambda
来处理它。
public static String processFile(BufferedReaderProcessor p) throws IOException {
try(BufferedReader br = new BufferedReader(new FileReader("src\\main\\resources\\test.txt"))){
return p.process(br);
}
}
public interface BufferedReaderProcessor{
String process(BufferedReader b) throws IOException;
}
public static void main(String[] args) throws IOException {
String oneLine = processFile((BufferedReader b) -> b.readLine());
System.out.println("oneLine: " + oneLine);
String twoLines = processFile((BufferedReader b) -> b.readLine() + b.readLine());
System.out.println("twoLines: " + twoLines);
}
准备:
策略接口 ValidationStrategy
应用类 Validator
// 定义策略接口(只有一个方法,所以它也是一个函数式接口)
public interface ValidationStrategy {
boolean execute(String s);
}
// 构造时指定策略,validate 执行策略返回结果。
public class Validator{
private final ValidationStrategy strategy;
public Validator(ValidationStrategy v){
this.strategy = v;
}
public boolean validate(String s){
return strategy.execute(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 LambdaDemo {
public static void main(String[] args) {
Validator numericValidator = new Validator(new IsNumeric());
boolean b1 = numericValidator.validate("aaaa");
Validator lowerCaseValidator = new Validator(new IsAllLowerCase ());
boolean b2 = lowerCaseValidator.validate("bbbb");
}
}
Lambda
写法省了策略实现类
,直接 new Validator
策略传Lambda
就OK了。
public static void main(String[] args) {
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");
}
如果你需要采用某个算法的框架,同时又希望有一定的灵活度,能对它的某些部分进行改进,
那么采用模板方法设计模式是比较通用的方案。
@Data
@AllArgsConstructor
public class Customer {
private int id;
private String name;
}
public abstract class OnlineBanking {
public void processCustomer(int id){
Customer c = Database.getCustomerWithId(id); // 从数据库取
makeCustomerHappy(c);
}
abstract void makeCustomerHappy(Customer c);
}
public class BankA extends OnlineBanking {
@Override
void makeCustomerHappy(Customer c) {
System.out.println(String.format("ID: %d - %s 你好!", c.getId(), c.getName()));
}
}
客户端代码
public class LambdaDemo {
public static void main(String[] args) {
BankA bankA = new BankA();
bankA.processCustomer(666);
}
}
// ID: 666 - 笨笨 你好!
Lambda
写法改造processCustomer
加一个参数,类型为函数式接口Consumer<T>
public class OnlineBankingLambda {
public void processCustomer(int id, Consumer<Customer> makeCustomerHappy){
Customer c = Database.getCustomerWithId(id); // 从数据库取
makeCustomerHappy.accept(c);
}
}
使用时,第二个参数传Lambda
public class LambdaDemo {
public static void main(String[] args) {
OnlineBankingLambda onlineBankingLambda = new OnlineBankingLambda();
onlineBankingLambda.processCustomer(666, c -> {
System.out.println(String.format("ID: %d - %s 你好!", c.getId(), c.getName()));
});
}
}
观察者模式是一种比较常见的方案,某些事件发生时(比如状态转变),如果一个对象(通
常我们称之为主题)需要自动地通知其他多个对象(称为观察者),就会采用该方案。创建图形用户界面(GUI)程序时,你经常会使用该设计模式。这种情况下,你会在图形用户界面组件(比如按钮)上注册一系列的观察者。如果点击按钮,观察者就会收到通知,并随即执行某个特定的行为。 但是观察者模式并不局限于图形用户界面。比如,观察者设计模式也适用于股票交易的情形,多个券商可能都希望对某一支股票价格(主题)的变动做出响应。
书上写有的点多,我再简化一下。
// 观察者接口
interface Observer {
void notify(String 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));
}
}
实现 N 个具体的观察者。(书上还对消息内容做了判断,满足条件的消息,才处理。我这里简化了)
// 观察者 A
class ObserverA implements Observer{
public void notify(String tweet) {
System.out.println("观察者A 收到通知:" + tweet);
}
}
// 观察者 B
class ObserverB implements Observer{
public void notify(String tweet) {
System.out.println("观察者B 收到通知:" + tweet);
}
}
// 观察者 C
class ObserverC implements Observer{
public void notify(String tweet) {
System.out.println("观察者C 收到通知:" + tweet);
}
}
客户端代码
public static void main(String[] args) {
Feed f = new Feed();
f.registerObserver(new ObserverA());
f.registerObserver(new ObserverB());
f.registerObserver(new ObserverC());
f.notifyObservers("大家好,我是笨笨,笨笨的笨,笨笨的笨,谢谢!");
}
Lambda
写法无需实现几个类来用。直接传Lambda
。
public static void main(String[] args) {
Feed f = new Feed();
f.registerObserver(tweet -> System.out.println("观察者A 收到通知:" + tweet));
f.registerObserver(tweet -> System.out.println("观察者A 收到通知:" + tweet));
f.registerObserver(tweet -> System.out.println("观察者A 收到通知:" + tweet));
f.notifyObservers("大家好,我是笨笨,笨笨的笨,笨笨的笨,谢谢!");
}
责任链模式是一种创建处理对象序列(比如操作序列)的通用方案。一个处理对象可能需要
在完成一些工作之后,将结果传递给另一个对象,这个对象接着做一些工作,再转交给下一个处理对象,以此类推。
通常,这种模式是通过定义一个代表处理对象的抽象类来实现的,在抽象类中会定义一个字
段来记录后续对象。一旦对象完成它的工作,处理对象就会将它的工作转交给它的后继。
典型场景 :Java 中 Servlet Filter 过滤器
定义一个Filter 过滤器
接口,再实现两个Filter 过滤器
。
public abstract class MyFilter<T> {
protected MyFilter<T> nextFilter;
public void setNextFilter(MyFilter<T> nextFilter){
this.nextFilter = nextFilter;
}
public T handle(T input){
T r = handleWork(input);
if(nextFilter != null){
return nextFilter.handle(r);
}
return r;
}
abstract protected T handleWork(T input);
}
public class FilterA extends MyFilter<String> {
public String handleWork(String text){
return "【" + text + "】";
}
}
public class FilterB extends MyFilter<String> {
public String handleWork(String text){
return text.replaceAll("笨笨", "笑虾");
}
}
客户端
public class LambdaDemo {
public static void main(String[] args) {
MyFilter<String> filterA = new FilterA(); // 实例化 FilterA
MyFilter<String> filterB = new FilterB(); // 实例化 FilterB
filterA.setNextFilter(filterB); // 把过滤器串联起来
String result = filterA.handle("大家好,我是笨笨,笨笨的笨,笨笨的笨,谢谢!");
System.out.println(result); // 【大家好,我是笑虾,笑虾的笨,笑虾的笨,谢谢!】
}
}
Lambda
写法这里用到了函数式接口 UnaryOperator它有两个方法:
R apply(T t)
:用来执行;
Function<T,V> andThen(Function<? super R,? extends V> after)
:用来串连。
这里就完全借助函数式接口
来实现了:
public class LambdaDemo {
public static void main(String[] args) {
UnaryOperator<String> filterA = (String text) -> "【" + text + "】";
UnaryOperator<String> filterB = (String text) -> text.replaceAll("笨笨", "笑虾");
Function<String, String> pipeline = filterA.andThen(filterB);
String result = pipeline.apply("大家好,我是笨笨,笨笨的笨,笨笨的笨,谢谢!");
System.out.println(result); // 【大家好,我是笑虾,笑虾的笨,笑虾的笨,谢谢!】
}
}
使用工厂模式,你无需向客户暴露实例化的逻辑就能完成对象的创建。比如,我们假定你为一家银行工作,他们需要一种方式创建不同的金融产品:贷款、期权、股票,等等。
通常,你会创建一个工厂类,它包含一个负责实现不同对象的方法,如下所示:
准备:
产品接口 Product
ProductA, ProductB, ProductC
ProductFactory 工厂类
interface Product{}
class ProductA implements Product{}
class ProductB implements Product{}
class ProductC implements Product{}
class ProductFactory {
public static Product createProduct(String name){
switch(name){
case "A": return new ProductA();
case "B": return new ProductB();
case "C": return new ProductC();
default: throw new RuntimeException("没有产品:" + name);
}
}
}
客户端
public class LambdaDemo {
public static void main(String[] args) {
Product a = ProductFactory.createProduct("A");
System.out.println(a);
}
}
Lambda
写法我暂时没领悟这个有啥优势。
public class ProductFactory {
final static Map<String, Supplier<Product>> map = new HashMap<>();
static {
map.put("A", ProductA::new);
map.put("B", ProductB::new);
map.put("C", ProductC::new);
}
public static Product createProduct(String name){
Supplier<Product> p = map.get(name);
if(p != null) return p.get();
throw new IllegalArgumentException("没有产品:" + name);
}
}
客户端(完全一样)
public class LambdaDemo {
public static void main(String[] args) {
Product a = ProductFactory.createProduct("A");
System.out.println(a);
}
}
我们可以借助某个字段访问Lambda函数
,对它进行测试。但Lambda
声明为public
可不太好。
public class Point{
public final static Comparator<Point> compareByXAndThenY =
comparing(Point::getX).thenComparing(Point::getY);
// …
}
@Test
public void testComparingTwoPoints() throws Exception {
Point p1 = new Point(10, 15);
Point p2 = new Point(10, 20);
int result = Point.compareByXAndThenY.compare(p1 , p2);
assertEquals(-1, result);
}
我们不必针对p -> new Point(p.getX() + x, p.getY())
进行测试,只要测试包含Lambda
的方法moveAllPointsRightBy
就可以了。
public static List<Point> moveAllPointsRightBy(List<Point> points, int x){
return points.stream()
.map(p -> new Point(p.getX() + x, p.getY()))
.collect(toList());
}
@Test
public void testMoveAllPointsRightBy() throws Exception{
List<Point> points = Arrays.asList(new Point(5, 5), new Point(10, 5));
List<Point> expectedPoints = Arrays.asList(new Point(15, 5), new Point(20, 5));
List<Point> newPoints = Point.moveAllPointsRightBy(points, 10);
assertEquals(expectedPoints, newPoints);
}
注意,上面的单元测试中,Point类恰当地实现equals方法非常重要,否则该测试的结果就取决于Object类的默认实现。
可能你会碰到非常复杂的Lambda表达式,包含大量的业务逻辑,比如需要处理复杂情况的定价算法。你无法在测试程序中引用Lambda表达式,这种情况该如何处理呢?一种策略是将
Lambda
表达式转
换为方法引用
(这时你往往需要声明一个新的常规方法),我们在8.1.3节详细讨论过这种情况。这之后,你可以用常规的方式对新的方法进行测试。
接受函数作为参数的方法或者返回一个函数的方法(所谓的“高阶函数”,higher-order function,我们在第14章会深入展开介绍)更难测试。如果一个方法接受Lambda表达式作为参数,你可以采用的一个方案是使用不同的Lambda表达式对它进行测试。比如,你可以使用不同的谓词对第2章中创建的filter方法进行测试。
public List<Integer> filter(List<Integer> list, Function<Integer, Boolean> fun){
return list.stream().filter(fun::apply).collect(Collectors.toList());
}
@Test
public void testFilter() throws Exception{
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
List<Integer> even = filter(numbers, i -> i % 2 == 0);
List<Integer> smallerThanThree = filter(numbers, i -> i < 3);
assertEquals(Arrays.asList(2, 4), even);
assertEquals(Arrays.asList(1, 2), smallerThanThree);
}
如果被测试方法的返回值是另一个方法,该如何处理呢?你可以仿照我们之前处理Comparator的方法,把它当成一个函数接口,对它的功能进行测试。
然而,事情可能不会一帆风顺,你的测试可能会返回错误,报告说你使用Lambda表达式的方式不对。因此,我们现在进入调试的环节。
调试有问题的代码时,程序员的兵器库里有两大老式武器,分别是:
查看栈跟踪
输出日志
Lambda
表达式是匿名的它没有名字
,它的栈跟踪大概长这样:Debugging.lambda$main$0
这名字是编译器自动生成的,正常项目中Lambda
一多了 $xxx
这种名字完全看不懂。
解决方案:使用 方法引用 就能看到正常的方法名了。
peek 的设计初衷就是在流的每个元素恢复运行之前,插入执行一个动作。但是它不像forEach那样恢复整个流的运行,而是在一个元素上完成操作之后,它只会将操作顺承到流水线中的下一个操作。
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);
System.out.println("=============================================================");
List<Integer> result = numbers.stream()
.peek(x -> System.out.println("from stream: " + x))
.map(x -> x + 17)
.peek(x -> System.out.print("after map: " + x))
.filter(x -> x % 2 == 0)
.peek(x -> System.out.print("after filter: " + x))
.limit(3)
.peek(x -> System.out.print("after limit: " + x))
.collect(toList());
输出效果
20
22
=====================================================================
from stream: 2
after map: 19from stream: 3
after map: 20after filter: 20after limit: 20from stream: 4
after map: 21from stream: 5
after map: 22after filter: 22after limit: 22
下面回顾一下这一章的主要内容。
Lambda
表达式能提升代码的可读性
和灵活性
。匿名类
,尽量用Lambda
表达式替换它们,但是要注意二者间语义的微妙差别
,比如关键字this,以及变量隐藏。Lambda
表达式比起来,方法引用
的可读性更好 。Stream API
替换迭代式的集合处理
。Lambda
表达式有助于避免使用面向对象设计模式时容易出现的僵化的模板代码,典型的比如策略模式、模板方法、观察者模式、责任链模式,以及工厂模式。Lambda
表达式,也同样可以进行单元测试
,但是通常你应该关注使用了Lambda表达式的方法的行为
。复杂
的Lambda
表达式抽象到普通方法
中。Lambda
表达式会让栈跟踪
的分析变得更为复杂
。(可用方法引用改善)peek
方法在分析Stream
流水线时,能将中间
变量的值输出
到日志中,是非常有用的工具。(直接用 Idea 的流调试工具更直观)笑虾:《Java8实战》读书笔记04:Stream流操作 - 基本用法
笑虾:《Java8实战》读书笔记05:Stream流操作 - collect 收集数据
java.util.stream.Stream
java.util.stream.Collector 接口定义了实现归约操作(即收集器)的规范
java.util.stream.Collectors 工具类提供了大量常用的收集器
java.util.Comparator 比较器