如何更简捷地在 Java 中进行函数式编程
在Java
编码过程中考虑声明式地使用函数式编程。
Java
开发人员已经习惯于使用命令式和面向对象方式的编程范式,这时因为 Java
语言天生就支持这些特性。 在 Java 8
中,我们可以使用一系列更加强大的新特性和语法。 函数式编程自被提出以来已经有几十年了,它通常比面向对象编程更简洁、更有表现力、更不容易出错并且更容易并行化。 因此,有充分的理由在 Java
程序中引入函数式的编程特性。 但是,以函数式风格编程需要对现有代码设计进行一些更改。
关于这个系列
Java 8
是这门语言自诞生以来最重要的更新 —— 它包含了更丰富的新功能。 本系列的作者兼教育家 Venkat Subramaniam
提供了一种使用 Java 8
的常用方法:使你通过简短的探索,以重新思考那些被认为习以为常的Java
约定,同时思考如何逐渐地将新技术和语法集成到您的程序中。
我发现在编码时通过声明式地思考而不是命令式,可以让我们更轻松地过渡到这种更具功能性的编程风格中,在 Java 8 idioms series系列的第一篇文章中,我解释了命令式、声明式和函数式编程之间的区别和共同点。现在我将向您展示如何使用声明式的思维将函数式编程运用到你的实际编程中。
命令式的风格
习惯于使用命令式编程风格的开发人员喜欢告诉程序该做什么以及如何做。 这是一个简单的例子:
目录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()
方法首先初始化一个标识变量found
,这种变量也被称为garbage
变量,有些开发者经常将这些变量名称为f
、t
或者temp
,表示这些变量只是用来表示中间状态,它们本不该存在,这里则是将其命名位found
。
接下来程序遍历names
列表的每一个元素。它检查这些元素是否与Nemo
相匹配,如果匹配则将found
置为true
并跳出循环,
上面的代码风格可以说是每一位java开发者都熟悉的命令式风格,你定义了程序的每一步,包括迭代哪些元素,如何比较,匹配时执行哪些操作,何时跳出循环等。命令式风格使得你能对程序拥有绝对的控制,这看起来很不错。另一方面,在许多场景下,你可以以此来减少工作量。
声明式风格
在声明式编程下,你仍然需要告诉程序需要做什么,但是实现的细节交给底层的函数库。让我们使用声明式风格来重写上面的findNemo
方法:
以声明式的风格实现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()
方法来实现了功能。总的来说,虽然你仍然需要告诉程序怎么做:检查集合中是否有我们想要的值,但是实现的细节已经交给底层函数库。
在命令式的代码中,你通过指定遍历操作来指示程序按你的想法来实现功能,但在声明式的版本中,你不用关心实现的过程如何运作,只需关心它返回的结果,根据返回的结果来执行你想要的操作,看起来更加省心。
寻来自己以声明式的风格编程和思考将大大简化你向函数式编程转变的过程。这是因为函数式编程构建于声明式风格之上,而声明式思维提供了从命令式编程到函数式编程的过渡。
函数式风格
虽然函数式编程的风格总是声明式的,但这不代表简单地使用声明式编程就是函数式编程。这是因为函数式编程还需要与高阶函数使用相结合。下面的图示表明了命令式、声明式和函数式编程之间的关系。
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
中。
声明式思维要求你在设计这个方法时从“如何设计”转换成“为什么这样设计”中。当调用方法 incrementPageVisit()
时,您希望将给定页面的计数初始化为 1 或者是将其值进行自增。 这就是“为什么这样设计”。
下一步就是在JDK库中寻找一个库方法,这个库方法可以遍历整个HashMap
,并且完成我们要实现的功能。
很明显,这个库函数就是merge()
,下面的目录4将使用函数式的风格来实现功能,但是需要注意,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
中时,创建它并将默认值作为它的初始值。第三个参数是lambda
表达式,它是一个接收旧值和新值的函数。lambda
表达式返回它的参数之和,以此来实现增加指定网页的访问计数。 (编者注:感谢 István Kovács 指出并更正了代码中的错误。)
将目录4
中 incrementPageVisit()
方法的单行代码与目录3
中的多行代码进行对比。虽然目录4
中的代码只是函数式风格的一个简单示例,但是其中声明式的编程思维更有助于提高我们水平。
总结
在 Java
程序设计中采用函数式编程思想有很多好处:代码更简洁、表达能力更强、修改部分更少、更容易代码的并行化,并且它的代码通常比面向对象编程的代码更容易理解。难点在于,如何将你的思维从绝大多数开发人员所熟悉的命令式编程风格转变为声明式思维。
虽然编写函数式风格的代码并不简单,但是在这过程中,你的关注点将实现从 “程序怎么实现”到”你希望它怎么实现”的巨大飞跃。 通过让底层函数库来代替你管理程序的执行,您将逐渐地、直观地了解到作为函数式编程核心的高阶函数的作用。