今天,用通俗易懂的大白话来搞明白Java里的函数式编程和Lambda表达式
为什么引入函数式编程,lambda表达式?
大家都知道,JDK1.8引入了函数式编程,lambda表达式。
那有没有想过,为什么引入这个东西?没它之前我代码不也写的好好的吗?
我个人的理解,JDK的每次升级,无非是围绕三方面:提升安全性、提升效率、简化书写。
比如:
JDK1.5引入泛型,是为了安全性方面的考虑;
JDK1.6引入自旋锁、偏向锁、轻量级锁等等一些手段,对synchronized进行优化,是为了提升效率;
而JDK1.8引入函数式编程,主要是为了简化书写。
你可能需要了解的一些东西
首先需要简单了解一下函数式编程的思想:函数式编程的思想简单来说就是,可以在书写代码的过程中将一个行为过程作为参数进行传递,主要目的是为了简化书写
它的底层实现其实还是匿名内部实现类,不懂匿名内部类的需要补一下基础知识
函数式接口最大的特征就是,这个接口里只能有一个待实现的方法。
所以我理解的是,以前的匿名内部类,既可以满足只有一个待实现方法的情况,也可以满足多个待实现方法的情况;但是函数式的这种,它只能用于只有一个待实现方法的情况
所以,我觉得函数式编程只是匿名内部类的一种特殊情况而已,它只能满足匿名内部类能满足的众多情况中的其中一种,只是它的写法上形式上和之前的匿名内部类有所不同(仅此而已,它底层还是匿名内部类,并且规定你匿名内部类实现的这个接口里只能有一个方法),并且它在书写格式上进行了大大的简化,因为把它弄出来的主要目的就是为了简化书写。这是我对函数式编程和匿名内部类的一些理解,我认为这个很重要。
上述这段话,可以先作为一个了解,如果暂时不理解的话,问题也不大。
有疑问很正常,继续往下看,花几分钟看完文章后,保证给你整的明明白白的。
一、初识Lambda
1、基本语法
到目前为止,对函数式编程和Lambda表达式没有一点了解的话,可以出门左拐,看我另外一篇对函数式和Lambda进行了简短介绍的文章:JDK8 Lambda表达式
对函数式编程和Lambda有了一些简单的了解后,可以发现下面的内容,是我们对Lambda最直观的认识:
( ) -> { }
上边是Lambda表达式最基本的语法:左边的括号里是参数,中间是一个横杠箭头 -> ,右边的花括号里是表达式或代码块;
举个例子,比如:
(String name)->{
return "姓名:" + name;
}
上边这种写法,大家应该不陌生吧,咱们大部分情况下都是直接把上边的这种Lambda表达式作为一个参数,传递到 某个需要这种函数式接口作为参数的方法里。
其实,咱们也可以把Lambda表达式 在等号左边用个函数式接口来接受一下它的,比如下边这种写法:上边的例子,如果左边接收的类型补全的话,是这样的:
Functionfunctiona = (String name)->{ return "姓名:" + name; };
那左边接收Lambda表达式的这个到底是个啥东西呢?它其实就是个普普通通的接口,只不过这个接口里只有一个待实现的方法(仅此而已)。如果有不理解的,也没事,脑子里先有这么个概念就行,具体的内容,等会儿下边会慢慢讲到。
2、简写形式
如果你了解的再深入一点点的话,肯定会知道上边的语法也可以简写
那简写的规则是啥呢?我理解的,大白话来说大概就两点:
一、左边括号里的参数类型可以省略,而且如果只有一个参数的话,连左边的括号也可以省略不写
二、右边花括号里如果只有一行代码,那么,花括号、return和分号,这三个可以一起省略不写
按照上边总结的简写规则:左边参数类型去掉,又因为只有一个参数,参数的括号也去掉;右边只有一行代码,所以花括号、return和分号,这三个一起省略不写。所以,上边的例子最终可以简写如下:
//左边参数类型去掉,又因为只有一个参数,所以括号也去掉 //右边只有一行代码,所以右边花括号、return和分号,这三个一起省略不写 name -> "姓名:" + name;
左边接收的类型补全的话,是这样:
Functionfunction = name -> "姓名:" + name;
看完简写的形式后,你没懵逼吧?
醒醒啊,别懵啊兄弟,这种简写的形式,它只是满足了我上边说的 能简写的条件,只是换了一种写法,仅此而已。所以,记住我上边说的简写条件吧,很重要。
是不是觉得 这种写法简洁了许多?那肯定简洁啊,用Lambda表达式的目的就是简化书写,怎么简单怎么来。
二、自定义函数式接口
刚才写了个小例子
Functionfunction = (String name)->{ return "姓名:" + name; };
而且刚才顺便也提出了一个疑问:右边是Lambda表达式,那左边接收它的这个 Function
它就是咱们一直说的函数式编程里的函数式接口
上边的例子,我用的是JDK里自带的这个函数式接口 Function
其实,咱们也可以自己去定义,自己去手写一个这种函数式接口
那什么样的接口,才称得上函数式接口呢?
很简单,刚才也提了一下,你写的接口里只要 只有一个待实现的方法,它就能称得上一个函数式接口,当然接口上最好再加一个 @FunctionalInterface 注解,加这个注解是JDK官方推荐的写法。我理解的,之所以官方推荐你加这个注解,是因为,给你的接口加上这个注解后,你发现你写的接口里就只能定义一个待实现的方法了,你接口里再多写一个方法它就给你报红,编译报错。所以加上它就是为了确保你的接口里只有一个待实现的方法,确保你的接口是个函数式接口。其实,只要你规规矩矩的,接口里只写一个方法,不加它也行的,它只是个额外的保障,或者说是个推荐的规范。
好,接下来,上代码!咱们写一个自己的函数式接口
@FunctionalInterface interface MyFunction{ R gogogo(T t); }
完事了,你没看错,就是这么简单,千万不要觉得这些东西很难,下面咱们捋一捋
我这写的是不是一个接口?是
我这个接口里是不是只有一个未实现的方法 gogogo( ) ?是
我是不是还非常遵守JDK的建议,顺便给它加上了@FunctionalInterface 注解?是
既然是 是 是,那这就完事儿了,一个活生生的,崭新的函数式接口就诞生了!
就这么几行代码,没啥看不懂的吧,如果有不懂的,那我猜只能是里边的那个 大写的 T 和 R,这俩是啥玩意?这是JDK1.5之后开始支持的泛型,如果被我猜中,不懂的话,可以看我之前写的关于泛型的文章:Java基础知识之泛型以及自定义泛型
好,那我们继续?
既然我们自定义的函数式接口写好了,那就可以来用一下了
还是上边的例子,咱们就可以不用JDK里自带的 Function
MyFunctionmf = (name) -> { return "姓名:" + name; }; //可以调用一下里边的方法 mf.gogogo("张三");
好,到这里,咱们自定义的函数式接口就ok了
但是,咱们每次用之前都得去自己定义一个函数式接口吗?
还有就是,你是否有以下的疑问?
好,别急,跟着我慢慢来,我会一个一个的给你整明白。
三、JDK里提供的函数式接口
刚才问道:咱们每次用之前都得去自己定义一个函数式接口吗?
那当然不是了!
你想想,你实际开发中,你接口里的方法无非就是:有入参+无返回值、无入参+有返回值、
有入参+有返回值,或者 无入参+无返回值,无非就是这几种场景
注:这里说的是平常使用过程中的大部分情况,如果万一有不满足你的需求,你也可以按照上边讲的,去自己手写定义接口
所以JDK里给我们提供了四种 函数式编程的接口,基本上能覆盖咱们实际使用中的99%的情况
1、Consumer
2、Supplier
3、Function
4、Predicate
上代码之前,说一个东西:
大家平常使用lambda的过程中,大部分情况都是作为参数直接写在某个方法的括号里的
应该很少把它单独定义出来吧,就是 = 等号左边用一个类型来接收它(就像咱们上边例子里写的)很少这么写吧
但是,是可以这么写的,那这么写的话,就会有个疑问
那就是,这么写 那左边我该用什么来接收呢?
答案很简单,左边用什么接收,是看你右边的Lambda的 入参和返回值来定的
比如,
你写的右边的Lambda表达式如果是 有入参+无返回值,那就用Consumer 这里必须敲黑板,重点来了啊!!!咱们上边的例子里就是这种有入参+有返回值的,所以我刚开始就用了JDK里的Function来接收,后来我又自己定义了一个MyFunction接口,我的MyFunction接口里的那个方法,我写的也是有入参+有返回值,所以,你看,我用我的MyFunction也能接收; 到这里,是不是慢慢的对函数式编程和Lambda表达式有点感觉了。 实际开发中百分之九十九的情况都可以用上边讲的那几种JDK里已经定义好的函数式接口来接收,而不用再自己手动定义。 ok,来点具体的代码,亲自体验一下 可以看下JDK里的 Consumer 看上图,有人可能会问,你不是说,函数式接口要求 接口里只有一个方法吗?那图上Consumer接口里怎么有两个方法啊? 我说的是接口里只有一个待实现的方法,Consumer接口里是有两个方法,但是待实现的方法只有一个啊,另外一个不算,另外一个是加了default的,那default是个啥?这个也是JDK1.8之后加入的新特性,就是JDK1.8之后,接口里能有默认的实现方法了,感兴趣的话,可以多学学JDK更新的新特性,要不就落伍了。 所以,你发现没,JDK里的这个Consumer接口里边其实也没啥,无非就是定义了一个有入参+无返回值的未实现的方法,那咱们完全可以写一个一样的接口,但还是那句话,没必要重复造轮 上代码用一下 注:stream()流里的forEach,源码里其实用的就是Consumer型的,可以看下JDK里源码的截图 先看下JDK里的Supplier 这个更简洁,里边就定义了一个 无入参+有返回值的 未实现的方法 上代码用一下 先看下JDK里的Function 这个图,我没把Function 这个未实现的apply方法它是一个 有入参+有返回值的 上代码用一下 先看下JDK里的Predicate Predicate 这个未实现的test方法它是一个 有入参+有返回值的,而且它的返回值不再是泛型了,而是已经确定类型的布尔型 上代码用一下 注:stream()流里的filter,源码里用的就是Predicate型的,可以看下JDK里源码的截图 以上说的四种是JDK提供的四种最基本的 函数式编程的接口 其实,除了以上四种基本的以外,还有其他的一些函数式编程的接口 比如: 1、BiFunction 2、BiConsumer 3、BiPredicate 这三个就不挨个讲了,按照上面讲的套路,你可以点击源码进去看看 你会发现,上边说的最基本的四种里,有三个是有入参的,但是它们的入参都只是一个 然后先在说的这三种,它们都是入参是多个的,是对上面三个有入参但参数个数只有一个的一种补充 其实你发现还有一种情况,上边我没说,那就是 无入参+无返回值 的这种情况 那这种情况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拿来用,是没有一点多线程的意思的,就仅仅是因为它是个无入参,无返回值的函数式接口而已,仅此而已 说白了,其实他再去自己新个无入参,无返回值的 函数式接口也是可以的 可以看下我下边的这个代码例子: 你说,上边这个例子,它和多线程有毛关系? 所以,你看,你如果没彻底搞懂函数式编程,没彻底搞懂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了,反正能抓着老鼠的猫就是好猫。 铁子们,如果觉得文章对你有所帮助,可以点关注,点赞 也可以关注下公众号:扫码 或 wx搜索:“聊5毛钱的java” ,欢迎一起学习交流,关注公众号可领取博主的Java学习视频+资料,保证都是干货 3Q~ 纯手敲原创不易,如果觉得对你有帮助,可以打赏支持一下,哈哈,感谢~ //1、Consumer
//2、Supplier
3、Function//3、Function
4、Predicate//4、Predicate
//5、Runnable public abstract void run(); 无入参,无返回值
Runnable runnable = () -> {
System.out.println("哈哈哈哈哈");
};
四、总结



