Java函数式编程思维
Posted 中兴开发者社区
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java函数式编程思维相关的知识,希望对你有一定的参考价值。
目录
引言
一直以来,Java都被认为是一种面向对象的编程语言,“万事万物皆对象”的思想已经深入人心。但随着Java8的发布,一切看起来似乎有些改变。Lambda表达式和Stream的引入,让Java焕发了新的活力,它允许人们可以用函数式编程思维思考问题。本文主要介绍了函数式编程思想在Java中的应用。
指令式还是声明式?
先看一段代码:计算商品价格的最大值。
我们一般会这样实现:
int max = 0;
for(int price : prices) {
if (max < price) max = price; }
这就是典型的“指令式”(imperative)写法。它利用计算机的指令或者语法,告诉计算机一步步要做什么。
针对同样的功能,再看看另一种写法:
int max = prices.stream().reduce(0, Math::max);
这段简洁的代码就是“声明式”(declarative)写法。他更像是告诉计算机要实现什么样的功能,而不关注计算机内部如何处理。
如果熟悉软件设计原则的读者会发现,这正是“好莱坞原则”的使用,Tell,Don’t Ask!
换个角度看设计模式
在GoF的经典著作《设计模式》一书中,详细介绍了23种常见的设计模式。细心的读者可能会发现,书名下面还有一行小字:可复用面向对象软件的基础。也就是说它是用面向对象思想实现的。这么多年过去了,对设计模式的争论一直在进行,但这已不再重要,今天让我们以命令模式为例,从函数式的角度重新审视设计模式。
命令模式一般会对命令进行封装,对外提供接口供使用者调用。
先看一个例子:扫地机器人可以执行直行、左转、右转等指令操作。
定义指令接口和实现类:
// 命令接口
public interface Command {
void execute(); }
// 前进命令实现
public class forward implements Command {
public void execute() { System.out.println("go forward"); } }
// 右转命令实现
public class Right implements Command {
@Override public void execute() { System.out.println("go right"); } }
下面实现机器人:
public class Robot {
public static void move(Command... commands) {
for (Command command : commands) { command.execute(); } }
public static void main(String[] args) { Robot.move(new Forward(), new Right()); } }
虽然功能实现了,但有一个问题就是:创建的类太多!业务逻辑本来是要关注的焦点,但却被淹没在过多的类实现中。
我们看看函数式编程怎么实现?
因为Command接口中的execute()是一个无入参和无返回结果的方法,这让我们很自然的想起了Java内置的函数式接口Runnable,它也有一个同样签名的run()方法。虽然Runnable接口本来是用在多线程处理中的,但这里我们取巧的用在函数式编程中。
首先我们把Robot类的move()方法的入参替换为Runnable接口:
public static void flexibleMove(Runnable... commands) { Stream.of(commands).forEach(Runnable::run); }
这样一来,我们只需要在类方法中实现命令就可以了。
public static void forward() { System.out.println("go forward"); }
public static void right() { System.out.println("go right"); }
调用就变成了:
Robot.flexibleMove(Robot::forward, Robot::right);
这种实现减少了很多命令实现类,把焦点更多放在业务逻辑上。
设计连贯接口
连贯接口(Fluent Interface)是内部DSL一种常用的设计手法。它通常采用Builder设计模式让方法返回this对象,这样方法就能像链一样调用,形成连贯接口。
// 定义一个邮件发送
builderpublic class MailBuilder {
public MailBuilder from(final String address) {return this; }
public MailBuilder to(final String address) {return this; }
public MailBuilder subject(final String line) {return this; }
public MailBuilder body(final String message) {return this; }
public void send() { System.out.println("sending..."); } }
客户端调用为:
new MailBuilder() .from("ding.yi@zte.com.cn") .to("wxcop@zte.com.cn") .subject("article") .body("fp in java") .send();
这就是我们常见的连贯接口的使用方式。但它也有两个小问题:
new的方式让连贯接口的可读性降低。
这还是“指令式”的写法,先构造邮件体,再发送邮件。更好的语义是mailer.send(mail)的方式。
那能不能做到这两点呢?使用函数式接口是可以做到的。
在Java内置的函数式接口中,有一个叫Consumer的接口,可以用来接收参数进行消费,这和我们的意图正好相符。
代码实现如下:
public class Mailer {
private Mailer() {}
public Mailer from(final String address) { return this; }
public Mailer to(final String address) { return this; }
public Mailer subject(final String line) { return this; }
public Mailer body(final String message) { return this; }
public static void send(final Consumer<Mailer> block) {
final Mailer mailer = new Mailer(); block.accept(mailer); System.out.println("sending..."); } }
从上面的实现可以看出,构造函数变成了私有,send()方法变成了静态的,并且接收一个Consumer类型的block。
这样客户端调用就变成了:
Mailer.send(mail -> mail.from("ding.yi@zte.com.cn") .to("wxcop@zte.com.cn") .subject("article") .body("fp in java"));
这样的代码既保留了连贯接口,也更具有表现力。
使用资源
我们日常开发中经常会对资源进行操作,比如数据库连接,文件操作,锁操作等。这些对资源的操作有一个共性特点:先打开资源,然后对资源进行操作,最后关闭资源。代码通常会这样写:
resource.open();
try { doSomethingWith(resource); } finally { resource.close(); }
这是一段样板代码,看似简单,但在使用的过程中,总会有粗心的程序员忘记加上finally语句来释放资源,从而导致内存泄露。
那是否有一种方法能在操作完资源后自动关闭资源呢?
利用函数式接口是可以做到的,可以把Consumer接口传递到样板代码中,这样客户端只用关注对资源的操作处理就可以了,关闭操作会自动完成。
代码实现如下:
public static void handle(Consumer<Resource> consumer) { Resource resource = new Resource();
try { consumer.accept(resource); } finally { resource.close(); } }
这样在客户端使用的时候,就可以写成:
handle(resource -> doSomethinWith(resource));
这样再也不用担心使用资源后,忘记释放资源了!
延迟加载
延迟加载是函数式编程的一个重要特征,使用得当的话,可以很好的提升软件性能。
下面看一个例子:对一个耗时较长的操作,执行“与”操作。
先看看没有使用延迟加载的效果:
public class Evaluation {
public static boolean evaluate(final int value) { System.out.println("evaluating ..." + value);
try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
return value > 100; }
public static void eagerEvaluator(final boolean input1, final boolean input2) { System.out.println("eagerEvaluator called..."); System.out.println("accept?: " + (input1 && input2)); }
public static void main(String[] args) throws InterruptedException { eagerEvaluator(evaluate(1), evaluate(2)); } }
evaluate()操作是一个耗时操作,比如耗时20s,eagerEvaluator()方法传入的是两个布尔结果,在内部进行“与”操作,由于两个evaluate()方法必须都要执行完,所以总的操作时间至少需要40s。
我们知道,Java中的&&操作是一个短路操作,也就是说,如果第一个条件结果为false,就不再进行后续条件的判断,直接返回false。
我们是否能用上这个特性呢?如果传给evaluate()方法的参数是一个函数式接口,这样它就不会立即执行,而是等到真正调用的时候才执行,从而达到使用短路操作的效果。
下面是代码的实现:
public static void lazyEvaluator(final Supplier<Boolean> input1, final Supplier<Boolean> input2) { System.out.println("lazyEvaluator called..."); System.out.println("accept?: " + (input1.get() && input2.get())); }
传给lazyEvaluator()方法的是一个Supplier函数式接口,这样在调用的时候就可以写成:
lazyEvaluator(() -> evaluate(1), () -> evaluate(2));
这样传入的lambda表达式,只有在调用时才执行,由于第一个条件返回false,就执行了短路操作,直接返回了结果。这样操作时间缩短了一半,性能提升明显。
结语
虽然Java引入了函数式编程元素,但也许Java终究不可能成为一门函数式编程语言,但这并不能妨碍我们使用函数式编思维解决问题。世界上的问题终究不是对立的,或许把面向对象和函数式编程结合起来使用,可能会取得意想不到的效果。
以上是关于Java函数式编程思维的主要内容,如果未能解决你的问题,请参考以下文章