65.9K
CodeProject 正在变化。 阅读更多。
Home

Java 8 函数式编程入门

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (7投票s)

2014年7月2日

CPOL

6分钟阅读

viewsIcon

21551

Java 8 可能是近年来最令人兴奋的 Java 语言版本之一。其中一个主要特性是支持函数式编程,这也是本文的重点。这种支持主要体现在三个特性上:支持 [工作管道] 流。流允许我们以函数式的方式,通过一系列阶段来处理数据。d

Java 8 可能是近年来最令人兴奋的 Java 语言版本之一。其中一个主要特性是支持函数式编程,这也是本文的重点。这种支持主要体现在三个特性上。

  • 支持 [工作管道] 流。流允许我们以函数式的方式,通过一系列阶段来处理数据。我们可以从 List 等容器实例创建流,或者使用 IntStream 等特定流类创建它们。
  • 支持 lambda 函数。简单来说,这是一个匿名函数(我们不需要给它命名),它接受一些参数并返回一个值。返回的值将传递到管道的下一个操作。
  • 支持将函数传递给其他函数。

鉴于函数式编程自 20 世纪 50 年代以来就已存在,并且直到最近才被主流广泛忽视,为什么它会成为一个热门话题?我的看法是,因为它能够轻松地并行处理工作,利用多核处理器,惰性(按需)求值,以及易于与其他语言(如 Java)集成。当然,JVM 为 Scala 提供了良好的基础,Scala 甚至可以嵌入到 Java 程序中,从而兼顾两者的优点,并为开发人员提供更简单的迁移路径。然而,Scala 的学习曲线相当陡峭,而且已经有大量的 Java 程序员,因此 Java 本身最终面临着追赶的压力,以避免为了做一些很酷且有用的事情而不得不学习一门全新的语言。

废话不多说,让我们开始进入正题,来看一个 "Hello World" 示例。

public class HelloWorld
{
	public static void main(String[] args)
	{
		List<String> countries = Arrays.asList("France", "India", 
			"China", "USA", "Germany");

		for (String country : countries)
		{
			System.out.println("Hello " + country + "!");
		}
	}
}

这对于所有 Java 程序员来说应该都很熟悉;我们使用增强型 for 循环获取一个国家列表并打印问候语。

那么,就这样做有什么问题吗?对于一个简单的例子来说,问题不大,但想象一下如果我们做一些更复杂的事情。

我们想编写一些测试来检查代码是否正常工作,但测试循环体可能很棘手。当然,我们可以将循环体提取到自己的函数中,但如果我们到处都这样做,最终会得到许多用于循环体的微小函数。

如果我们想并行处理项目怎么办?我们需要将一个国家交给一个实现问候语功能的线程。应该是一个线程处理一个国家,还是一个批次的国家?我们如何传递批量的工作并检查何时完成?我们还必须处理线程之间的通信。

另一个问题是,循环体的代码与循环变量的来源是分离的。要查看国家是如何创建的,我们必须找到 for 语句。我们更希望告诉国家列表执行消息打印。问题在于列表不理解被告知去做事情,它们只是容器。

然而,自 Java 8 以来,容器可以将它们的内容传递给一个能够处理指令的实体——Stream。我已经修改了示例以使用流。

	public static void main(String[] args)
	{
		List<String> countries = Arrays.asList("France", "India", "China",
				"USA", "Germany");

		countries.stream().forEach(
				(String country) -> System.out
						.println("Hello " + country + "!"));
	}

国家列表现在创建一个 Stream,并将其传递给容器中的“spliterator”。这是一个迭代器,能够将内容分割成工作批次。我们将在以后的文章中探讨这种批处理(然后可以并行化)。

Stream 本身是一个接口,由抽象类 ReferencePipeline 实现。然后可以将作业链接到返回的 Stream。每个作业执行一些工作,并在完成后调用下游作业。在示例中,列表创建一个 Stream,并将 forEach 作业添加到其管道中。就像第一个示例中的循环一样,forEach 作业对每个项目执行一些工作。有趣的 -> 表示 lambda 表达式(匿名函数),这里接收一个名为 country 的 String 并将其打印在消息中。

由于在这种情况下编译器可以推断出 country 是一个 String,因此我们可以省略类型,并且由于只有一个参数,我们也可以省略左侧的 (),从而得到更简洁的写法:

