今天,用通俗易懂的大白话来彻底搞明白Java里的函数式编程和Lambda表达式

为什么引入函数式编程,lambda表达式?

大家都知道,JDK1.8引入了函数式编程,lambda表达式。

那有没有想过,为什么引入这个东西?没它之前我代码不也写的好好的吗?

我个人的理解,JDK的每次升级,无非是围绕三方面:提升安全性、提升效率、简化书写。

比如:

1、JDK1.5引入泛型,是为了安全性方面的考虑;

关于泛型的知识,如果感兴趣,可以看我之前的文章:Java基础知识之泛型以及自定义泛型

2、JDK1.6引入自旋锁、偏向锁、轻量级锁等等一些手段,对synchronized进行优化,是为了提升效率;

关于synchronized以及这些锁的升级过程,感兴趣的话,看我另外一篇:Synchronized、偏向锁、自旋锁、轻量级锁以及锁的升级过程

3、而JDK1.8引入函数式编程,主要是为了简化书写。

关于函数式编程和Lambda表达式,本篇来讲

我最近开通了几个专栏,里边有更多用大白话讲的干货,全部都是我自己对一些知识点的理解,绝对干货,不拖泥带水,感兴趣的小伙伴可以去看看,专栏持续更新中,后期会持续更新更多的大白话干货

通俗易懂的大白话干货系列

带你玩转实际工作中的Jenkins系列

ok,进入今天的正题吧

你可能需要了解的一些东西

首先需要简单了解一下函数式编程的思想:函数式编程的思想简单来说就是,可以在书写代码的过程中将一个行为过程作为参数进行传递,主要目的是为了简化书写

它的底层实现其实还是匿名内部实现类,不懂匿名内部类的需要补一下基础知识

函数式接口最大的特征就是,这个接口里只能有一个待实现的方法。

所以我理解的是,以前的匿名内部类,既可以满足只有一个待实现方法的情况,也可以满足多个待实现方法的情况;但是函数式的这种,它只能用于只有一个待实现方法的情况
所以,我觉得函数式编程只是匿名内部类的一种特殊情况而已,它只能满足匿名内部类能满足的众多情况中的其中一种,只是它的写法上形式上和之前的匿名内部类有所不同(仅此而已,它底层还是匿名内部类,并且规定你匿名内部类实现的这个接口里只能有一个方法),并且它在书写格式上进行了大大的简化,因为把它弄出来的主要目的就是为了简化书写。这是我对函数式编程和匿名内部类的一些理解,我认为这个很重要。

上述这段话,可以先作为一个了解,如果暂时不理解的话,问题也不大。

有疑问很正常,继续往下看,花几分钟看完文章后,保证给你整的明明白白的。

一、初识Lambda

1、基本语法

到目前为止,如果对Java里的函数式编程和Lambda表达式一点都没有了解的话,可以先看一下我另外一篇对函数式编程和Lambda进行简短介绍的文章:JDK8 Lambda表达式

对函数式编程和Lambda有了一些简单的了解后,可以发现下面的内容,是我们对Lambda最直观的认识:

( ) -> { }

上边是Lambda表达式最基本的语法:左边的括号里是参数,中间是一个横杠箭头 -> ,右边的花括号里是表达式或代码块;

举个例子,比如:

(String name)->{
    return "姓名:" + name;
}

上边这种写法,大家应该不陌生吧,咱们大部分情况下都是直接把上边的这种Lambda表达式作为一个参数,传递到 某个需要这种函数式接口作为参数的方法里。

其实,咱们也可以把Lambda表达式 在等号左边用个函数式接口来接受一下它的,比如下边这种写法:上边的例子,如果左边接收的类型补全的话,是这样的:

Function<String,String> functiona = (String name)->{
    return "姓名:" + name;
};

那左边接收Lambda表达式的这个到底是个啥东西呢?它其实就是个普普通通的接口,只不过这个接口里只有一个待实现的方法(仅此而已)。如果有不理解的,也没事,脑子里先有这么个概念就行,具体的内容,等会儿下边会慢慢讲到。

2、简写形式

如果你了解的再深入一点点的话,肯定会知道上边的语法也可以简写

那简写的规则是啥呢?我理解的,大白话来说大概就两点:

一、左边括号里的参数类型可以省略,而且如果只有一个参数的话,连左边的括号也可以省略不写

二、右边花括号里如果只有一行代码,那么,花括号、return和分号,这三个可以一起省略不写

按照上边总结的简写规则:左边参数类型去掉,又因为只有一个参数,参数的括号也去掉;右边只有一行代码,所以花括号、return和分号,这三个一起省略不写。所以,上边的例子最终可以简写如下:

