以声明式的思想在你的 Java 程序中使用函数式编程技术

Java™ 开发人员习惯于面向命令式和面向对象的编程,因为这些特性自 Java 语言首次发布以来一直受到支持。在 Java 8 中,我们获得了一组新的强大的函数式特性和语法。函数式编程已经存在了数十年,与面向对象编程相比,函数式编程通常更加简洁和达意,不易出错,并且更易于并行化。所以有很好的理由将函数式编程特性引入到 Java 程序中。尽管如此,在使用函数式特性进行编程时,就如何设计你的代码这一点上需要进行一些改变。

关于本文

Java 8 是 Java 语言自诞生以来最重要的更新,它包含如此多的新特性,以至于你可能想知道应该从哪开始了解它。在本系列中,身为作家和教育家的 Venkat Subramaniam 提供了一种符合 Java 语言习惯的 Java 8 学习方式。邀请你进行简短的探索后,重新思考你认为理所当然的 Java 一贯用法和规范,同时逐渐将新技术和语法集成到你的程序中去。

我认为,以声明式的思想而不是命令式的思想来编程,可以更加轻松地向更加函数化的编程风格过渡。在 Java 8 idioms series 这个系列的第一篇文章中,我解释了命令式、声明式和函数式编程风格之间的异同。然后,我将向你展示如何使用声明式的思想逐渐将函数式编程技术集成到你的 Java 程序中。

命令式风格(面向过程)

受命令式编程风格训练的开发者习惯于告诉程序需要做什么以及如何去做。这里是一个简单的例子:

清单 1. 以命令式风格编写的 findNemo 方法

import java.util.*;

public class FindNemo {
public static void main(String[] args) {
List<String> names =
Arrays.asList("Dory", "Gill", "Bruce", "Nemo", "Darla", "Marlin", "Jacques"); findNemo(names);
} public static void findNemo(List<String> names) {
boolean found = false;
for(String name : names) {
if(name.equals("Nemo")) {
found = true;
break;
}
} if(found)
System.out.println("Found Nemo");
else
System.out.println("Sorry, Nemo not found");
}
}

方法 findNemo() 首先初始化一个可变变量 flag,也称为垃圾变量(garbage variable)。开发者经常会给予某些变量一个临时性的名字,例如 fttemp 以表明它们根本不应该存在。在本例中,这些变量应该被命名为 found

接下来,程序会循环遍历给定的 names 列表,每次都会判断当前遍历的值是否和待匹配值相同。在这个例子中,待匹配值为 Nemo,如果遍历到的值匹配,程序会将标志位设为 true,并执行流程控制语句 "break" 跳出循环。

这是对于广大 Java 开发者最熟悉的编程风格 —— 命令式风格的程序,因此你可以定义程序的每一步:你告诉程序遍历每一个元素,和待匹配值进行比较,设置标志位,以及跳出循环。命令式编程风格让你可以完全控制程序,有的时候这是一件好事。但是,换个角度来看,你做了很多机器可以独立完成的工作,这势必导致生产力下降。因此,有的时候,你可以通过少做事来提高生产力。

声明式风格

声明式编程意味着你仍然需要告诉程序需要做什么,但是你可以将实现细节留给底层函数库。让我们看看使用声明式编程风格重写清单 1 中的 findNemo 方法时会发生什么:

清单 2. 以声明式风格编写的 findNemo 方法

public static void findNemo(List<String> names) {
if(names.contains("Nemo"))
System.out.println("Found Nemo");
else
System.out.println("Sorry, Nemo not found");
}

首先需要注意的是,此版本中没有任何垃圾变量。你也不需要在遍历集合中浪费精力。相反,你只需要使用内建的 contains() 方法来完成这项工作。你仍然要告诉程序需要做什么,集合中是否包含我们正在寻找的值,但此时你已经将细节交给底层的方法来实现了。

在命令式编程风格的例子中,你控制了遍历的流程,程序可以完全按照指令进行;在声明式的例子中,只要程序能够完成工作,你完全不需要关注它是如何工作的。contains() 方法的实现可能会有所不同,但只要结果符合你的期望,你就会对此感到满意。更少的工作能够得到相同的结果。

训练自己以声明式的编程风格来进行思考将更加轻松地向更加函数化的编程风格过渡。原因在于,函数式编程风格是建立在声明式风格之上的。声明式风格的思维可以让你逐渐从命令式编程转换到函数式编程。

函数式编程风格

虽然函数式风格的编程总是声明式的,但是简单地使用声明式风格编程并不等同与函数式编程。这是因为函数式编程时将声明式编程和高阶函数结合在了一起。图 1 显示了命令式,声明式和函数式编程风格之间的关系。

