现代编程语言都具备的Lambda到底是个啥?详细聊聊Lambda与函数式接口

6,408 阅读11分钟

本文是 JDK8 系列文章的第三篇,说起来有点惭愧,距离本专栏的上一次更新已经一年了,之前主要更新了数据结构和其他的一些文章,而且还停更了一段时间,这次打算继续恢复之前的节奏来更新,希望读者老爷们继续支持~

说回正题,本篇主要给大家带来 Lambda 的前世今生和函数式接口的关系,并详细讲解函数式接口中的四个主要接口和一众衍生接口,在文章正式开始之前,想必一些读者也是知道 Lambda 是匿名表达式的,但是可能并不清楚它是根据什么规则进行的转换,想必大家看完本文之后会有一个清晰的了解。

以下是本文的整体脉络:

和耳朵 (2).png

1. Lambda

咱们首先来说说 Lambda 这个名字,Lambda 并不是一个什么的缩写,它是希腊第十一个字母 λ 的读音,同时它也是微积分函数中的一个概念,所表达的意思是一个函数入参和出参定义,在编程语言中其实是借用了数学中的 λ,并且多了一点含义,在编程语言中功能代表它具体功能的叫法是匿名函数(Anonymous Function),根据百科的解释:

匿名函数(英语:Anonymous Function)在计算机编程中是指一类无需定义标识符(函数名)的函数子程序

到这我们应该看懂了,在编程语言中引入了 λ 的数学中的意思后,还加入了“匿名”这个概念,为什么要加它呢?显然是为了让开发者写起来更加方便,不必去想具体的函数名,尤其是在流式表达中,匿名能让你更加高效。

接着再来说说Lambda 的历史,虽然它在 JDK8 发布之后才正式出现,但是在编程语言界,它是一个具有悠久历史的东西,最早在 1958 年在Lisp 语言中首先采用,而且虽然Java脱胎于C++,但是C++在2011年已经发布了Lambda 了,但是 JDK8 的 LTS 在2014年才发布,所以 Java 被人叫做老土不是没有原因的,现代编程语言则是全部一出生就自带 Lambda 支持,所以Lambda 其实是越来越火的一个节奏~

那么Lambda 到底好在哪?不用写函数名?其实我觉得要回答这个问题首先要明白Lambda 在编程语言方面到底是什么?

上面也说了,Lambda 在编程语言中往往是一个匿名函数,也就是说Lambda 是一个抽象概念,而编程语言提供了配套支持,比如在 Java 中其实为Lambda 进行配套的就是函数式接口,通过函数式接口生成匿名类和方法进行Lambda 式的处理。

那么,既然是这一套规则我们明白了,那么Lambda 所提供的好处在Java中就是函数式接口所提供的能力了,函数式接口往往则是提供了一些通用能力,这些函数式接口在JDK中也有一套完整的实践,那就是 Stream

Stream 提供了一套完整的流式处理方法帮助我们进行流式调用,熟悉Stream 的读者应该知道使用它能带来多么大的便捷,更多关于 Stream 的知识可以看我的 延迟执行与不可变,系统讲解JavaStream数据处理 ,在这篇文章中有着详细的叙述。

那么总结起来,Lambda 在Java中所提供的好处就是使用函数式接口对一些问题进行了抽象,从而得到了一些通用能力,这些通用能力就是使用Lambda 最大的好处,下面将会具体讲解JDK中都定义了哪些通用能力,看到这的小伙伴可以给本文点个赞,以示鼓励。

2. 函数式接口

在 Java 中,所有的函数式接口都是以 @Functionallnterface 进行标注的,就像这样:

@FunctionalInterface
public interface Runnable {

    public abstract void run();
}

在一个接口上打上 @Functionallnterface并且定义一个抽象方法,这样的类我们就称之为函数式接口,当然这个方法并不一定非要用抽象关键字来修饰,比如:

@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);
    
}

当然,不写 @Functionallnterface 注解其实也没关系,但是需要保证,这个接口只定义了一个抽象方法,接口的默认方法不算,那个可以称得上是接口的静态方法了。

为什么只能有一个抽象方法呢?因为你的自定义逻辑就是这个方法的匿名函数,最终会调用这个方法,所以只能有一个。

然后你就可以使用 Lambda 表达式来进行书写了,就像这样:

        Thread thread = new Thread(() -> {
            
        });

看吧,很方便的写法就定义了一个Runnable 的匿名子类出来,不过 Runnable 这种使用Lambda 只是为了生成一个匿名子类的情况确实无法完全发挥Lambda 的作用,Lambda 更大的作用还是在解决具体的问题上,而非创造一个匿名类。

举个例子,假如你想定义一个对商品数据进行商品筛查的函数,那么它可能是这样的:

    public List<Goods> filter(List<Goods> list, String type) {
        
        List<Goods> result = new ArrayList<>();
        
        for (Goods goods : list) {
            
            if (goods.getType().equals(type)) {
                
                result.add(goods);
                
            }
        }
        
        return result;
    }

ok,看起来一切没问题,但是架不住需求改变啊,很快你又需要定义一个对商品金额进行筛查的方法,那么它可能是这样的:

    public List<Goods> gt(List<Goods> list, Integer price) {

        List<Goods> result = new ArrayList<>();

        for (Goods goods : list) {

            if (goods.getPrice() > price) {

                result.add(goods);

            }
        }

        return result;
    }

那么你可以发现:大部分代码基本没变,只有入参和判断逻辑发生了一点改变,这个时候你可能会想,能不能把判断逻辑直接抽象成一个匿名函数,每次只需要简单写一个这个判断函数即可,再把方法入参封装成一个东西,在任何场景下都可以使用。

