06_fp_01_lambda

第六十章 Lambda表达式

1 简介

匿名函数(Anonymous Function)是一些没有定义函数名字的函数。从开发人员的角度看,这些函数没有名字;而从编程语言或者编译器的角度看,匿名函数是一些没有绑定标识符(Not Bound to an Identifier)的函数实体。换句话说,这些已定义的函数实体无法通过标识符(或者名字)唯一标识。匿名函数常常用在函数调用或者函数返回的语句中,因为在这些场景下,匿名函数实际上绑定到了入参或者返回参数上。

Lambda表达式可以看做是一种匿名函数(Anonymous Function)。它既可以作为参数传入另一个函数,也可作为另一个函数的返回值。除了其使用方面以外,Lambda表达式也作为Java支持函数式编程的第一步,在Java 8中被引入Java语言。

2 Lambda 表达式的优势

开发人员可能需要一段时间的学习和练习才能熟练掌握Lambda表达式。但是,当熟练后,Lambda表达式能为开发人员带来许多好处。我们将用下面的一个例子来说明Lambda表达式的优势。在这个例子中,我们首先创建一个四个元素的数组。然后,调用数组的sort函数将元素排序。在排序中使用的比较元素大小的函数是一个Lambda表达式。

Integer[] ages = {25, 18, 45, 31};
Arrays.sort(ages, (x, y) -> y - x);

2.1 代码更加简洁、清晰

首先,上述代码将数据与排序的逻辑分离;ages是一个包含四个整形元素的数组;而Arrays.sort函数实现了排序的大部分逻辑。其中,遗留下来未实现的是如何比较两个元素的大小。例子中的Lambda表达式(x, y) -> y - x正好补足了元素比较的逻辑。从开发人员的角度看,开发人员只需将开发和维护的重点放在Lambda表达式上即可,简化了开发和维护的难度。

2.2 代码可读性、可维护性更好

在熟悉了Lambda表达式的语法后,使用适当的格式化或者缩进,能使代码变得逻辑更加清晰,可读性更强。因为在使用Lambda表达式之后,Lambda表达式的逻辑是与外部逻辑分离的。这使得代码更新或者维护更加容易。

2.3 更容易使用函数式编程

在函数式编程思想下,所有的逻辑都是由函数处理的。然而,按照Java函数定义的语法,写出一个函数的完整定义往往费时费力,而且函数的入参和出参可以根据上下文环境推断出来。因此,Lambda表达式能为函数式编程人员节省大量的时间,使得函数之间的衔接更容易。

2.4 代码更易于并行化

从上例可知,排序算法是在Arrays.sort函数里实现的。而且,在进行元素比较时,Lambda表达式仅使用了需要比较的两个元素。其他元素的取值与当前两个元素的比较过程是独立的。因此,Arrays.sort可采用并行排序算法实现。值得注意的是,我们并不是说Arrays.sort函数是并行实现的。我们只是指出Arrays.sort函数实现的逻辑有可能并行化。例如:Arrays.sort很容易的被Arrays.parallelSort替代。

Integer[] ages = {25, 18, 45, 31};
Arrays.parallelSort(ages, (x, y) -> y - x);

3 Lambda 表达式的使用方法

3.1 Lambda 表达式的语法

Lambda表达式的语法非常简单。在->的左侧是Lambda表达式的参数列表(Parameter List),右侧是Lambda表达式的内容(Body)。

LambdaParameters -> LambdaBody

参数列表中所有的参数由一对括号包含,参数之间由逗号分隔。如果只有一个参数,而且该参数是标识符(Identifier)时,参数列表的括号可以省略。每个参数都应有一个数据类型。如果未给出数据类型,Java编译器需要能够推断出参数的数据类型。此种参数被称为Inferred-type Parameter。如果开发人员显示的给出了参数的类型,此种参数被称为Declared-type Parameter。这两种参数不能混合使用在一个Lambda表达式中。Lambda表达式也支持变长参数,用法与函数定义相似。如下是一些合法的Lambda表达式的例子。

() -> {}         // 没有参数,结果是返回Void
(x) -> x + 1     // 一个Inferred-type参数,返回x+1的值
(int x) -> x + 1 // 一个Declared-type参数,返回x+1的值
x -> x + 1       // 只有一个Inferred-type参数,括号可以省略
(x, y) -> x + y  // 有两个参数,返回两个参数的和 
(int... x) -> x.length  // 返回变长参数的个数

Lambda表达式的右侧是主体部分。它可以是一个表达式,或者是由一对花括号包含的代码块。与函数定义类似,Lambda表达式的主体代表着当这个Lambda表达式运行时执行的内容。但是,与函数定义不同的是,Lambda可使用由上下文定义的变量,例如:局部变量,this和super。这是Lambda表达式的一个特性:闭包(Closure),我们会在下一小节详细讲解。

除了返回一个值以外,Lambda表达式还可以返回Void。当Lambda表达式无返回语句或者使用return;语句时,返回值为Void。例如:

() -> {}    // 返回Void
(x) -> {System.out.println(x)}  // 返回Void

Lambda表达式可用在赋值上下文(Assignment Context)、调用上下文(Invocation Context)、和类型转换上下文(Type Casting Context)中。Java编译器需要将Lambda表达式转换成一个Functional Interface的对象。在转换过程中,Java编译器还需要推断出参数的类型。我们将在后续章节详细介绍Functional Interface

3.2 Lambda闭包(Lambda Closure)

当运行Lambda表达式时,Java虚拟机需要执行Lambda表达式主体部分的语句。这个过程与运行函数调用时执行函数主体部分语句的过程十分相似。然而,一个重要的区别是,在Lambda表达式中,可以引用Lambda表达式所在的上下文中的变量。这种机制为闭包Closure。

如下例所示。LambdaExample类的成员函数increment返回一个Supplier的对象。Supplier是一个Functional Interface。在该函数中,Lambda表达式引用了输入参数start。当increment函数被调用时,Java编译器会自动生成一个匿名类,实现Supplier的接口。在该匿名类中不仅需要保留这个Lambda表达式的定义,而且需要保存一份start的值的拷贝,以便于在被调用时,能够访问变量start的值。因为临时变量是保存在栈上的,在increment函数返回后,start变量就不能被访问了。这个保存一份start的值的拷贝的过程被称为Lambda Capturing。

然而,仅这些是不够的。因为,当保存了一份start的值的拷贝之后,start的值有可能变化,而这个变化并不能体现在它的拷贝中。所以,Java语言对局部变量有一个额外的限制:即局部变量必须是final或者effectively final的。这个限制限定了局部变量在当前的上下文中不能被修改。Effectively final可以被认为是那些可以被定义为final,而开发人员却没有设置为final的变量,这也是为了兼容老代码而增加的额外的负担(对于Java编译器而言)。

当Lambda表达式使用this或者super时并没有这样的限制。因为,this和super引用的对象是在堆上的,只要不被垃圾回收,就可以随时访问。(如果Lambda表达式中使用了this或者super,则在新创建的匿名类的对象中会引用this或者super指向的对象。所以,在它们被使用前,它们不会被垃圾回收。

import java.util.function.Supplier;

public class LambdaExample {
    Supplier<Integer> increment(int start) {
        // Error,start has to be final or effectively final 
        return () -> start++;
    }
}

4 结语

本章详细介绍了Lambda表达式的使用方法。Lambda表达式的引入是Java支持函数式编程的第一步。经过一段时间的学习和练习后,熟练掌握Lambda表达式能极大的简化代码,提高编码效率。同时,也能帮助开发人员更加有效的管理和维护代码。我们将在接下来的几章中介绍Functional Interface。在它与Method Reference,Lambda表达式的共同作用下,它们将面向对象编程和函数式编程的思想融合在一起,不仅增强了Java语言的适用面,也丰富了Java语言的特性。

 

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.