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

栏目: 编程语言 · Java · 发布时间: 5年前

内容简介:该文章已收录【修炼内功】跃迁之路Lambda表达式,可以理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型。

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

该文章已收录【修炼内功】跃迁之路

Lambda表达式,可以理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型。

这里,默认您已对 Java 8的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 的函数出来,貌似又陷入了无穷加班的怪圈~

还有没有优化空间?能否将筛选条件抽离到单一属性,如 byColorbyMinWeight 等,之后再做与或计算传递给筛选函数?

接下来就是我们要介绍的 高阶函数

高阶函数

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

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

以上的定义听起来可能有些绕口。结合上节示例,我们的诉求是将苹果的颜色、重量或者其他筛选条件也抽离出来,而不是硬编码到代码中

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

至此,还可以抽象出 anyMatchesnonMatches 等高阶函数,组合使用

// 筛选出 颜色为红色 并且 重量在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 时使用(更改)

柯里化

柯里化是逐步传值,逐步缩小函数的适用范围,逐步求解的过程。

如,设计一个函数,实现在延迟一定时间之后执行给定逻辑,并可以指定执行的执行器

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 中,调用的时候,只需要关心具体的执行逻辑即可

环绕执行(提取共性)

有时候我们会发现,很多代码块十分相似,但又有些许不同

比如,目前有两个接口可以查询汇率, queryExchangeRateAqueryExchangeRateB ,我们需要在开关 exchangeRateSwitch 打开的时候使用 queryExchangeRateA 查询,否则使用 queryExchangeRateB 查询,同时在一个接口异常失败的时候,自动降低到另一个接口进行查询

同样,目前有两个接口可以查询关税, queryTariffsAqueryTariffsB ,同样地,我们需要在开关 tariffsSwitch 打开的时候使用 queryTariffsA 查询,否则使用 queryTariffsB 查询,同时在一个接口异常失败的时候,自动降低到另一个接口进行查询

其实,以上两种场景,除了开关及具体的接口逻辑外,整体流程是一致的

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

再分析,其实接口调用的降级逻辑也是一样的

这里不再列举如何使用抽象类的方法如解决该类问题,我们直接使用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表达式,还有非常多的内容及技巧,无法使用有限的篇幅进行介绍,同时也希望与各位一同讨论。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

HTML5

HTML5

Matthew David / Focal Press / 2010-07-29 / USD 39.95

Implement the powerful new multimedia and interactive capabilities offered by HTML5, including style control tools, illustration tools, video, audio, and rich media solutions. Understand how HTML5 is ......一起来看看 《HTML5》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具