今天,用通俗易懂的大白话来彻底搞明白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 注解?是
既然是 是 是,那这就完事儿了,一个活生生的,崭新的函数式接口就诞生了!
就这么几行代码,没啥看不懂的吧,如果有不懂的,那我猜只能是里边的那个 大写的 T 和 R,这俩是啥玩意?这是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表达式
发布评论