//左边参数类型去掉,又因为只有一个参数,所以括号也去掉
//右边只有一行代码,所以右边花括号、return和分号,这三个一起省略不写
name -> "姓名:" + name;

左边接收的类型补全的话,是这样:

Function<String,String> function = name -> "姓名:" + name;

看完简写的形式后,你没懵逼吧?

别懵啊兄弟,这种简写的形式,它只是满足了我上边说的 能简写的条件,只是换了一种写法,仅此而已。所以,记住我上边说的简写条件吧,很重要。

是不是觉得 这种写法简洁了许多?那肯定简洁啊,用Lambda表达式的目的就是简化书写,怎么简单怎么来。

好,知道了简写的规则后,接下来,看一个有趣的小玩意,看看你是不是真的理解了Lambda的简写

如上图, user -> new UserDO();  就这么一句小玩意儿,你觉得它代表啥意思?

第一眼,乍一看,觉得这个Lambda代表的是,左边只有一个入参,省略了括号;右边代码只有一句,所以省略了 花括号、return和分号。这么解释可能对,也可能不对。为啥呢?

你想想啊,你能确定右边没return,是因为它省略了没写;还是它根本就不是省略的,是本身本来就没有返回值?你能确定吗?

所以,就这同样的一句代码:user -> new UserDO();    它代表的东西可能不一样

如果把这一句写全的话,可能是如下图的两种情况

 然后简写可以如下

 发现了没,右边同样一模一样的一句代码,左边接收它的类型竟然可以不一样。那左边为啥不一样?其实说白了,左边用啥类型,真正是由你右边参数返回值来决定的。

这里他俩看起来一样,是由于简写这种形式导致的,这种简写,它迷惑了你。

不简写的话,它们俩其实是不一样的:一个有返回值,一个没返回值。如果有return返回值 那左边就用Function这个函数式接口来接收它;如果没return返回值就用 Consumer这个函数式接口来接收它。

到这里你可能会有疑问,就是左边的这个玩意,它到底是啥,一会儿是种类型的来接收,一会儿又是另外一个类型的来接收。

有疑问,不要紧,就先记住:左边用啥类型,是看你右边的参数和返回值来决定的

就是你写的Lambda的入参和返回值非常重要,它俩会决定你Lambda属于哪种类型(接口)。这一点很重要!

看完后边的内容,你再回过头来看这里,你就会明白了,你发现会有点前后呼应的感觉。看完后边,你会忽然发现:哦~~ soga,原来是因为右边的参数和返回值不同,所以左边才对应不同的类型;甚至说,这个类型不一定非得是JDK里定义好的,也可以是我自己手写的一个函数式接口,只要这个接口里的方法入参返回值符合我右边Lambda的入参和返回值就行了

(点我,点我~~完整看完文章后,再来这里看我一次)

二、自定义函数式接口

刚才写了个小例子

Function<String,String> function = (String name)->{
    return "姓名:" + name;
};

而且前边不止一次的提出了一个疑问:右边是Lambda表达式,那左边接收它的这个 Function<String,String> function  它到底是个啥玩意???

好,重点来了!

它其实就是咱们一直在说的:函数式编程里的函数式接口

上边的例子,我用的是JDK里自带的这个函数式接口  Function<T, R> 来接收的

其实,咱们也可以自己去定义,自己去手写一个这种函数式接口

那什么样的接口,才称得上函数式接口呢?

很简单,刚才也提了一下,你写的接口里只要 只有一个待实现的方法,它就能称得上一个函数式接口,当然接口上最好再加一个 @FunctionalInterface 注解,加这个注解是JDK官方推荐的写法。我理解的,之所以官方推荐你加这个注解,是因为,给你的接口加上这个注解后,你发现你写的接口里就只能定义一个待实现的方法了,你接口里再多写一个方法它就给你报红,编译报错。所以加上它就是为了确保你的接口里只有一个待实现的方法,确保你的接口是个函数式接口。其实,只要你规规矩矩的,接口里只写一个方法,不加它也行的,它只是个额外的保障,或者说是个推荐的规范。

好,接下来,上代码!咱们写一个自己的函数式接口

@FunctionalInterface
interface MyFunction<T,R>{
    R gogogo(T t);
}

完事了,你没看错,就是这么简单,千万不要觉得这些东西很难,下面咱们捋一捋

我这写的是不是一个接口?是

我这个接口里是不是只有一个未实现的方法  gogogo( ) ?是

我是不是还非常遵守JDK的建议,顺便给它加上了@FunctionalInterface 注解?是

