一般来说我们会认为在编程领域通用的lambda意义就是匿名函数(关于为什么叫做lambda可以参考这里),1958年Lisp首先采用匿名函数,不过随着时间的推移,越来越多的语言开始采用这种语法(比如PHP,C++,Python,等等)。这种趋势正好验证了Paul Graham在The roots of Lisp(Lisp之根源)所提出的一种观点

我认为目前为止只有两种真正干净利落, 始终如一的编程模式:C语言模式和Lisp语言模式。此二者就像两座高地,在它们中间是尤如沼泽的低地。随着计算机变得越来越强大,新开发的语言一直在坚定地趋向于Lisp模式。二十年来,开发新编程语言的一个流行的秘决是,取C语言的计算模式,逐渐地往上加Lisp模式的特性,例如运行时类型和无用单元收集。

我个人对上面的这句话无比的赞同,因为我们也可以观察到,不论PHP、C++还是Python,又或者是最近火的不行的要写天写地写宇宙的JavaScript,他们最初的语法都是借鉴了C,然后又开始逐渐的汲取Lisp的的语言特性,这正是上面所提到的取C语言的计算模式,逐渐地往上加Lisp模式的特性

Java在版本8中加入了 λ 表达式,不过这个新添加的语法糖和Lisp不一样并不是用来实现匿名函数的(Java中没有匿名函数),而是用于匿名内部类,新添加的 λ 表达式相当于增加了一种新的书写匿名内部类的方式。

1. 匿名内部类

Java中的匿名内部类使得程序员可以创建一个继承自某一个类或者实现了某一个接口的匿名类,这个类在实现的时候不需要有名字,而可以直接创建一个对象供使用。请看例子

1
2
3
public interface Lelouch {
void geass(String s);
}
1
2
3
4
5
6
7
Lelouch lelouch = new Lelouch() {
@Override
public void geass(String s) {
System.out.println(s);
}
};
lelouch.geass("ルルーシュ·ヴィ·ブリタニアが命じる");

上面我们定义了一个名叫鲁鲁修的接口,在接口中定义了一个方法叫做geass,方法包含了一个String类型的参数。然后我们使用了匿名内部类来创建了一个实现了鲁鲁修这个接口的实现类,并且在匿名类中我们重写了geass这个方法,方法很简单,就是打印这个参数中的字符串。通过匿名内部类的方式创建了一个实现了Lelouch接口的对象之后,创建一个指向了这个对象的类型为Lelouch名为lelouch的引用,之后通过这个引用执行匿名对象的geass方法,成功打印出了ルルーシュ·ヴィ·ブリタニアが命じる这句话。
鲁鲁修

2. lambda表达式

上面这个例子中我们创建了匿名内部类使用了6行代码,接下来,我们使用lambda表达式只需要一行就能够搞定,并且实现的的效果匿名内部类的效果一样。不过需要注意的是,lambda表达式和匿名内部类还有些不太一样的地方:

  1. 匿名内部类可以通过继承类或者实现接口实现,lambda表达式只适用于实现接口的情况
  2. 使用了lambda表达式的接口必须有且仅有一个方法

我们把上面的代码改写如下

1
Lelouch lelouch = (s) -> System.out.println(s);

上面这行代码就是一个实现了和上面的代码相同的效果的lambda表达式,我们来解释下(s)->System.out.println(s)是什么意思。

  • (s)代表了接口Lelouch中唯一的方法的参数。如果只有一个参数,可以不加括号;如果没有参数,使用()即可;如果有多个参数,只需要按顺序把参数输进去,形如(x, y)。
  • ->,lambda的操作符,使用了它就代表使用了lambda表达式。
  • System.out.println(s)是一个方法执行的区域,可以使用第一个括号中所定义好的参数。如果只有一条语句,直接写出来即可;如果有多条语句,需要使用{}把代码括起来。

上面就是对lambda表达式的解释,很简单,很容易。只要把它和匿名内部类对照起来看,就很明了了。下面是一个稍微复杂点的例子:

1
2
3
public interface Lelouch {
void geass(String name, String s);
}
1
2
3
4
5
6
7
8
9
Lelouch lelouch = (name, s) -> {
if ("ルルーシュ".equals(name)) {
System.out.println(s);
} else {
System.out.println("Yes, your majesty.");
}
};
lelouch.geass("ルルーシュ", "ルルーシュ·ヴィ·ブリタニアが命じる");
lelouch.geass("king", "");

打印结果为

ルルーシュ·ヴィ·ブリタニアが命じる
Yes, your majesty.

这里我们把参数由一个添加为了两个,并且把一行执行代码变为了一个代码块,这在lambda表达式中也是合法的。

如果lambda表达式中只有一条执行语句,并且lambda表示的参数和这条执行语句所需要的参数一致,那我们就可以用::来调用方法进行执行。
例如对于第一个例子

1
Lelouch lelouch = (s) -> System.out.println(s);

因为geass方法和System.out.println所需要的参数是一模一样的(都为s),那么我们就可以使用如下的这种lambda表达式来实现

1
Lelouch lelouch = System.out::println;

上面这条语句是对原始的lambda表达式的进一步的简写,这种语法对参数的个数是没有限制的,只需要保证接口中的方法参数和所执行的语句的方法参数一致就可以了。下面是对这种语法的一个稍微复杂一点的示例,能够加深你的理解

1
2
3
public interface Person {
void info(String name, int age, String location);
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Lambda {

public static void main(String[] args) {
Person person = new Lambda()::printInfo;
person.info("张三", 20, "南京");
}

void printInfo(String name, int age, String location) {
System.out.println("[name = " + name + ", age = " + age + ", location = " + location + "]");
}

}

需要知道的是,lambda表达式只是Java的一个语法糖,这种操作在编译的时候编译器就会把它的语法转化为内部类的语法,之后的操作就和内部类的操作是一样的了,也就是说lambda语法不会涉及到JVM中的相关操作。

参考:

Java SE 8: Lambda Quick Start