图 1. 命令式、声明式和函数式编程风格之间的关系

Java 中的高阶函数

在 Java 中,你可以将对象传递给方法,在方法中创建对象,也可以从方法中返回对象。同时你也可以用函数做相同的事情。也就是说,你可以将函数传递给方法,在方法中创建函数,也可以从方法中返回函数。

在这种情况下,方法是类的一部分(静态或实例),但是函数可以是方法的一部分,并且不能有意地与类或实例相关联。一个可以接收、创建、或者返回函数的方法或函数称之为高阶函数

一个函数式编程的例子

采用新的编程风格需要改变你对程序的看法。这是一个从简单例子的练习开始,到构建更加复杂程序的过程。

清单 3. 命令式编程风格下的 Map

import java.util.*;

public class UseMap {
public static void main(String[] args) {
Map<String, Integer> pageVisits = new HashMap<>(); String page = "https://agiledeveloper.com"; incrementPageVisit(pageVisits, page);
incrementPageVisit(pageVisits, page); System.out.println(pageVisits.get(page));
} public static void incrementPageVisit(Map<String, Integer> pageVisits, String page) {
if(!pageVisits.containsKey(page)) {
pageVisits.put(page, 0);
} pageVisits.put(page, pageVisits.get(page) + 1);
}
}

清单 3 中,main() 函数创建了一个 HashMap 来保存网站访问次数。同时,incrementPageVisit() 方法增加了每次访问给定页面的计数。我们将聚焦此方法。

以命令式编程风格写的 incrementPageVisit() 方法:它的工作是为给定页面增加一个计数,并存储在 Map 中。该方法不知道给定页面是否已经有计数值,所以会先检查计数值是否存在,如果不存在,会为该页面插入一个值为"0"的计数值。然后再获取该计数值,递增它,并将新的计数值存储在 Map 中。

以声明式的方式思考需要你将方法的设计从 "how" 转变到 "what"。当 incrementPageVisit() 方法被调用时,你需要将给定的页面计数值初始化为 1 或者计数值加 1。这就是 what

因为你是通过声明式编程的,那么下一步就是在 JDK 库中寻找可以完成这项工作且实现了 Map 接口的方法。换言之,你需要找到一个知道如何完成你指定任务的内建方法。

事实证明 merge() 方法非常适合你的而目的。清单 4 使用新的声明式方法对清单 3 中的 incrementPageVisit() 方法进行修改。但是,在这种情况下,你不仅仅只是选择更智能的方法来写出更具声明性风格的代码,因为 merge() 是一个更高阶的函数。所以说,新的代码实际上是一个体现函数式风格的很好的例子:

清单 4. 函数式编程风格下的 Map

public static void incrementPageVisit(Map<String, Integer> pageVisits, String page) {
pageVisits.merge(page, 1, (oldValue, value) -> oldValue + value);
}

在清单 4 中,page 作为第一个参数传递给 merge():map 中键对应的值将会被更新。第二个参数作为初始值,如果 Map 中不存在指定键的值,那么该值将会赋值给 Map 中键对应的值(在本例中为"1")。第三个参数为一个 lambda 表达式,接受当前 Map 中键对应的值和该函数中第二个参数对应的值作为参数。lambda 表达式返回其参数的总和,实际上增加了计数值。(编者注:感谢 István Kovács 修正了代码错误)

清单 4incrementPageVisit() 方法中的单行代码与清单 3 中的多行代码进行比较。虽然清单 4 中的程序是函数式编程风格的一个例子,但通过声明性地思想去思考问题帮助能够我们实现飞跃。

总结

在 Java 程序中采用函数式编程技术和语法有很多好处:代码更简洁,更富有表现力,移动部分更少,实现并行化更容易,并且通常比面向对象的代码更易理解。 目前面临的挑战是,如何将你的思维从绝大多数开发人员所熟悉的命令式编程风格转变为以声明式的方式进行思考。

虽然函数式编程并没有那么简单或直接,但是你可以学习专注于你希望程序做什么而不是如何做这件事,来取得巨大的飞跃。通过允许底层函数库管理执行,你将逐渐直观地了解用于构建函数式编程模块的高阶函数。