既然是 是 是,那这就完事儿了,一个活生生的,崭新的函数式接口就诞生了!

就这么几行代码,没啥看不懂的吧,如果有不懂的,那我猜只能是里边的那个 大写的  TR,这俩是啥玩意?这是JDK1.5之后开始支持的泛型,如果被我猜中,不懂的话,可以看我之前写的关于泛型的文章:Java基础知识之泛型以及自定义泛型

好,那我们继续?

既然我们自定义的函数式接口写好了,那就可以来用一下了

还是上边的例子,咱们就可以不用JDK里自带的 Function<String,String> 来接收咱们写的Lambda了,可以改成下面的代码

MyFunction<String,String> mf = (name) -> {
    return "姓名:" + name;
};
//可以调用一下里边的方法
mf.gogogo("张三");

好,到这里,咱们自定义的函数式接口就ok了

但是,咱们每次用之前都得去自己定义一个函数式接口吗?

还有就是,你是否有以下的疑问?

 好,别急,跟着我慢慢来,会一个一个的给你整明白。

三、JDK里提供的函数式接口

刚才问道:咱们每次用之前都得去自己定义一个函数式接口吗?

那当然不是了!

你想想,你实际开发中,你接口里的方法无非就是:有入参+无返回值、无入参+有返回值、

有入参+有返回值,或者 无入参+无返回值,无非就是这几种场景

注:这里说的是平常使用过程中的大部分情况,如果万一有不满足你的需求,你也可以按照上边讲的,去自己手写定义接口

所以JDK里给我们提供了四种最基本的函数式接口,基本上能覆盖咱们实际使用中的大部分情况
1、Consumer<T>  void accept(T t);   有入参,无返回值,消费型
2、Supplier<T>  T get();   无入参,有返回值,提供者或生产者型
3、Function<T, R>   R apply(T t);   有入参,有返回值,(我叫它经典型)
4、Predicate<T>   boolean test(T t);   有入参,有返回值且返回值的类型必须是布尔型的,断言型

上代码之前,说一个东西:

大家平常使用lambda的过程中,大部分情况都是作为参数直接写在某个方法的括号里的
应该很少把它单独定义出来吧,就是 = 等号左边用一个类型来接收它(就像咱们上边例子里写的)很少这么写吧。但是,这么写是绝对可以的。而且,你之前没在左边接收过它,但是右边的Lambda它肯定也是有类型的(就是不管你左边接收不接收它,它肯定都是有类型的),你想想,你平常不都是直接把Lambda当成参数传给一个方法,那这个方法接收的参数,它不还是得有个类型吗?那就会有个疑问,那左边接收它的这个东西,或者说你调用的这个方法接收的这个参数类型,它到底是个啥?

换句话说,我左边接收它的话,该用什么东西来接收呢?
答案很简单,左边用什么接收,是看你右边的Lambda的 入参和返回值来定的
比如,

  • 你写的右边的Lambda表达式如果是 有入参+无返回值,那就用Consumer<T> 来接收;
  • 你写的右边的Lambda表达式如果是 无入参+有返回值,那就用Supplier<T> 来接收;
  • 你写的右边的Lambda表达式如果是 有入参+有返回值,那就用Function<T> 来接收;

这里必须敲黑板,重点来了啊!!!咱们上边的例子里就是这种有入参+有返回值的,所以我刚开始就用了JDK里的Function来接收,后来我又自己定义了一个MyFunction接口,我的MyFunction接口里的那个方法,我写的也是有入参+有返回值,所以,你看,我用我的MyFunction也能接收;

到这里,是不是慢慢的对函数式编程和Lambda表达式有点感觉了。

实际开发中百分之九十九的情况都可以用上边讲的那几种JDK里已经定义好的函数式接口来接收,而不用再自己手动定义。

ok,来点具体的代码,亲自体验一下

1、Consumer<T> 有入参,无返回值,消费型

可以看下JDK里的 Consumer<T> 源码

 看上图,有人可能会问,你不是说,函数式接口要求 接口里只有一个方法吗?那图上Consumer接口里怎么有两个方法啊?

我说的是接口里只有一个待实现的方法,Consumer接口里是有两个方法,但是待实现的方法只有一个啊,另外一个不算,另外一个是加了default的,那default是个啥?这个也是JDK1.8之后加入的新特性,就是JDK1.8之后,接口里能有默认的实现方法了,感兴趣的话,可以多学学JDK更新的新特性,要不就落伍了。

所以,你发现没,JDK里的这个Consumer接口里边其实也没啥,无非就是定义了一个有入参+无返回值的未实现的方法,那咱们完全可以写一个一样的接口,但还是那句话,没必要重复造轮

