0%

Java语言新特性漫谈:Java 8之Lambda表达式篇

本文是《Java语言新特性漫谈》系列文章中的一篇,该系列文章主要探讨各 Java 版本的语言特性方面的增强更新。

Lambda 表达式是什么?

Lambda 表达式,在其他语言中,对等概念有:闭包、匿名函数、代码块等等。简单来说,它是一段代码,但是它可以作为“值”赋值给变量——这当然包括为方法传参——即所谓的“行为参数化”。

Java 8 之前的版本下,通常只有“数据”可以通过变量传递,而如果要传递的是“行为”,命令模式可能是个不错的选择——但是,我们会发现,我们的原意是传递一些代码,最终却传递了一个对象。为此,还需要声明相关接口和类……WTF。这就好比旧时的资本家所说的:我想雇佣的是一双手,结果来的是一个人。

在没有 Lambda 表达式的年代里,经常会看到以下代码:

1
2
3
4
5
6
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("没有 Lambda 的执行方法");
}
}

这段代码实际的意图是想传递代码 System.out.println("没有 Lambda 的执行方法"); 以便在稍后执行它,仅此而已。那如果使用 Lambda 表达式编写会是什么样呢?

1
Runnable lambda = () -> System.out.println("Running from Lambda");

等号右边的就是 Lambda 表达式。是不是简洁了很多,最重要的是,代码本身对其意图的表达更清晰了。

语法

那么,Lambda 表达式到底怎么写呢?从上面的示例其实已经可以看到其基本构成了。

Java 8 为 Lambda 表达式引入了新的操作符 ->,称为“Lambda 操作符”,由于它像一个箭头,也可称为“箭头操作符”。-> 左侧是参数列表,右侧是执行代码块——也称“Lambda体”。比较完整的语法格式如下:

1
2
3
4
(modifiers type param1, ...) -> {
// 执行代码……
return returnValue;
}

在各种语言中,Lambda 表达式如果使用操作符的话,通常都是使用“箭头”形式。比如,JavaScript 在 ES6 加入了 Lambda 表达式的一种实现,使用的操作符是 =>,也是“箭头”形式,被称为“箭头函数”。

从语法上看 Lambda 表达式还是比较复杂的结构,但是,通常情况下,为了简洁,我们不使用它的完整形式,多多少少会省略一些部分,主要包括:

  • 类型和修饰符
  • 参数列表的圆括号
  • Lambda 体的花括号
  • return 关键字

省略类型和修饰符

通常,使用 Lambda 表达式都不会指定参数类型,因为编译器能够通过上下文自动推断出来。

简单地说,Lambda 表达式的参数,可以指定类型,也可以依靠推断。

但是,如果有多个参数,那它们的声明形式应该是一致的,即要么都指定类型,要么都推断,不允许混合。

另外,只有指定类型的参数才可以使用修饰符。

省略参数列表圆括号

只有一种情况可以省略参数列表的圆括号,那就是仅有一个参数的时候。

省略 Lambda 体花括号

多条语句必须用花括号括起来,只有单条语句时才可以省略花括号。

省略 return

仅有一条返回语句时,花括号与 return 可以一起省略。

总之,知道了 Lambda 表达式的语法,我们就可以编写它了。

Lambda 究竟是什么?

Lambda 表达式究竟是什么呢?这是一个好问题。从概念上说 Lambda 表达式代表一个代码块,但是 Java 是面向对象的语言,并不直接支持。

那让我们回过头来看下 Lambda 表达式出现的位置,它必须出现在以下三个位置:

  • 赋值上下文
  • 调用上下文
  • 强制类型转换上下文

这三个位置都表明,Lambda 表达式是有类型的,它是某个类型的对象。那它是什么类型的对象呢?

这里再看一下上文中的示例:

1
Runnable lambda = () -> System.out.println("Running from Lambda");

Lambda 表达式出现在赋值上下文中,很显然,它被赋值给了一个 Runnable 接口变量,换句话说,这个 Lambda 表达式实际是一个 Runnable 接口对象。

函数式接口

事实上,所有 Lambda 表达式的类型都是一个有且仅有一个抽象方法的接口,这被称为“函数式接口”——这也是 Java 8 新增特性。

函数式接口可以自定义,仅仅需要为接口添加新注解 @FunctionalInterface 即可。

1
2
3
4
@FunctionalInterface
interface MyFunctionalInterface {
void action(String param);
}

Java 8 定义了很多函数式接口(参考这里),因此,大部分情况下不需要自定义。

方法引用

你应该注意到了,在上文编写 Lambda 表达式时,执行的代码块都是自行编写的。但如果有现有的代码可以引用,那就方便很多。

要引用现有的代码——其实就是方法——就需要用到 Java 8 新加入的特性,称为“方法引用”。

方法引用共有四种形式:

  • 引用静态方法 ClassName::staticMethod
  • 引用对象的实例方法 object::instanceMethod
  • 引用某类型任意对象的实例方法 TypeName::instanceMethod
  • 引用构造方法 ClassName::new

可以看到,方法引用也引入了一个新操作符 ::

小结

Lambda 表达式代表的是一个待执行的代码块,它使得“行为”也可以传递。

但本质上,在 Java 中它仍然是对象,它是某个函数式接口的对象。

Lambda 体中的代码不必自行编写,也可以引用已存在的方法,这就要用到“方法引用”。