一, 函数式编程
函数式编程, 同面向对象编程, 指令式编程一样, 是一种软件编程范式, 在多种编程语言中都有应用. 百科词条中有很学术化的解释, 但理解起来并不容易. 不过, 我们可以借助于数学中函数的概念, 来理解函数式编程的要义所在. 在数学中, 我们常见的函数表达式形如 y=f(x), 表示的是一种输入输出的映射关系: x 表示输入, y 表示输出, f 是表示两者之间的映射运算逻辑. 在求值的时候, 你完全不用考虑映射运算 f, 只要给定输入 x, 得到相应的输出 y; 输入不变, 输出也不会改变, 就这么简单. 类比到程序语言中来, 所谓函数式编程, 就是让我们以数学中函数映射的思想来编写出函数式的程序代码, 让代码着重于输入和输出, 而底层的映射处理逻辑, 你完全可以当黑盒看待, 这样, 我们的业务关注点会更加清晰; 而且, 同数学函数一样, 函数式编程的代码具有状态无关性 -- 即相同的输入永远产生相同的输出, 这在解决并发编程中共享变量状态一致性问题中有很大的应用场景.
在 Java 中, 提到函数式编程, 最先想到的肯定是 Lambda 表达式了 (PS: 切忌把 Lambda 表达式和函数式编程划等号, Lambda 表达式只是符合这种函数式编程风格的匿名函数而已).Lambda 表达式在 Java8 中终于被重磅引入了 (隔壁 Python,C#,C++ 早就引入了哟喂), 这让很多以前代码中的匿名写法得以通过函数式的代码进行极致的简化, 有多简化呢? 比如使用 IDEA 开发的时候, 如果你选择的 Java 编译版本达到 Java8 的话, 在编写匿名内部类的时候, 编译器会不厌其烦的提示你将匿名写法替换成 Lambda 表达式 --
你替换以后, 原来几行的代码就简化成了下面这个样子 --
这就是让你初见懵逼, 再见着迷的函数式编程范例了. 常有人说, 相较于匿名内部类的写法, Lambda 表达式使代码更加简洁, 易读. 简洁确实简洁, 毕竟减少了很多样板代码, 非要说易读, 博主是有些迟疑的. 从语法上来说, 除非你对 Java8 的这种新特性有过相当的学习, 否则刚开始接触这种写法, 你反而会有些凌乱, 不解其意. 喜感的是, 这种只强调输入和输出的函数式编程风格其显著优点恰是让你一见代码就知其业务内容; 你之所以会凌乱, 在于其语法相较于我们面向对象编程中熟悉的类, 接口, 字段等概念太过新颖了, 需要一定的学习成本.
在上面的代码示例中, 我们之所以写匿名内部类, 是因为在单一业务场景中, 我们不想额外的编写接口的实现再去构造对象执行方法, 而是直接创建匿名对象, 执行完接口中的方法, 对象的使命也就结束了. 相较于先实现再建对象的方式, 匿名内部类的写法算是一种代码的减省, 虽然可读性差了些, 但咱不怕! 难受的一点在于, 即使匿名写法, 依然要遵循接口实现的规范, 会多出很多样板式的代码; 而实际上, 我不过是想基于接口的方法定义去实现某种行为而已. 也就是说, 我的关注点在于接口的行为实现, 而不是样板的语法层面. 这个时候, Lambda 表达式就得以大显身手了, 它如你所愿, 让你以函数式的编码风格, 只关注行为本身的逻辑实现, 其它的无关代码通通舍弃. 就像上面的示例中, 将传统的匿名写法改成 Lambda 表达式写法后, 样板代码没了, 简洁的代码让你一眼就能看出, 你的代码要干什么.-- 这, 就是 Lambda! 虽然初见时确实有些语法障碍, 但在突破障碍之后, 你会从心的喜欢这种编程方式 -- 至少, 在你的代码走位中应该适时的加入些 Lambda 这种风骚的'姿势'了.
有人说, 不就是代码简化嘛, 语法糖而已啦. 关于 Lambda 表达式是不是语法糖 https://segmentfault.com/a/1190000019577184 的说法, 又可以开篇讲义了. 只能简单的说, Lambda 确实也是语法糖, 但绝不是简单只为简化匿名内部类的写法的语法糖. 当然, 除了国内大神们的探究, 你也可以去 Stack Overflow 上提问或搜寻答案, 看看老外们都怎么解释的.
二, Lambda
Lambda 表达式的个人理解, 其实上文中已经给出了. 现在, 我们从语法层面, 来说说实际项目中该如何编写基于 Lambda 的函数式风格代码. 博主以上面的代码为例, 整了一副草图, 帮助你快速读懂 Lambda 语法:
这只是最简单的形式. 空括号 () 表示没有输入参数, 如果匿名接口有参数, 你按照正常方法的参数定义编写即可, 如 (Object o),(Object o1,Object o2) 等. 但由于 Java7 开始就有了类型推断, 通常我们是可以省略参数类型的, 所以参数可以简化成 (o) ,(o1,o2) 的形式, 甚至在只有一个参数的时候, 括号也能省略而只保留参数 o . 至于表达式的主体部分, 也就是我们的业务代码, 既可以是是一个表达式, 如上面的一条打印, 也可以是用花括号 { } 包围的一段代码块, 具体以实际业务为准, 当然, 也要考虑代码的可读性. 最后列举一些常见 Lambda 代码示例:
- // 开启普通的线程任务
- new Thread(() -> System.out.println("函数式编程 -- 陈本布衣")).start();
- // 可以是代码块的形式
- new Thread(() ->{
- System.out.println("函数式编程中的代码块");
- System.out.println("函数式编程中的代码块");
- System.out.println("函数式编程中的代码块");
- System.out.println("函数式编程中的代码块");
- }).start();
- // 开启异步单线程, 可获取线程任务返回值
- Future<Integer> future = Executors.newSingleThreadExecutor().submit(() -> 10);
- // GUI 图形界面编程中的事件处理
- new JButton().addActionListener((ActionEvent event) -> System.out.println("按钮点击事件"));
- // 参数根据上下文推断, 单参数可省略括号 ()
- new JButton().addActionListener(event -> System.out.println("按钮点击事件"));
友情提示: 由于 Lambda 对代码的极致简化和新语法, 初学者很难一步到位的写出正确的 Lambda 表达式代码, 对初学者, 比较好的实践建议先用匿名内部类的形式先实现, 最后借助于 IDE 的快捷功能自动生成, 待熟练之后, 再装逼不迟!
三, 函数接口
只学会了 Lambda 表达式的语法还远远不够, 因为你不光要能手撸 Lambda 表达式代码, 更重要的是你要搞清楚, 在哪种场景下可以撸, 哪种场景下无法撸, 这是有讲究的. 虽然上文中举了几个示例, 但在实际应用中是远远不够的. 博主说过, Lambda 表达式本质上是一个匿名函数, 这么说, 难道只要接口采用匿名类实现的地方, 都可以使用 Lambda 吗? 答案当然是否定的! 你可以亲自试一下, 自己编写一个多方法的接口, 也采用匿名实现, 你看 IDEA 会不会那么热情的提醒你.
其实, 在 Java8 中伴随 Lambda 一起引入的, 还有函数式接口这一概念. 所谓函数式接口, 是只有一个抽象方法的接口, 只有这种接口才能被用来作为 Lambda 表达式的类型 -- 也就是说, 只有函数式接口的匿名实现, 你才可以用 Lambda 表达式去改写代码. 感觉这种限定缩窄了 Lambda 的应用范围, 我上哪儿给你找那么多只有一个抽象函数的接口啊? 有的, 有的, 而且还不少. Java8 为了支持函数式编程的应用场景, 特意新增加了一个全是函数式接口的包 java.util.function, 里面包含了四十多个函数式接口, 足够你玩一阵子的了, 而且这还不包括旧有的接口中符合函数接口定义的众多接口, 如 Runnable,Comparator<T>, 以及 Java8 中为了更便利的操作集合而新增的特性类库等. 从 Java8 开始, 你在源码中可以发现, 无论旧有的和新引入的函数式接口, 其接口声明上都会有 @FunctionalInterface 注解, 该注解其实就是专门用来标注函数式接口的, 算是一个标识注解. 当然, 也不是说函数接口就一定要用标示 @FunctionalInterface 注解来标注, 只要符合只有一个抽象方法的接口定义, 没有 @FunctionalInterface 标注也能成为 Lambda 表达式的类型, 只不过在接口上加上注解 (尤其自己在定义函数式接口的时候), 可以让编译器帮你检查错误.
函数接口, 说这么多其实差不多就算完整了, 但是且慢, 博主还是要纠结一下: 只有一个抽象方法的接口, 是为函数式接口, 那么, 是不是不止一个抽象方法的接口, 就一定不是抽象接口呢? 上一段的阐述中, 布衣博主故意列了一个 Comparator<T > 接口, 其在 Java8 中的源码如下:
- @FunctionalInterface
- public interface Comparator<T> {
- int compare(T o1, T o2);
- boolean equals(Object obj);
- // 省略非抽象的 静态 和 默认方法
- ...
- }
咦, 这厮不对啊, 有两个抽象接口, 怎么也成了函数式接口? 遇到这种情况, 切莫慌张, 找寻答案最好的方式, 还是要从该接口声明的注释中去找, 万一写源码的人搞错了呢? 当然, 错肯定是错不了, 不过 Comparator 接口声明的注释中也没有给出合理的解释, 还是只能从源头 @FunctionalInterface 注解的注释中去看看有没有答案. 在 @FunctionalInterface 注解的注释文档中你会找到这样的描述:
If an interface declares an abstract method overriding one of the public methods of {@code java.lang.Object}, that also does not count toward the interface's abstract method count since any implementation of the interface will have an implementation from {@code java.lang.Object} or elsewhere.
这算是很白话的英语了, 简单翻译一下就是, 因为所有接口都是默认继承了 Object 类的, 所以, 接口中如果有自 Object 类中继承而来的 public 方法, 就不能算成抽象方法了.
好啦, 对于函数式编程讲解的的开篇, 算是讲完了. 但这仅仅是开始, 对于函数式编程这样一种新的编程尝试, 还有很多值得学习和讨论的地方. 后续博主会继续深入探究 Java8 中针对函数式编程引入的一些方法类库, 以及这些新特性能给我们的编码带来哪些便利.
限于法力有限, 只能粗浅讲解; 欢迎挑刺, 不胜感激.
来源: http://www.bubuko.com/infodetail-3340790.html