上代码用一下

//1、Consumer<T>  void accept(T t); 有入参,无返回值,消费型的
Consumer<String> consumer = (a) -> {

};
//stream()流里的forEach,源码里用的就是Consumer
List<Object> list = new ArrayList<>();
list.forEach(item-> {});

注:stream()流里的forEach,源码里其实用的就是Consumer型的,可以看下JDK里源码的截图

2、Supplier<T> 无入参,有返回值,提供者或者生产者型

先看下JDK里的Supplier<T>接口源码

这个更简洁,里边就定义了一个 无入参+有返回值的 未实现的方法

上代码用一下

//2、Supplier<T>  T get();   无入参,有返回值,提供者生产者型的
Supplier<String> supplier = ()->{
    return "aaa";
};

3、Function<T, R> 有入参,有返回值

先看下JDK里的Function<T, R>接口源码

 这个图,我没把Function<T, R> 接口里的内容截全,下边都是加了default的默认实现方法,太长了,没必要看,我们只关心这个未实现的apply方法就行了

这个未实现的apply方法它是一个 有入参+有返回值的

上代码用一下

//3、Function<T, R>   R apply(T t);   有入参,有返回值
Function function = (b) -> {
    return "bbb";
};

4、Predicate<T> 有入参,有返回值且返回值的类型必须是布尔型的,断言型

先看下JDK里的Predicate<T>接口源码

Predicate<T>接口和上边的Function<T, R> 接口一样,图片我没截全,下边也都是加了default的默认实现方法,我们只关心这个未实现的test方法就行了

这个未实现的test方法它是一个 有入参+有返回值的,而且它的返回值不再是泛型了,而是已经确定类型的布尔型

上代码用一下

//4、Predicate<T>   boolean test(T t);   有入参,有返回值且返回值的类型必须是布尔型的
Predicate predicate = (c)->{
    return true;
};
//stream()流里的filter,源码里用的就是Predicate
List<Object> list1 = new ArrayList<>();
list1.stream().filter(item->false);

注:stream()流里的filter,源码里用的就是Predicate型的,可以看下JDK里源码的截图

5、 入参是多个的

以上说的四种是JDK提供的四种最基本的 函数式编程的接口

其实,除了以上四种基本的以外,还有其他的一些函数式编程的接口

比如:

1、BiFunction<T, U, R>    R apply(T t, U u);

2、BiConsumer<T, U>    void accept(T t, U u);

3、BiPredicate<T, U>   boolean test(T t, U u);

这三个就不挨个讲了,按照上面讲的套路,你可以点击源码进去看看

你会发现,上边说的最基本的四种里,有三个是有入参的,但是它们的入参都只是一个

然后现在说的这三种,它们都是入参是多个的,是对上面三个有入参但参数个数只有一个的一种补充

6、让你感到惊喜和意外的Runnable接口

其实你发现还有一种情况,上边我没说,那就是 无入参+无返回值 的这种情况

那这种情况JDK帮咱们定义好了吗?

这种情况JDK里其实也是有的,只不过,它不是在JDK1.8新升级的代码中专门新写一个接口来搞的(小提示:你可以看看源码,上边说的那几种都是JDK1.8之后新写的接口,注释里都写了@since 1.8),他是直接在原来的JDK里就有的一个接口上改造的 给这个接口加上了个 @FunctionalInterface 注解。你猜是JDK1.8之前就有的哪个接口?

你想想:无入参,无返回值,Runnable接口里的run方法,那太符合啦

Runnable接口里的run方法它不就是 无入参,无返回值吗?

所以 JDK1.8只是在原有的Runnable接口上加上了@FunctionalInterface注解,而没有像之前那几种是新写的接口,你可以往上翻一翻看看我对刚才讲的那几种源码的截图,上边的注释都写了@since 1.8

所以如果咱们实际开发中遇到了,需要使用无入参,无返回值的这种场景,就可以优先考虑 Runnable接口,而不需要自己去动手写一个新接口。

知道了这些后,所以,如果以后在看别人写的代码时,忽然看到他写一个方法的入参是一个函数式的Runnable 接口,但是你分析来分析去,也没发现他这个地方和多线程有什么关系,那么请不要惊讶,他这里肯定就是用的我上边说的Runnable的用法

其实就是巧了 JDK自带的 Runnable接口,刚好是 无入参,无返回值的这种,正好满足他现在的需求,所以他就没再自己去写一个接口,而是直接把Runnable接口拿来用了,但是这里把Runnable拿来用,是没有一点多线程的意思的,就仅仅是因为它是个无入参,无返回值的函数式接口而已,仅此而已