countries.stream()
   .forEach(country -> System.out.println("Hello " + country + "!"));

请注意,这比仅仅引用一个恰好是国家的循环变量要清楚得多,即我们在对国家进行操作。它也更容易阅读,因为更少的样板代码掩盖了实际的业务逻辑。更清晰、更容易阅读的代码更容易理解,也更容易发现错误。我们也不必担心在样板代码中出错,这减少了我们的测试工作。总的来说,编码更有趣,开发时间也缩短了。

让我们通过一些实验来了解其底层的工作原理。forEach 正在接受的 lambda 实际上是 Consumer<T> 的一个实现。

* 提醒一下那些对泛型不太熟悉或第一次接触的人:这意味着 Consumer 类型使用一个未知的类型 T,该类型在编译时确定。

我们可以通过创建一个内部类来明确地展示 Consumer 的使用。

private static class Greeter<T> implements Consumer<T>
{
	@Override
	public void accept(T t)
	{
		System.out.println("Hello " + t + "!");
	}
}

并修改管道:

countries.stream().forEach(new Greeter<String>());

Consumer 有一个 accept 方法,forEach 每次都会用管道中的每个项目调用它。

让我们将消息的形成和打印分成两个作业。要形成问候消息,我们可以使用 map 函数。它将一个 lambda 函数应用于输入,转换输入。打印作业仍然由 forEach 执行。

public static void main(String[] args)
{
	List<String> countries = Arrays.asList("France", "India",
			"China", "USA", "Germany");

	countries.stream().map(country -> "Hello " + country + "!")
			.forEach(System.out::println);
}

注意 System.out::println 这种奇怪的语法。我们实际上是将一个函数传递给 forEach,即 System.out 实例的 println 方法。:: 语法只是表示传递右侧的函数,并使用左侧的对象调用。左侧可以是静态调用的类名、对象,或者对象的别名(this 或 super)。请注意,我们不能只传递函数名,因为这将被解释为类型为 Consumer 的变量。

如果我们在一个也具有 doPrint 方法(接受 String 并返回 void)的对象实例中运行它,而不是静态的 main 方法,我们可以这样写:

       countries.stream().map(country -> "Hello " + country + "!")
                        .forEach(this::doPrint);

让我们看看 map 在底层是如何工作的。Map 接收一个 Function 类的实例,并调用其 apply 方法。Apply 接受类型 T 并返回类型 U。我们将创建一个 Greeter 内部类来演示这一点。

private static class Greeter<T> implements Function<T, String>
{
	@Override
	public String apply(T t)
	{
		return "Hello " + t + "!";
	}
}

...

countries.stream().map(new Greeter<String>())
                  .forEach(System.out::println);

当我们在这里使用 lambda 函数时,它就被当作 Function 的一个匿名实例。

最后,让我们在一个同时拥有 doPrint 和 makeGreeting 方法(接受 String 并返回 String)的对象中运行管道,看看它是什么样子。

public class HelloWorldConcise
{
	private void doPrint(String str)
	{
		System.out.println(str);
	}

	private String greet(String country)
	{
		return "Hello " + country + "!";
	}
		
	public void greetCountries()
	{
		List<String> countries = Arrays.asList("France", "India", 
			"China", "USA", "Germany");

		countries.stream().map(this::greet).forEach(this::doPrint);
	}

	public static void main(String[] args)
	{
		new HelloWorldConcise().greetCountries();
	}
}

greetCountries 函数非常简洁。很明显,我们遍历每个国家,从中创建一个问候语,然后打印它。另一个很棒的地方是它非常容易测试。如果我们扩展 HelloWorldConcise 类以创建一个 HelloWorldConciseTest,我们可以实现自己的 makeGreeting 和 doPrint 版本。我们的 makeGreeting 在调用其父版本后可用于检查问候语是否正确形成,而 doPrint 方法可以是一个存根。

* 好的,在测试期间打印到标准输出不是什么大事,但想象一下如果 doPrint 将问候语发送到网页,我们就不能仅仅为了测试它而设置所有这些。

这就是我们对 Java 8 函数式编程的初步了解。希望您觉得它很有用。在下一篇文章中,我将介绍一些可以包含在管道中的其他操作。


修订历史

2014 年 7 月 2 日 - 修正了类别

© . All rights reserved.