java 函数式编程
欢迎回到由两部分组成的教程,介绍Java上下文中的函数式编程。 在Java开发人员的函数式编程(第1部分)中 ,我使用JavaScript实例开始使用五种函数式编程技术 :纯函数,高阶函数,惰性求值,闭包和currying。 用JavaScript展示这些示例,使我们能够以更简单的语法关注技术,而无需了解Java更复杂的功能编程功能。
在第2部分中,我们将使用Java 8之前的Java代码来重新研究这些技术。您将看到,该代码是有功能的,但是编写或阅读并不容易。 您还将了解到新的函数式编程功能,这些功能已完全集成到Java 8的Java语言中。 即lambda,方法引用,功能接口和Streams API。
在本教程中,我们将回顾第1部分中的示例,以了解JavaScript和Java示例之间的比较。 您还将看到当我使用lambda和方法引用等功能语言功能更新某些Java 8之前的示例时会发生什么。 最后,本教程包括一个动手练习,旨在帮助您练习功能性思维 ,您可以通过将一段面向对象的Java代码转换为其功能等效的方法来进行操作。
许多开发人员没有意识到这一点,但是可以在Java 8之前用Java编写功能性程序。为了全面了解Java中的功能性编程,让我们快速回顾一下Java 8之前的功能性编程功能。有了这些,您可能会对Java 8中引入的新功能(例如lambda和函数接口)如何简化Java的函数式编程方法有更多的了解。
即使在Java 8中对功能编程进行了改进,Java仍然是一种命令式,面向对象的编程语言。 它缺少范围类型和其他功能,使其更具功能性。 名词性类型也困扰着Java,这是每个类型都必须有一个名称的规定。 尽管有这些限制,但是使用Java功能的开发人员仍然可以从编写更简洁,可重用和可读的代码中受益。
匿名内部类以及接口和闭包是三个较旧的功能,它们支持较旧版本的Java中的函数式编程:
在以下各节中,我们将回顾使用Java语法的第1部分中介绍的五种技术。 您将看到在Java 8之前如何实现这些功能性技术。
清单1将源代码呈现给示例应用程序DaysInMonth
,该应用程序是使用匿名内部类和功能接口编写的。 该应用程序演示了如何编写纯函数,该函数在Java早于Java 8之前就已经可以在Java中实现。
interface Function
{
R apply(T t);
}
public class DaysInMonth
{
public static void main(String[] args)
{
Function dim = new Function()
{
@Override
public Integer apply(Integer month)
{
return new Integer[] { 31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31 }[month];
}
};
System.out.printf("April: %d%n", dim.apply(3));
System.out.printf("August: %d%n", dim.apply(7));
}
}
清单1中的泛型Function
接口描述了一个具有类型T
的单个参数和类型R
的返回类型的函数。 Function
接口声明一个R apply(T t)
方法,该方法将该函数应用于给定参数。
main()
方法实例化一个实现Function
接口的匿名内部类。 apply()
方法取消对month
装箱操作,并使用它对month
的天数数组进行索引。 返回此索引处的整数。 (为简单起见,我忽略了leap年。)
main()
接下来通过调用apply()
返回两次4月和8月的天数来执行此函数两次。 这些计数随后被打印。
我们已经设法创建了一个函数,以及一个纯函数! 回想一下, 纯函数仅取决于其参数,而没有外部状态。 没有副作用。
如下编译清单1:
javac DaysInMonth.java
运行生成的应用程序,如下所示:
java DaysInMonth
您应该观察以下输出:
April: 30
August: 31
接下来,我们将研究高阶函数,也称为一等函数。 请记住, 高阶函数会接收函数参数和/或返回函数结果。 Java将函数与方法相关联,该方法在匿名内部类中定义。 此类的实例传递给另一个Java方法,该方法用作高阶函数。 以下面向文件的代码片段演示了如何将函数传递给高阶函数:
File[] txtFiles =
new File(".").listFiles(new FileFilter()
{
@Override
public boolean accept(File pathname)
{
return pathname.getAbsolutePath().endsWith("txt");
}
});
此代码段将基于java.io.FileFilter
功能接口的函数传递给java.io.File
类的File[] listFiles(FileFilter filter)
方法,告诉它仅返回那些带有txt
扩展名的文件。
清单2显示了另一种使用Java中高阶函数的方法。 在这种情况下,代码将比较器函数传递给sort()
高阶函数以进行升序排序,并将第二个比较器函数传递给sort()
以进行降序排序。
import java.util.Comparator;
public class Sort
{
public static void main(String[] args)
{
String[] innerplanets = { "Mercury", "Venus", "Earth", "Mars" };
dump(innerplanets);
sort(innerplanets, new Comparator()
{
@Override
public int compare(String e1, String e2)
{
return e1.compareTo(e2);
}
});
dump(innerplanets);
sort(innerplanets, new Comparator()
{
@Override
public int compare(String e1, String e2)
{
return e2.compareTo(e1);
}
});
dump(innerplanets);
}
static void dump(T[] array)
{
for (T element: array)
System.out.println(element);
System.out.println();
}
static void sort(T[] array, Comparator cmp)
{
for (int pass = 0; pass < array.length - 1; pass++)
for (int i = array.length - 1; i > pass; i--)
if (cmp.compare(array[i], array[pass]) < 0)
swap(array, i, pass);
}
static void swap(T[] array, int i, int j)
{
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
清单2导入了java.util.Comparator
功能接口,该接口描述了可以对任意但相同类型的两个对象执行比较的函数。
该代码的两个重要部分是sort()
方法(实现Bubble Sort算法)和main()
方法中的sort()
调用。 尽管sort()
远不能实现功能,但它演示了一个高阶函数,该函数接收一个函数(比较器)作为参数。 它通过调用其compare()
方法来执行此功能。 该函数的两个实例在main()
两个sort()
调用中传递。
如下编译清单2:
javac Sort.java
运行生成的应用程序,如下所示:
java Sort
您应该观察以下输出:
Mercury
Venus
Earth
Mars
Earth
Mars
Mercury
Venus
Venus
Mercury
Mars
Earth
惰性求值是Java 8所不具备的另一种功能编程技术,该技术会延迟对表达式的求值,直到需要其值为止。 在大多数情况下,Java会急切地评估绑定到变量的表达式。 Java支持以下特定语法的惰性求值:
&&
和||
运算符,当左操作数为false( &&
)或true( ||
)时,将不会计算其右操作数。 ?:
运算符,它评估一个布尔表达式,然后根据布尔表达式的true / false值仅评估两个替代表达式(兼容类型)中的一个。 函数式编程鼓励进行面向表达式的编程,因此您将要避免使用尽可能多的语句。 例如,假设您要用ifThenElse()
方法替换Java的if
- else
语句。 清单3显示了第一次尝试。
public class EagerEval
{
public static void main(String[] args)
{
System.out.printf("%d%n", ifThenElse(true, square(4), cube(4)));
System.out.printf("%d%n", ifThenElse(false, square(4), cube(4)));
}
static int cube(int x)
{
System.out.println("in cube");
return x * x * x;
}
static int ifThenElse(boolean predicate, int onTrue, int onFalse)
{
return (predicate) ? onTrue : onFalse;
}
static int square(int x)
{
System.out.println("in square");
return x * x;
}
}
清单3所限定ifThenElse()
方法,该方法采用布尔谓词和一对整数,返回onTrue
整数时谓词为真 ,且onFalse
整数否则。
清单3还定义了cube()
和square()
方法。 这些方法分别对一个整数进行立方和平方运算并返回结果。
main()
方法调用ifThenElse(true, square(4), cube(4))
,后者应仅调用square(4)
,随后调用ifThenElse(false, square(4), cube(4))
,后者应调用只有cube(4)
。
编译清单3,如下所示:
javac EagerEval.java
运行生成的应用程序,如下所示:
java EagerEval
您应该观察以下输出:
in square
in cube
16
in square
in cube
64
输出显示,无论布尔表达式如何,每个ifThenElse()
调用都会导致两个方法都执行。 我们无法利用?:
运算符的惰性,因为Java急切地评估方法的参数。
尽管无法避免对方法参数进行急切的评估,但我们仍然可以利用?:
的惰性评估来确保仅调用square()
或cube()
。 清单4显示了如何。
interface Function
{
R apply(T t);
}
public class LazyEval
{
public static void main(String[] args)
{
Function square = new Function()
{
{
System.out.println("SQUARE");
}
@Override
public Integer apply(Integer t)
{
System.out.println("in square");
return t * t;
}
};
Function cube = new Function()
{
{
System.out.println("CUBE");
}
@Override
public Integer apply(Integer t)
{
System.out.println("in cube");
return t * t * t;
}
};
System.out.printf("%d%n", ifThenElse(true, square, cube, 4));
System.out.printf("%d%n", ifThenElse(false, square, cube, 4));
}
static R ifThenElse(boolean predicate, Function onTrue,
Function onFalse, T t)
{
return (predicate ? onTrue.apply(t) : onFalse.apply(t));
}
}
清单4通过声明此方法接收一对Function
参数将ifThenElse()
转换为高阶函数。 尽管将这些参数传递给ifThenElse()
时会急切地求值,但?:
运算符仅使这些函数之一执行(通过apply()
)。 编译和运行应用程序时,您可以同时看到急切评估和惰性评估。
编译清单4,如下所示:
javac LazyEval.java
运行生成的应用程序,如下所示:
java LazyEval
您应该观察以下输出:
SQUARE
CUBE
in square
16
in cube
64
尼尔·福特(Neal Ford)的“ 懒惰,第1部分:探索Java的懒惰评估 ”提供了对懒惰评估的更多见解。 作者介绍了一个基于Java的惰性迭代器以及几个面向惰性的Java框架。
匿名内部类实例与闭包关联。 为了可访问,必须将外部作用域变量声明为final
或(从Java 8开始)声明为有效final (在初始化后表示未修改)。 考虑清单5。
interface Function
{
R apply(T t);
}
public class PartialAdd
{
Function add(final int x)
{
Function partialAdd = new Function()
{
@Override
public Integer apply(Integer y)
{
return y + x;
}
};
return partialAdd;
}
public static void main(String[] args)
{
PartialAdd pa = new PartialAdd();
Function add10 = pa.add(10);
Function add20 = pa.add(20);
System.out.println(add10.apply(5));
System.out.println(add20.apply(5));
}
}
清单5与我以前在JavaScript中介绍的闭包在Java中等效(请参阅第1部分 ,清单8)。 此代码声明了add()
高阶函数,该函数返回用于执行add()
函数的部分应用的函数。 apply()
方法访问add()
外部范围内的变量x
,该变量必须在Java 8之前声明为final
。该代码的行为与JavaScript等效。
编译清单5,如下所示:
javac PartialAdd.java
运行生成的应用程序,如下所示:
java PartialAdd
您应该观察以下输出:
15
25
您可能已经注意到,清单5中的PartialAdd
演示了闭包。 它还演示了curring ,这是将多参数函数的求值转换为等效单参数函数序列的求值的一种方法。 清单5中的pa.add(10)
和pa.add(20)
都返回一个闭包,该闭包记录一个操作数(分别为10
或20
)和一个执行加法的函数-第二个操作数( 5
)通过add10.apply(5)
传递add10.apply(5)
或add20.apply(5)
。
通过Currying,我们可以一次评估一个函数自变量,从而生成一个新函数,每步少一个参数。 例如,在PartialAdd
应用程序中,我们使用以下函数:
f(x, y) = x + y
我们可以同时应用两个参数,得出以下结果:
f(10, 5) = 10 + 5
但是,使用currying时,我们仅应用第一个参数,得出以下结果:
f(10, y) = g(y) = 10 + y
现在,我们有一个函数g
,它仅包含一个参数。 这是我们调用apply()
方法时将要评估的函数。
名称PartialAdd
代表add()
函数的部分应用 。 它不代表部分添加。 固化是关于执行部分功能。 这与执行部分计算无关。
您可能会对我使用“部分应用程序”一词感到困惑,尤其是因为我在第1部分中说过,currying与“ 部分应用程序”不同 ,后者是将多个参数固定到一个函数,产生另一个函数的过程较小的团结 。 使用部分应用程序时,可以产生带有多个参数的函数,但是使用currying时,每个函数必须仅具有一个参数。
清单5给出了Java 8之前的基于Java的小例子。现在考虑清单6中的CurriedCalc
应用程序。
interface Function
{
R apply(T t);
}
public class CurriedCalc
{
public static void main(String[] args)
{
System.out.println(calc(1).apply(2).apply(3).apply(4));
}
static Function>>
calc(final Integer a)
{
return new Function>>()
{
@Override
public Function>
apply(final Integer b)
{
return new Function>()
{
@Override
public Function
apply(final Integer c)
{
return new Function()
{
@Override
public Integer apply(Integer d)
{
return (a + b) * (c + d);
}
};
}
};
}
};
}
}
清单6使用currying评估函数f(a, b, c, d) = (a + b) * (c + d)
。 给定表达式calc(1).apply(2).apply(3).apply(4)
,此函数按以下方式进行处理:
f(1, b, c, d) = g(b, c, d) = (1 + b) * (c + d)
g(2, c, d) = h(c, d) = (1 + 2) * (c + d)
h(3, d) = i(d) = (1 + 2) * (3 + d)
i(4) = (1 + 2) * (3 + 4)
编译清单6:
javac CurriedCalc.java
运行生成的应用程序:
java CurriedCalc
您应该观察以下输出:
21
因为currying是关于执行函数的部分应用的,所以参数的应用顺序无关紧要。 例如,而不是传递a
对calc()
和d
到最嵌套apply()
方法(其执行计算),我们可以逆转这些参数名称。 这将导致dcba
而不是abcd
,但仍将达到21
的相同结果。 (本教程的源代码包括CurriedCalc
的替代版本。)
Java 8之前的函数式编程并不美观。 从一流函数创建,传递函数和/或从一流函数返回函数所需的代码太多。 Java的早期版本也缺少预定义的功能接口和一流的功能,例如过滤器和映射。
Java 8通过引入Java语言的lambda和方法引用在很大程度上降低了冗长程度。 它还提供了预定义的功能接口,并通过Streams API提供了过滤,映射,缩小和其他可重用的一流功能。
我们将在下一部分中一起查看这些改进。
lambda是通过表示功能接口的实现来描述功能的表达式。 这是一个例子:
() -> System.out.println("my first lambda")
从左到右, ()
标识lambda的形式参数列表(没有参数), ->
表示lambda表达式,而System.out.println("my first lambda")
是lambda的主体(将执行的代码) )。
lambda具有type ,它是lambda实现的任何功能接口。 这种类型就是java.lang.Runnable
,因为Runnable
的void run()
方法也有一个空的形式参数列表:
Runnable r = () -> System.out.println("my first lambda");
您可以在需要Runnable
参数的任何地方传递lambda; 例如, Thread(Runnable r)
构造函数。 假设发生了先前的分配,则可以将r
传递给此构造函数,如下所示:
new Thread(r);
另外,您可以将lambda直接传递给构造函数:
new Thread(() -> System.out.println("my first lambda"));
这肯定比Java 8之前的版本更紧凑:
new Thread(new Runnable()
{
@Override
public void run()
{
System.out.println("my first lambda");
}
});
我以前的高阶函数演示展示了一个基于匿名内部类的文件过滤器。 这是基于lambda的等效项:
File[] txtFiles = new File(".").listFiles(p -> p.getAbsolutePath().endsWith("txt"));
在第1部分中,我提到函数式编程语言使用表达式而不是语句。 在Java 8之前,您可以很大程度上消除函数式编程中的语句,但不能消除return
语句。
上面的代码片段显示,lambda不需要return
语句即可返回值(在这种情况下为布尔值true / false):您只需指定表达式而无需return
[并添加]分号。 但是,对于多语句lambda,您仍然需要return
语句。 在这些情况下,您必须按以下方式将lambda的主体放在括号之间(不要忘记用分号终止该语句):
File[] txtFiles = new File(".").listFiles(p -> { return p.getAbsolutePath().endsWith("txt"); });
我还有两个示例来说明lambda的简洁性。 首先,让我们从清单2所示的Sort
应用程序中重新访问main()
方法:
public static void main(String[] args)
{
String[] innerplanets = { "Mercury", "Venus", "Earth", "Mars" };
dump(innerplanets);
sort(innerplanets, (e1, e2) -> e1.compareTo(e2));
dump(innerplanets);
sort(innerplanets, (e1, e2) -> e2.compareTo(e1));
dump(innerplanets);
}
我们还可以从清单6所示的CurriedCalc
应用程序更新calc()
方法:
static Function>>
calc(Integer a)
{
return b -> c -> d -> (a + b) * (c + d);
}
Runnable
, FileFilter
和Comparator
是功能接口的示例,它们描述了功能。 Java 8通过要求使用java.lang.FunctionalInterface
注释类型对功能接口进行注释(如@FunctionalInterface
来形式化此概念。 用此类型注释的接口必须恰好声明一个抽象方法。
您可以使用Java的预定义功能接口(稍后讨论),也可以轻松指定自己的接口,如下所示:
@FunctionalInterface
interface Function
{
R apply(T t);
}
然后,您可以使用此功能界面,如下所示:
public static void main(String[] args)
{
System.out.println(getValue(t -> (int) (Math.random() * t), 10));
System.out.println(getValue(x -> x * x, 20));
}
static Integer getValue(Function f, int x)
{
return f.apply(x);
}
如果您不熟悉Lambda,则可能需要更多背景知识才能理解这些示例。 在这种情况下,请参阅 “ Java中的lambda表达式入门 ”中对lambda和功能接口的进一步介绍。 您还将找到有关此主题的大量有用的博客文章。 一个示例是“ 使用Java 8函数进行功能编程 ”,作者Edwin Dalorzo在其中展示了如何在Java 8中使用lambda表达式和匿名函数。
每个lambda最终都是幕后生成的某个类的实例。 探索以下资源以了解有关lambda架构的更多信息:
我想您会发现Java语言架构师Brian Goetz的视频演示了lambda的幕后花絮,尤其令人着迷。
一些lambda仅调用现有方法。 例如,以下lambda在lambda的单个参数上调用System.out
的void println(s)
方法:
(String s) -> System.out.println(s)
lambda给出(String s)
作为其形式参数列表,以及一个代码体,该代码体的System.out.println(s)
表达式将s
的值打印到标准输出流。
要保存击键,可以将lambda替换为method reference ,这是对现有方法的紧凑引用。 例如,您可以将以下代码片段替换为以下代码:
System.out::println
在这里, ::
表示正在引用System.out
的void println(String s)
方法。 该方法参考产生的代码比我们之前使用lambda实现的代码短得多。
我以前展示了清单2中的lambda版本的Sort
应用程序。这是用方法引用编写的相同代码:
public static void main(String[] args)
{
String[] innerplanets = { "Mercury", "Venus", "Earth", "Mars" };
dump(innerplanets);
sort(innerplanets, String::compareTo);
dump(innerplanets);
sort(innerplanets, Comparator.comparing(String::toString).reversed());
dump(innerplanets);
}
String::compareTo
方法的引用版本比(e1, e2) -> e1.compareTo(e2)
的lambda版本短。 但是请注意,创建等效的逆序排序需要较长的表达式,该表达式还包括方法引用: String::toString
。 不用指定String::toString
,我可以指定等效的s -> s.toString()
lambda。
在有限的空间里,方法引用的内容远远超出了我的能力范围。 要了解更多信息,请查看“ 如何使用Java中的方法引用入门 ”中有关为静态方法,非静态方法和构造函数编写方法参考的介绍。
Java 8引入了预定义的功能接口( java.util.function
),因此开发人员无需为常见任务创建我们自己的功能接口。 这里有一些例子:
Consumer
功能接口表示一个接受单个输入参数且不返回结果的操作。 它的void accept(T t)
方法对参数t
执行此操作。 Function
功能接口表示一个接受一个参数并返回结果的函数。 它的R apply(T t)
方法将此函数应用于参数t
并返回结果。 Predicate
功能接口表示一个参数的谓词 (布尔值函数)。 它的boolean test(T t)
方法根据参数t
评估此谓词,并返回true或false。 Supplier
功能界面表示结果的提供者。 它的T get()
方法不接收任何参数,但返回结果。 清单1中的DaysInMonth
应用程序显示了一个完整的Function
接口。 从Java 8开始,您可以删除此接口并导入相同的预定义的Function
接口。
“ Java中的lambda表达式入门 ”提供了Consumer
和Predicate
功能接口的示例。 请查看博客文章“ Java 8-惰性参数评估 ”,以发现Supplier
的有趣用法。
此外,尽管预定义的功能接口很有用,但它们也存在一些问题。 博客Pierre-Yves Saumont 解释了原因 。
Java 8引入了Streams API,以促进数据项的顺序和并行处理。 该API基于stream ,其中stream是一系列元素的序列,这些元素源自源并支持顺序和并行聚合操作。 源存储元素(例如集合)或生成元素(例如随机数生成器)。 聚合是根据多个输入值计算得出的结果。
流支持中间操作和终端操作。 中间操作返回新的流,而终端操作消耗该流。 将操作连接到管道中 (通过方法链接 )。 流水线从源开始,然后是零个或多个中间操作,最后以终端操作结束。
流是功能性API的示例。 它提供了筛选,映射,缩小和其他可重用的一流功能。 我在第1部分清单1中显示的Employees
应用程序中简要地演示了该API。清单7提供了另一个示例。
import java.util.Random;
import java.util.stream.IntStream;
public class StreamFP
{
public static void main(String[] args)
{
new Random().ints(0, 11).limit(10).filter(x -> x % 2 == 0)
.forEach(System.out::println);
System.out.println();
String[] cities =
{
"New York",
"London",
"Paris",
"Berlin",
"BrasÌlia",
"Tokyo",
"Beijing",
"Jerusalem",
"Cairo",
"Riyadh",
"Moscow"
};
IntStream.range(0, 11).mapToObj(i -> cities[i])
.forEach(System.out::println);
System.out.println();
System.out.println(IntStream.range(0, 10).reduce(0, (x, y) -> x + y));
System.out.println(IntStream.range(0, 10).reduce(0, Integer::sum));
}
}
main()
方法首先创建一个从0开始到10结束的伪随机整数流。该流仅限于10个整数。 filter()
第一类函数接收一个lambda作为其谓词参数。 谓词从流中删除奇数整数。 最后, forEach()
第一类函数通过System.out::println
方法引用将每个偶数整数打印到标准输出。
main()
方法接下来创建一个整数流,该整数流将产生一个从0开始到10结束的连续整数范围mapToObj()
第一类函数接收一个lambda,该lambda将整数映射到等价字符串中的等价字符串上。 cities
阵列。 然后,通过forEach()
第一类函数及其System.out::println
方法参考将城市名称发送到标准输出。
最后, main()
演示了reduce()
第一类函数。 产生与上一个示例相同范围的整数的整数流被缩减为其值的总和,随后将其输出。
limit()
, filter()
, range()
和mapToObj()
都是中间操作,而forEach()
和reduce()
是终端操作。
编译清单7,如下所示:
javac StreamFP.java
运行生成的应用程序,如下所示:
java StreamFP
我从一次运行中观察到以下输出:
0
2
10
6
0
8
10
New York
London
Paris
Berlin
BrasÌlia
Tokyo
Beijing
Jerusalem
Cairo
Riyadh
Moscow
45
45
您可能希望10个而不是7个伪随机偶数(在0到10之间,这要归功于range(0, 11)
)从7出现在输出的开头。 毕竟, limit(10)
似乎表明将输出10个整数。 但是,事实并非如此。 尽管limit(10)
调用会产生正好是10个整数的流,但filter(x -> x % 2 == 0)
调用会导致从流中删除奇数个整数。
如果您不熟悉Streams,请查看我的介绍Java SE 8的新Streams API的教程,以获取有关此功能API的更多信息。
许多Java开发人员不会像Haskell这样的语言追求纯函数式编程,因为它与熟悉的命令式,面向对象的范例有很大的不同。 Java 8的功能性编程功能旨在弥合这种差距,使Java开发人员可以编写更易于理解,维护和测试的代码。 功能代码也更可重用,并且更适合Java中的并行处理。 有了所有这些激励措施,实际上没有理由不将Java的功能性编程选项合并到您的Java代码中。
功能思维是尼尔·福特(Neal Ford)创造的一个术语,它是指从面向对象的范式到功能编程范式的认知转变。 正如您在本教程中所看到的,通过使用函数技术重写面向对象的代码,可以学到很多关于函数编程的知识。
通过访问清单2中的Sort应用程序,总结到目前为止所学的知识。在此快速提示中,我将向您展示如何编写纯功能的Bubble Sort ,首先使用Java 8之前的技术,然后使用Java 8的功能特点。
这个故事“ Java开发人员的功能编程,第2部分”最初由JavaWorld发布 。
翻译自: https://www.infoworld.com/article/3319078/functional-programming-for-java-developers-part-2.html
java 函数式编程