[译]通往 Java 函数式编程的捷径的更多相关文章

  1. paip.提升效率---filter map reduce 的java 函数式编程实现

    #paip.提升效率---filter map reduce 的java 函数式编程实现 ======================================================= ...

  2. 《深入理解Java函数式编程》系列文章

    Introduction 本系列文将帮助你理解Java函数式编程的用法.原理. 本文受启发于JavaOne 2016关于Lambda表达式的相关主题演讲Lambdas and Functional P ...

  3. Java函数式编程原理以及应用

    一. 函数式编程 Java8所有的新特性基本基于函数式编程的思想,函数式编程的带来,给Java注入了新鲜的活力. 下面来近距离观察一下函数式编程的几个特点: 函数可以作为变量.参数.返回值和数据类型. ...

  4. Java函数式编程:一、函数式接口,lambda表达式和方法引用

    Java函数式编程 什么是函数式编程 通过整合现有代码来产生新的功能,而不是从零开始编写所有内容,由此我们会得到更加可靠的代码,并获得更高的效率 我们可以这样理解:面向对象编程抽象数据,函数式编程抽象 ...

  5. Java函数式编程:二、高阶函数,闭包,函数组合以及柯里化

    承接上文:Java函数式编程:一.函数式接口,lambda表达式和方法引用 这次来聊聊函数式编程中其他的几个比较重要的概念和技术,从而使得我们能更深刻的掌握Java中的函数式编程. 本篇博客主要聊聊以 ...

  6. Java函数式编程:三、流与函数式编程

    本文是Java函数式编程的最后一篇,承接上文: Java函数式编程:一.函数式接口,lambda表达式和方法引用 Java函数式编程:二.高阶函数,闭包,函数组合以及柯里化 前面都是概念和铺垫,主要讲 ...

  7. Java函数式编程和lambda表达式

    为什么要使用函数式编程 函数式编程更多时候是一种编程的思维方式,是种方法论.函数式与命令式编程的区别主要在于:函数式编程是告诉代码你要做什么,而命令式编程则是告诉代码要怎么做.说白了,函数式编程是基于 ...

  8. 精练代码:一次Java函数式编程的重构之旅

    摘要:通过一次并发处理数据集的Java代码重构之旅,展示函数式编程如何使得代码更加精练. 难度:中级 基础知识 在开始之前,了解"高阶函数"和"泛型"这两个概念 ...

  9. Java 函数式编程(Lambda表达式)与Stream API

    1 函数式编程 函数式编程(Functional Programming)是编程范式的一种.最常见的编程范式是命令式编程(Impera Programming),比如面向过程.面向对象编程都属于命令式 ...

随机推荐

  1. python 定时修改数据库

    当需要定时修改数据库时,一般我们都选择起一个定时进程去改库.如果将这种定时任务写入业务中,写成一个接口呢,定时进程显得有些不太合适?如果需要定时修改100次数据库,常规做法会启动100个进程,虽然这种 ...

  2. scrapy简单分布式爬虫

    经过一段时间的折腾,终于整明白scrapy分布式是怎么个搞法了,特记录一点心得. 虽然scrapy能做的事情很多,但是要做到大规模的分布式应用则捉襟见肘.有能人改变了scrapy的队列调度,将起始的网 ...

  3. CSS| 框模型-border

    CSS 边框属性

  4. EntityFramework Code-First 简易教程(五)-------领域类配置

    前言:在前篇中,总是把领域类(Domain Class)翻译成模型类,因为我的理解它就是一个现实对象的抽象模型,不知道对不对.以防止将来可能的歧义,这篇开始还是直接对Domain Class直译. 前 ...

  5. 遇到电脑IP地址冲突了怎么解决

    由于路由器是自动分配IP地址的,如果多个设备设置的是IP地址自动获取,就会出现IP地址冲突的情况当局域网内有相同IP,并且该机器启动了防火墙,那就没办法自动更新到下一个IP的地址了,所以此时发生了冲突 ...

  6. SAP SD模块功能构成

  7. Sql 注入详解:宽字节注入+二次注入

    sql注入漏洞 原理:由于开发者在编写操作数据库代码时,直接将外部可控参数拼接到sql 语句中,没有经过任何过滤就直接放入到数据库引擎中执行了. 攻击方式: (1) 权限较大时,直接写入webshel ...

  8. 第 16 章 C 预处理器和 C 库(string.h 库中的 memcpy() 和 memmove())

    /*----------------------------------------- mems.c -- 使用 memcpy() 和 memmove() ---------------------- ...

  9. DFS普及组常用模板简单整理

    一些普及组会用到的DFS模板,其他的DFS我感觉普及组不会用到所以暂且搁着,等之后有时间了再细写w (至于我为什么最近不写TG相关只写最基础的PJ的内容,请戳这里了解) dfs各种模板big集合 1. ...

  10. python scrapy爬虫框架概念介绍(个人理解总结为一张图)

    python的scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架   python和scrapy的安装就不介绍了,资料很多 这里我个人总结一下,能更加快理解scrapy和快速上手一个简 ...