深入理解 Java 中的 Lambda

我花了相当多的阅读和编码时间才最终理解Java Lambdas如何在概念上正常工作的。我阅读的大多数教程和介绍都遵循自顶向下的方法,从用例开始,最后以概念性问题结束。在这篇文章中,我想提供一个自下而上的解释,从其他已建立的Java概念中推导出Lambdas的概念。

首先介绍下方法的类型化,这是支持方法作为一流公民的先决条件。基于此,Lambdas的概念是被以匿名类用法的进化和特例提出的。所有这一切都通过实现和使用高阶函数映射来说明。

这篇文章的主要受众是那些已掌握函数式编程基础的人,以及那些想从概念上理解Lambdas如何嵌入Java语言的人。

方法类型

从Java 8起方法就是一等公民了。按照标准的定义,编程语言中的一等公民是一个具有下列功能的实体,

  • 可以作为参数进行传递,
  • 可以作为方法的返回值
  • 可以赋值给一个变量.

在Java中,每一个参数、返回值或变量都是有类型的,因此每个一等公民都必须是有类型的。Java中的一种类型可以是以下内容之一:

  • 一种内建类型 (比如 int 或者 double)
  • 一个类 (比如ArrayList)
  • 一个接口 (比如 Iterable)

方法是通过接口进行定义类型的。它们不隐式的实现特定接口,但是在必要的时候,如果一个方法符合一个接口,那么在编译期间,Java编译器会对其进行隐式的检查。举个例子说明:

class LambdaMap { static void oneStringArgumentMethod(String arg) { System.out.println(arg); }}

关于oneStringArgumentMethod函数的类型,与之相关的有:它的的函数是静态的,返回类型是void,它接受一个String类型的参数。一个静态函数符合包含一个apply函数的接口,apply函数的签名相应地符合这个静态函数的签名。oneStringArgumentMethod函数对应的接口因此必须符合下列标准。

  • 它必须包含一个名为apply的函数。
  • 函数返回类型必须是void。
  • 函数必须接受一个String类型可以转换到的对象的参数。

在符合这个标准的接口之中,下面的这个是最明确的:

interface OneStringArgumentInterface { void apply(String arg);}

利用这个接口,函数可以分配给一个变量:

OneStringArgumentInterface meth = LambdaMap::oneStringArgumentMethod;

用这种方法使用接口作为类型,函数可以借此被分配给变量,传递参数并且从函数返回:

static OneStringArgumentInterface getWriter() { return LambdaMap::oneStringArgumentMethod;}static void write(OneStringArgumentInterface writer, String msg) { writer.apply(msg);}

最终函数是一等公民。

泛型函数类型

就像使用集合一样,泛型为函数类型增加了大量的功能和灵活性。实现功能上的算法而不考虑类型相关信息,泛型函数类型使其变为可能。在对map函数的实现中,会在下面用到这种功能。

在这提供的OneStringArgumentInterface一个泛型版本:

interface OneArgumentInterface { void apply(T arg);}

OneStringArgumentInterface函数可以被分配给它:

OneArgumentInterface meth = LambdaMap::oneStringArgumentMethod;

通过使用泛型函数类型,它现在可以以一种通用的方法实现算法,就像它在集合中使用的一样:

static  void applyArgument(OneArgumentInterface meth, T arg) { meth.apply(arg);} 

上面的函数并没有什么用,然而它至少可以提出一个想法:对函数作为第一个类成员的支持怎样可以形成非常简洁且灵活的代码:

applyArgument(Lambda::oneStringArgumentMethod, "X");

这看起来很漂亮,但是很多人会认为函数式的解决方案更清晰,更具可读性:

List out = new ArrayList<>();for (Double radius : Arrays.asList(1., 2., 3., 4.)) { out.add(Math.sqrt(radius) * Math.PI);}System.out.println(out);

到目前为止,最后是使用Lambda表达式。 读者应该注意Lambda如何取代上面提到的匿名类:

System.out.println( map(radius -> { return Math.sqrt(radius) * Math.PI; }, Arrays.asList(1., 2., 3., 4.)));

这看起来简洁明了 - 请注意 Lambda 表达式如何缺省任何明确的类型信息。 没有显式模板实例化,没有方法签名。

结语

总而言之,Java中的Lambdas的概念是整洁的。我支持编写更简洁、更清晰的代码,并让程序员免于编写可由编译器自动推断的架手架代码。它是语法糖,如上所述,它只不过是使用匿名类也能实现的功能。然而,我会说它是非常甜的语法糖。

另一方面,Lambdas还支持更加混淆以及难以调试的代码。Python社区很早就意识到了这一点 - 虽然Python也有Lambda,但它若被广泛使用则通常被认为是不好的风格(当嵌套函数可以被使用时,它并不难于规避)。对于Java来说,我会给出类似的建议。毫无疑问,在某些情况下,使用Lambdas会导致代码大大缩减并更易读,尤其在与流有关时。在其他情况下,如果采取更保守的做法和最佳实践,另外一种方法可能会是更好的替代。


分享到:


相關文章: