[译]通往 Java 函数式编程的捷径
以声明式的思想在你的 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)。开发者经常会给予某些变量一个临时性的名字,例如 f、t、temp 以表明它们根本不应该存在。在本例中,这些变量应该被命名为 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 修正了代码错误)
将清单 4 的 incrementPageVisit() 方法中的单行代码与清单 3 中的多行代码进行比较。虽然清单 4 中的程序是函数式编程风格的一个例子,但通过声明性地思想去思考问题帮助能够我们实现飞跃。
总结
在 Java 程序中采用函数式编程技术和语法有很多好处:代码更简洁,更富有表现力,移动部分更少,实现并行化更容易,并且通常比面向对象的代码更易理解。 目前面临的挑战是,如何将你的思维从绝大多数开发人员所熟悉的命令式编程风格转变为以声明式的方式进行思考。
虽然函数式编程并没有那么简单或直接,但是你可以学习专注于你希望程序做什么而不是如何做这件事,来取得巨大的飞跃。通过允许底层函数库管理执行,你将逐渐直观地了解用于构建函数式编程模块的高阶函数。
- 本文永久链接:https://github.com/xitu/gold-miner/blob/master/TODO1/an-easier-path-to-functional-programming-in-java.md
- 译者:maoqyhz
- 校对者:satansk、lihanxiang
- 如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。
[译]通往 Java 函数式编程的捷径的更多相关文章
- paip.提升效率---filter map reduce 的java 函数式编程实现
#paip.提升效率---filter map reduce 的java 函数式编程实现 ======================================================= ...
- 《深入理解Java函数式编程》系列文章
Introduction 本系列文将帮助你理解Java函数式编程的用法.原理. 本文受启发于JavaOne 2016关于Lambda表达式的相关主题演讲Lambdas and Functional P ...
- Java函数式编程原理以及应用
一. 函数式编程 Java8所有的新特性基本基于函数式编程的思想,函数式编程的带来,给Java注入了新鲜的活力. 下面来近距离观察一下函数式编程的几个特点: 函数可以作为变量.参数.返回值和数据类型. ...
- Java函数式编程:一、函数式接口,lambda表达式和方法引用
Java函数式编程 什么是函数式编程 通过整合现有代码来产生新的功能,而不是从零开始编写所有内容,由此我们会得到更加可靠的代码,并获得更高的效率 我们可以这样理解:面向对象编程抽象数据,函数式编程抽象 ...
- Java函数式编程:二、高阶函数,闭包,函数组合以及柯里化
承接上文:Java函数式编程:一.函数式接口,lambda表达式和方法引用 这次来聊聊函数式编程中其他的几个比较重要的概念和技术,从而使得我们能更深刻的掌握Java中的函数式编程. 本篇博客主要聊聊以 ...
- Java函数式编程:三、流与函数式编程
本文是Java函数式编程的最后一篇,承接上文: Java函数式编程:一.函数式接口,lambda表达式和方法引用 Java函数式编程:二.高阶函数,闭包,函数组合以及柯里化 前面都是概念和铺垫,主要讲 ...
- Java函数式编程和lambda表达式
为什么要使用函数式编程 函数式编程更多时候是一种编程的思维方式,是种方法论.函数式与命令式编程的区别主要在于:函数式编程是告诉代码你要做什么,而命令式编程则是告诉代码要怎么做.说白了,函数式编程是基于 ...
- 精练代码:一次Java函数式编程的重构之旅
摘要:通过一次并发处理数据集的Java代码重构之旅,展示函数式编程如何使得代码更加精练. 难度:中级 基础知识 在开始之前,了解"高阶函数"和"泛型"这两个概念 ...
- Java 函数式编程(Lambda表达式)与Stream API
1 函数式编程 函数式编程(Functional Programming)是编程范式的一种.最常见的编程范式是命令式编程(Impera Programming),比如面向过程.面向对象编程都属于命令式 ...
随机推荐
- Pycharm2017常用快捷键
Pycharm 的快捷键可以在[文件]-[设置]中自定义(见上图). 下方是根据网上资料整理的官方默认快捷键设置. 常用快捷键 Ctrl + / 行注释/取消行注释 Tab / Shift + Tab ...
- 07-OpenLDAP密码审计
OpenLDAP密码审计 阅读视图 密码审计的作用 操作实践 1. 密码审计的作用 开启密码审计的功能主要用于记录OpenLDAP用户修改密码,以及密码审计. 2. 操作实践 开启密码审计模块并配置密 ...
- 理解inode 以及 软链接和硬链接概念区分
inode简单理解 本文来源自网络文章,并针对文章内容加以批注和修改.希望能帮到你! 一. 磁盘设备 说到inode,首先必须要提及下<操作系统>中磁盘存储器的管理一节.磁盘设备是一种相当 ...
- linux 系统中用root切换到普通用户时显示的异常如-bash-4.1$
解决办法: 其实就是普通用户的家目录缺少配置文件导致: [root@xxx ~]# su - oldboy -bash-4.1$ 原因是普通用户的家目录下缺少文件: [root@xxx ~]# ls ...
- 07LaTeX学习系列之---Latex源文件的结构
目录 目录: (一)Latex源文件的结构: (二)基础语法: 2.空行: 3.document: 4.数学公式: 5.latex的文件格式分类: 目录: 本系列是有关LaTeX的学习系列,共计19篇 ...
- python基础 - 变量与运算符
变量与运算符 变量 定义一个变量 a = [1,2,3,4,5,6] print(a) # [1,2,3,4,5,6] 变量命名要求: 首字母不能是数字 只能包含字符数字下划线 不能是关键字 type ...
- 常见的web攻击方式
跨站脚本攻击(XSS) 概述 跨站脚本攻击(XSS,Cross-site scripting),指攻击者在网页中嵌入恶意脚本程序,是最常见和基本的攻击WEB网站的方法.攻击者在网页上发布包含攻击性代码 ...
- 通过SQL直接插入、修改ArcGIS SDE空间表中的数据
基于Arcgis Server 10.1 +Oracle 11g环境测试 ArcGIS SDE ? 1 2 INSERT INTO CAMERA_INFO(OBJECTID,ID,SHAPE) ...
- Android应用启动、退出分析
http://www.jianshu.com/p/72059201b10a §AMS和应用进程 §应用启动流程 §应用退出流程 §启动.退出消息 AMS和应用进程 应用进程 <- 系统管理 &l ...
- PHP开发小技巧③—实现多维数组转化为一维数组
在平常的项目开发中我们多会用到让多维数组转化为一维数组的情况,但是很多Programmer不会将其进行转化,也有些没有想到很好的算法然后经过乱起八糟的运算方式将其勉强转化好,但是所写的程序代码冗余非常 ...