【修炼内功】[Java8] Lambda表达式带来的编程新思路

25次阅读

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

该文章已收录【修炼内功】跃迁之路
Lambda 表达式,可以理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型。
这里,默认您已对 Java8 的 Lambda 表达式有一定了解,并且知道如何使用。

Java8 中引入的 Lambda 表达式,为编程体验及效率带来了极大的提升。
行为参数化
行为参数化,是理解函数式编程的一个重要概念。简单来说便是,一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。更为通俗的讲,行为参数化是指,定义一段代码,这段代码并不会立即执行,而是可以像普通变量 / 参数一样进行传递,被程序的其他部分调用。
我们通过一个特别通用的筛选苹果的例子,来逐步了解如何使用 Lambda 表达式实现行为参数化。(如果对行为参数化已十分了解,可直接跳过本节)

需求 1:筛选绿色苹果
我们需要将仓库中绿色的苹果过滤出来,对于这样的问题,大多数人来说都是手到擒来 (step1: 面向过程)
public static List<Apple> filterGreenApples(List<Apple> apples) {
List<apple> filteredApples = new LinkedList<>();
for (Apple apple: apples) {
if (“green”.equals(apple.getColor())) {
filteredApples.add(apple);
}
}
return filteredApples;
}

List<Apple> greenApples = filterGreenApples(inventory);

需求 2:筛选任意颜色苹果
对于这样的需求变更,可能也不是很难
public static List<Apple> filterApplesByColor(List<Apple> apples, String color) {
List<apple> filteredApples = new LinkedList<>();
for (Apple apple: apples) {
if (color.equals(apple.getColor())) {
filteredApples.add(apple);
}
}
return filteredApples;
}

List<Apple> someColorApples = filterApplesByColor(inventory, “red”);

需求 3:筛选重量大于 150 克的苹果
有了先前的教训,可能会学聪明一些,不会把重量直接写死到程序里,而是提供一个入参
public static List<Apple> filterApplesByWeight(List<Apple> apples, int minWeight) {
List<apple> filteredApples = new LinkedList<>();
for (Apple apple: apples) {
if (apple.getWeight() > minWeight) {
filteredApples.add(apple);
}
}
return filteredApples;
}

List<Apple> heavyApples = filterApplesByColor(inventory, 150);

需求 4:筛选颜色为红色且重量大于 150 克的苹果
如果照此下去,程序将变得异常难于维护,每一次小的需求变更,都需要进行大范围的改动。为了避免永无休止的加班,对于了解设计模式的同学,可能会将筛选逻辑抽象出来 (step2: 面向对象)
public interface Predicate<Apple> {
boolean test(Apple apple);
}
预先定义多种筛选策略,将策略动态的传递给筛选函数
public static List<Apple> filterApples(List<Apple> apples, Predicate predicate) {
List<apple> filteredApples = new LinkedList<>();
for (Apple apple: apples) {
if (predicate.test(apple)) {
filteredApples.add(apple);
}
}
return filteredApples;
}

Predicate predicate = new Predicate() {
@override
public boolean test(Apple apple) {
return “red”.equals(apple.getColor()) && apple.getWeight > 150;
}
};

List<Apple> satisfactoryApples = filterApples(inventory, predicate);
或者直接使用匿名类,将筛选逻辑传递给筛选函数
List<Apple> satisfactoryApples = filterApples(inventory, new Predicate() {
@override
public boolean test(Apple apple) {
return “red”.equals(apple.getColor()) && apple.getWeight > 150;
}
});
至此,已经可以满足大部分的需求,但对于这种十分啰嗦、被 Java 程序员诟病了多年的语法,在 Lambda 表达式出现后,便出现了一丝转机 (step3: 面向函数)
@FunctionalInterface
public interface Predicate<Apple> {
boolean test(Apple apple);
}

public List<Apple> filterApples(List<Apple> apples, Predicate<Apple> predicate) {
return apples.stream.filter(predicate::test).collect(Collectors.toList());
}

List<Apple> satisfactoryApples = filterApples(inventory, apple -> “red”.equals(apple.getColor()) && apple.getWeight > 150);
以上示例中使用了 Java8 的 stream 及 lambda 表达式,关于 stream 及 lambda 表达式的具体使用方法,这里不再赘述,重点在于解释什么是行为参数化,示例中直接将筛选逻辑 (红色且重量大于 150 克) 的代码片段作为参数传递给了函数(确切的说是将 lambda 表达式作为参数传递给了函数),而这段代码片段会交由筛选函数进行执行。
Lambda 表达式与匿名类很像,但本质不同,关于 Lambda 表达式及匿名类的区别,会在之后的文章详细介绍
如果想让代码更为简洁明了,可以继续将筛选逻辑提取为函数,使用方法引用进行参数传递
private boolean isRedColorAndWeightGt150(Apple apple) {
return “red”.equals(apple.getColor()) && apple.getWeight > 150;
}

List<Apple> satisfactoryApples = filterApples(inventory, this::isRedColorAndWeightGt150);
至此,我们完成了从面向过程到面向对象再到面向函数的编程思维转变,代码也更加具有语义化,不论是代码阅读还是维护,都较之前有了很大的提升
等等,如果需要过滤颜色为黄色并且重量在 180 克以上的苹果,是不是还要定义一个 isYellowColorAndWeightGt180 的函数出来,貌似又陷入了无穷加班的怪圈~
还有没有优化空间?能否将筛选条件抽离到单一属性,如 byColor、byMinWeight 等,之后再做与或计算传递给筛选函数?
接下来就是我们要介绍的高阶函数
高阶函数

高阶函数是一个函数,它接收函数作为参数或将函数作为输出返回

接受至少一个函数作为参数
返回的结果是一个函数

以上的定义听起来可能有些绕口。结合上节示例,我们的诉求是将苹果的颜色、重量或者其他筛选条件也抽离出来,而不是硬编码到代码中
private Predicate<apple> byColor(String color) {
return (apple) -> color.equals(apple.getColor);
}

private Predicate<Apple> byMinWeight(int minWeight) {
return (apple) -> apple.getWeight > minWeight;
}
以上两个函数的返回值,均为 Predicate 类型的 Lambda 表达式,或者可以说,以上两个函数的返回值也是函数
接下来我们定义与运算,只有传入的所有条件均满足才算最终满足
private Predicate<Apple> allMatches(Predicate<Apple> …predicates) {
return (apple) -> predicates.stream.allMatch(predicate -> predicate.test(apple));
}
以上函数,是将多个筛选逻辑做与计算,注意,该函数接收多个函数 (Lambda) 作为入参,并返回一个函数(Lambda),这便是高阶函数
如何使用该函数?作为苹果筛选示例的延伸,我们可以将上一节最后一个示例代码优化如下
List<Apple> satisfactoryApples = filterApples(inventory, allMatches(byColor(“red”), byMinWeight(150)));
至此,还可以抽象出 anyMatches、nonMatches 等高阶函数,组合使用
// 筛选出 颜色为红色 并且 重量在 150 克以上 并且 采摘时间在 1 周以内 并且 产地在中国、美国、加拿大任意之一的苹果
List<Apple> satisfactoryApples = filterApples(
inventory,
allMatches(
byColor(“red”),
byMinWeight(150),
apple -> apple.pluckingTime – currentTimeMillis() < 7L * 24 * 3600 * 1000,
anyMatches(byGardens(“ 中国 ”), byGardens(“ 美国 ”), byGardens(“ 加拿大 ”)
)
);
如果使用 jvm 包中的 java.util.function.Predicate,我们还可以继续优化,使代码更为语义化
// 筛选出 颜色为红色 并且 重量在 150 克以上 并且 采摘时间在 1 周以内 并且 产地在中国、美国、加拿大任意之一的苹果
List<Apple> satisfactoryApples = filterApples(
inventory,
byColor(“red”)
.and(byMinWeight(150))
.and(apple -> apple.pluckingTime – currentTimeMillis() < 7L * 24 * 3600 * 1000)
.and(byGardens(“ 中国 ”).or(byGardens(“ 美国 ”).or(byGardens(“ 加拿大 ”)))
);
这里使用了 Java8 中的默认函数,默认函数允许你在接口 interface 中定义函数的默认行为,从某方面来讲也可以实现类的多继承
示例中,and/or 函数接收一个 Predicate 函数 (Lambda 表达式) 作为参数,并返回一个 Predicate 函数(Lambda 表达式),同样为高阶函数
关于默认函数的使用,会在之后的文章详细介绍
闭包
闭包(Closure),能够读取其他函数内部变量的函数
又是一个比较抽象的概念,其实在使用 Lambda 表达式的过程中,经常会使用到闭包,比如
public Future<List<Apple>> filterApplesAsync() {
List<Apple> inventory = getInventory();

return CompletableFuture.supplyAsync(() -> filterApples(inventory, byColor(“red”)));
}
在提交异步任务时,传入了内部函数(Lambda 表达式),在内部函数中使用了父函数 filterApplesAsync 中的局部变量 inventory,这便是闭包。
如果该示例不够明显的话,可以参考如下示例
private Supplier<Integer> initIntIncreaser(int i) {
AtomicInteger atomicInteger = new AtomicInteger(i);
return () -> atomicInteger.getAndIncrement();
}

Supplier<Integer> increaser = initIntIncreaser(1);
System.out.println(increaser.get());
System.out.println(increaser.get());
System.out.println(increaser.get());
System.out.println(increaser.get());

//[out]: 1
//[out]: 2
//[out]: 3
//[out]: 4
initIntIncreaser 函数返回另一个函数 (内部函数),该函数(increaser) 使用了父函数 initIntIncreaser 的局部变量 atomicInteger,该变量会被函数 increaser 持有,并且会在调用 increaser 时使用(更改)
柯里化
柯里化(Currying),是把接受多个参数的函数变换成接受一个单一参数的函数。柯里化是逐步传值,逐步缩小函数的适用范围,逐步求解的过程。

如,设计一个函数,实现在延迟一定时间之后执行给定逻辑,并可以指定执行的执行器
public ScheduledFuture executeDelay(Runnable runnable, ScheduledExecutorService scheduler, long delay, TimeUnit timeunit) {
return scheduler.schedule(runnable, delay, timeunit);
}
目前有一批任务,需要使用执行器 scheduler1,并且均延迟 5 分钟执行
另一批任务,需要使用执行器 scheduler2,并且均延迟 15 分钟执行
可以这样实现
executeDelay(runnable11, scheduler1, 5, TimeUnit.SECONDS);
executeDelay(runnable12, scheduler1, 5, TimeUnit.SECONDS);
executeDelay(runnable13, scheduler1, 5, TimeUnit.SECONDS);

executeDelay(runnable21, scheduler2, 15, TimeUnit.SECONDS);
executeDelay(runnable22, scheduler2, 15, TimeUnit.SECONDS);
executeDelay(runnable23, scheduler2, 15, TimeUnit.SECONDS);
其实,我们发现这里是有规律可循的,比如,使用某个执行器在多久之后执行什么,我们可以将 executeDelay 函数进行第一次柯里化
public Function3<ScheduledFuture, Runnable, Integer, TimeUnit> executeDelayBySomeScheduler(ScheduledExecutorService scheduler) {
return (runnable, delay, timeunit) -> executeDelay(runable, scheduler, delay, timeunit);
}

Function3<ScheduledFuture, Runnable, Integer, TimeUnit> executeWithScheduler1 = executeDelayBySomeScheduler(scheduler1);

Function3<ScheduledFuture, Runnable, Integer, TimeUnit> executeWithScheduler2 = executeDelayBySomeScheduler(scheduler2);

executeWithScheduler1.apply(runnable11, 5, TimeUnit.SECONDS);
executeWithScheduler1.apply(runnable12, 5, TimeUnit.SECONDS);
executeWithScheduler1.apply(runnable13, 5, TimeUnit.SECONDS);

executeWithScheduler2.apply(runnable21, 15, TimeUnit.SECONDS);
executeWithScheduler2.apply(runnable22, 15, TimeUnit.SECONDS);
executeWithScheduler2.apply(runnable23, 15, TimeUnit.SECONDS);
函数 executeDelay 接收 4 个参数,函数 executeWithScheduler1/executeWithScheduler2 接收 3 个参数,我们通过 executeDelayBySomeScheduler 将具体的执行器封装在了 executeWithScheduler1/executeWithScheduler2 中
进一步,我们可以做第二次柯里化,将延迟时间也封装起来
public Function<ScheduledFuture, Runnable> executeDelayBySomeSchedulerOnDelay(ScheduledExecutorService scheduler, long delay, TimeUnit timeunit) {
return (runnable) -> executeDelay(runable, scheduler, delay, timeunit);
}

Function<ScheduledFuture, Runnable> executeWithScheduler1After5M = executeDelayBySomeSchedulerOnDelay(scheduler1, 5, TimeUnit.SECONDS);

Function<ScheduledFuture, Runnable> executeWithScheduler2After15M = executeDelayBySomeSchedulerOnDelay(scheduler2, 15, TimeUnit.SECONDS);

Stream.of(runnable11, runnable12,runnable13).forEach(this::executeWithScheduler1After5M);
Stream.of(runnable21, runnable22,runnable23).forEach(this::executeWithScheduler2After15M);
将具体的执行器及延迟时间封装在 executeWithScheduler1After5M/executeWithScheduler2After15M 中,调用的时候,只需要关心具体的执行逻辑即可
环绕执行(提取共性)
有时候我们会发现,很多代码块十分相似,但又有些许不同
比如,目前有两个接口可以查询汇率,queryExchangeRateA 及 queryExchangeRateB,我们需要在开关 exchangeRateSwitch 打开的时候使用 queryExchangeRateA 查询,否则使用 queryExchangeRateB 查询,同时在一个接口异常失败的时候,自动降低到另一个接口进行查询
同样,目前有两个接口可以查询关税,queryTariffsA 及 queryTariffsB,同样地,我们需要在开关 tariffsSwitch 打开的时候使用 queryTariffsA 查询,否则使用 queryTariffsB 查询,同时在一个接口异常失败的时候,自动降低到另一个接口进行查询
其实,以上两种场景,除了开关及具体的接口逻辑外,整体流程是一致的

再分析,其实接口调用的降级逻辑也是一样的
这里不再列举如何使用抽象类的方法如解决该类问题,我们直接使用 Java8 的 Lambda 表达式
首先,可以将降级逻辑提取为一个函数
@FunctionalInterface
interface ThrowingSupplier<T> {
T get() throw Exception;
}

/**
* 1. 执行 A
* 2. 如果 A 执行异常,则执行 B
*/
public <T> ThrowingSupplier<T> executeIfThrowing(ThrowingSupplier<T> supplierA, ThrowingSupplier<T> supplierB) throws Exception {
try {
return supplierA.get();
} catch(Exception e) {
// dill with exception
return supplierB.get();
}
}
至此,我们完成了降级的逻辑。接来下,将开关逻辑提取为一个函数
/**
* 1. switcher 打开,执行 A
* 2. switcher 关闭,执行 B
*/
public <T> T invoke(Supplier<Boolean> switcher, ThrowingSupplier<T> executeA, ThrowingSupplier<T> executeB) throws Exception {
return switcher.get() ? executeIfThrowing(this::queryExchangeRateA, this::queryExchangeRateB) : executeIfThrowing(this::queryExchangeRateB, this::queryExchangeRateA);
}
回到上边的两个需求,查询汇率及关税,我们可以
/**
* 查询汇率
*/
val exchangeRate = invoke(
exchangeRateSwitch::isOpen,
this::queryExchangeRateA,
this::queryExchangeRateB
)

/**
* 查询关税
*/
val queryTariffs = invoke(
tariffsSwitch::isOpen,
this::queryTariffsA,
this::queryTariffsB
)
以上,用到了 ThrowingSupplier,该点会在《[Java] Lambda 表达式“陷阱”》中详细介绍
设计模式
Lambda 表达式,会给以往面向对象思想的设计模式带来全新的设计思路,这部分内容希望在设计模式系列文章中详细介绍。
关于 Lambda 表达式,还有非常多的内容及技巧,无法使用有限的篇幅进行介绍,同时也希望与各位一同讨论。

正文完
 0