说白了,其实他再去自己新写个无入参,无返回值的 函数式接口也是可以的

可以看下我下边的这个代码例子:

//5、Runnable   public abstract void run();    无入参,无返回值
Runnable runnable = () -> {
    System.out.println("哈哈哈哈哈");
};

你说,上边这个例子,它和多线程有毛关系?

所以,你看,你如果没彻底搞懂函数式编程,没彻底搞懂lambda的话,是不是就搞不懂他写的方法里参数接收一个Runnable 是干啥呢?

一点题外话:

        说到这里,我想起来个有趣的事

        前段时间在群里,有人说Lambda不好,用了Lambda后代码的可读性不好,影响别人看懂代码,说他非常不建议大家写lambda表达式;还说代码里到处用Lambda的人都是为了让别人觉得自己厉害,都是为了炫耀。

        我去,他是真敢说啊!还他不建议写Lambda,他以为他是谁啊,JDK1.8之后,人家Java官方都推荐使用简化书写的Lambda。而且你看现在JDK或者其他的框架里的源码,都在大量的使用Lambda

        这种人没觉得说这些话的时候自己心里很虚吗?你自己如果彻底把lambda搞懂了,还会觉得写Lmabda的人牛逼吗?还会觉得别人是炫耀吗?简直是有病!

        自己不思进取,不学习新知识,还攻击别人,说别人代码可读性差,说别人炫耀的这种人,不是蠢就是坏!

四、总结

讲完了,最后来一点总结:

        有了JDK里帮咱们预先定义好的这么多 函数式接口后,基本上已经能满足咱们开发中的大部分场景。你想想,咱们在实际使用过程中基本上是跑不出上边说的这些情况的,退一步讲,就算JDK里满足不了你的场景,你也可以去你项目里 引用的其他第三方的maven jar包里找找,看看jar里是不是有人已经定义过你这种场景了。所以,在实际的开发中,我们是很少再去自定义函数式接口的

        接下来,你想想,其实你写的Lambda表达式,左边可以有多种函数式接口来接收它的,说白了就是 = 等号左边的那个类型不一定就是固定的一种,也就是说右边同样的一个Lambda表达式,左边可以用不同的函数式接口来接收它,左边来接收它的那个函数式接口,它只要满足你写的Lambda表达式的入参和返回值的情况就ok了。

        所以,左边来接收你写的lambda 的这个函数式接口,你可以在JDK里找一个满足你的,你也可以在你项目里引入的jar包里找一个满足你的,如果JDK和jar里都没有满足你的,那你可以手写一个,无非就这三种情况,但是还是那句话,JDK里那些大神预先定义的函数式接口,百分之九十九的情况下基本上都能满足你,所以说,在实际的开发中,我们是很少再去自定义函数式接口的,这句话能理解了吧?但是!如果JDK里或者jar里已经有了,但是你还非得重复造轮,非得去自己重新写一个接口来用,那也可以。但是何必呢,我相信你只要真正理解了函数式编程和Lambda的思想和用法之后,大概率是不会自己去手写的(程序员最烦干的事应该就是重复造轮了)

        当然也不绝对,看你心情吧,只要能满足你当前需求的入参返回值的情况就ok了,反正能抓着老鼠的猫就是好猫。

好,到这里我对于Java中的函数式编程和Lambda表达式的一些理解就写完了

强烈建议我非常建议看完文章后,回到上边,再看一遍,也许第二遍第三遍会有不同的理解,说不定在某个点或者某个瞬间你不理解的那个点就恍然大悟了,学习一个新东西如果有了这种恍然大悟的感觉,那说明你差不多已经吃透这个东西了,这种感觉来了你应该也会觉得挺爽

好,就写到这里吧,还希望大家不要觉得我说的太多,太啰嗦。

我觉得大家的眼睛也都是雪亮的!不管怎么样,我坚信,能让大家真正学到东西才是王道!这也是我最初决定写博客最主要的目标和动力

铁子们,如果觉得文章对你有所帮助,可以点关注,点赞

我最近开通了几个专栏,里边有更多用大白话讲的干货,全部都是我自己对一些知识点的理解,绝对干货,不拖泥带水,感兴趣的小伙伴可以去看看,专栏持续更新中,后期会持续更新更多的大白话干货

通俗易懂的大白话干货系列

带你玩转实际工作中的Jenkins系列

纯手敲原创不易,如果觉得对你有帮助,可以支持一下专栏哈,万分感谢

更多推荐

用通俗易懂的大白话搞明白Java里的函数式编程和Lambda表达式