看到这,你可能就有点明白了,因为在Java8 已经提供了Stream流去做这件事,上面这个场景其实对应的是Stream 中的filter 方法,而filter 方法的入参就是一个函数式接口——Predicate

还没明白吗?那我说的再清楚一点,Predicate 抽象了判断这个场景,而且这种抽象不局限于业务,是直接对某一类场景进行抽象,比如筛选商品类别,筛选商品大于某个金额或者小于某个金额,它不在纠结你到底想要怎么筛选,而是直接对筛选函数进行抽象,得到了Predicate,你想怎么筛选你自己写,剩下的交给它,它将一劳永逸的解决这类问题,当然这里面还有一部分 Stream 的功劳,不过主要思想还是 Predicate 在做,Stream 这里我们暂且不提。

像这种对于某个场景进行顶级抽象的函数式接口,JDK一共提供了四个:

  1. Consumer

  2. Supplier

  3. Predicate

  4. Function

接下来我将一一为大家进行讲述,除了这四个之外还有大量的衍生函数式接口,在JDK8中就有50个左右,不过都是在这四个基础上进行修改,不必担心记不住的问题。

3. Consumer

**Consumer **通过名字可以看出它是一个消费函数式接口,主要针对的是消费这个场景,它的代码定义如下:

@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);
    
}

通过泛型 T 定义了一个入参,但是没有返回值,它代表你可以针对这个入参做一些自定义逻辑,比较典型的例子是 Stream 中的 forEach 方法。

而我们的主要使用场景也往往是循环进行某项操作,比如有一堆手机号,循环进行发短信。

所以消费场景是 Consumer 的主要用武之地,但是有时候你还面临一个问题,一个入参似乎太少了,有时候你需要对两个对象进行操作,又懒得将它们合并成一个对象,这种情况 JDK 提供了 BiConsumer

@FunctionalInterface
public interface BiConsumer<T, U> {

    void accept(T t, U u);
}

这种你可以直接传进去两个参数了,什么?你想要三个参数的?那没有,三个或者三个以上我感觉就有必要合并成一个对象进行消费了。

除了这两个之外,还有DoubleConsumerIntConsumerLongConsumer这种限定了入参类型的 Consumer,这里不再多述。

4. Supplier

Supplier通过名字比较难看出来它是一个场景的函数式接口,它主要针对的是get这个场景或者说获取这个场景,它的代码定义如下:

@FunctionalInterface
public interface Supplier<T> {

    T get();
}

通过泛型 T 定义了一个返回值类型,但是没有入参,它代表你可以针对调用方获取某个值,比较典型的例子是 Stream 中的 collect 方法,通过自定义传入我们想要取得的某种对象进行对象收集。

而我们的主要使用场景也往往是收集和聚合这个场景了,这个场景我们也是对获得这个场景进行收集。

和Consumer一样,Supplier还具有以下衍生接口:

  1. BooleanSupplier

  2. DoubleSupplier

  3. IntSupplier

  4. LongSupplier

都是提前对获取的定义好了数据类型,思想一致,这里不再多述。

5. Predicate

Predicate前文我们已经介绍过,它主要针对的是判断这个场景,它的代码定义如下:

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);
}

通过泛型 T 定义了一个入参,返回了一个布尔值,它代表你可以传入一段判断逻辑的函数,比较典型的例子是 Stream 中的 filter方法。

我们对于它的使用场景实在是太多了,基本上做任何业务都有在内存中进行筛选 or 判断的场景。

所以判断和筛选场景是 Predicate的主要用武之地,但是有时候你还面临和上面一样的问题,一个入参似乎太少了,有时候你需要对两个对象进行操作,又懒得将它们合并成一个对象,这种情况 JDK 提供了 BiPredicate

@FunctionalInterface
public interface BiPredicate<T, U> {

    boolean test(T t, U u);
}

这种你可以直接传进去两个参数进行函数的自定义逻辑。

除了这两个之外,还有DoublePredicateIntPredicateLongPredicate这种限定了入参类型的Predicate,这里不再多述。

6. Function

Function 接口的名字不太能轻易看出来它的场景,它主要针对的则是 转换这个场景,其实说转换可能也不太正确,它是一个覆盖范围比较广的场景,你也可以理解为扩展版的Consumer,接口定义如下:

@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);
}

通过一个入参 T 进行自定义逻辑处理,最终得到一个出参 R,比较典型的例子是 Stream 中的 map 系列方法和 reduce 系列方法。

为什么我说也可以理解为一个扩展版的Consumer呢?我们还举例手机号发短信的场景好了,你通过循环发完短信之后可能想拿到发完短信之后的结果对象,来进行后续处理。

这个时候单纯的Consumer就不行了,因为它没有返回值,你就可以通过 Function 这种函数式对象进行处理了。

和 Consumer 一样,Function 也有一个衍生接口可以通过两个入参返回一个对象——BiFunction<T,U,R>

还有一些定义好了入参和出参的 Function 我这里就不再赘述了~

7. 写在最后

其实这篇文章在之前的Stream的时候就该写了,一直没有机会写,看完这篇文章之后,我建议读者们再配合另外两篇Stream 文章进行阅读效果应该会更好一些:

  1. 延迟执行与不可变,系统讲解JavaStream数据处理

  2. 归约、分组与分区,深入讲解JavaStream终结操作

下一篇的话,应该是 JDK8 的 CompletableFuture,这位更是重量级的,估计需要花费更多的时间了~

最后,如果觉得本文对你们有所帮助的话,请不要吝啬你们的点赞,毕竟不用投币😂😂😂,有什么问题也可以在评论区讨论,读者们的支持一直是我更新的最大动力,我们下期见。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