Lambda 表达式


十月的第三周,来学习 Lambda 表达式。

这周休息一下,本来呢要学习 Java 8 的函数式编程特性,Lambda 表达式只是其中的一部分,结果为了写之前的博文每周都欠债,这周水一下……


先来讨论,lambda 表达式是干什么的。

Java 是一门面向对象的语言,万物皆对象,做什么也都离不开对象。对象内可能有属性,可能有对象,也有可能有方法,就比如说:我,18 岁(属性),有只猫(对象),我喜欢撸猫(方法)。Java 面向对象的体现就在于,我多少岁、有什么、做什么(也就是对象的属性、方法等等),前提都在于“我”的存在。

所以说,Java 里存在着对象,也存在着行为(方法),但是行为一定要有对象作为前提,不存在脱离了对象的行为。就好比说我喜欢撸猫,【喜欢撸猫】这个行为,一定是要有“人存在”作为前提的,不存在着没有执行动作人的行为。

可如果,我只需要行为,不在乎对象是谁呢?

我举一个例子。比如说,有一个列表需要排序,排序是一种行为,但是列表本身没有排序的能力。那么,列表要实现排序这种行为,就要先找一个【排序者】,让它去执行排序这个行为。这是一条很清晰的逻辑线:列表要排序,先找一个【排序人】,让它去排序。这个逻辑看上去很正常,可问题在于,列表根本就不在乎,到底是谁去执行排序这个行为,列表在乎的,只是它需要排序这个行为本身。在这种场景下,我们只需要行为,而不需要去关心拥有行为能力的那个对象是谁。

说得再清晰一些:我们不想传入对象,我们只想传入行为。

这就是 lambda 表达式存在的意义。


所有教程介绍 lambda 表达式,上来就是一通劈头盖脸的匿名内部类代码,你想清楚逻辑就知道为什么要这么介绍。我们的目标是什么,是为了传入行为,而不在乎对象是谁。原来怎么解决的,用匿名内部类,虽然为了传入行为还是传入了对象,但是这个对象是一个匿名的对象。理解了吗,为什么用匿名内部类,是为了用一个匿名的对象传入我们想要的行为,对象是谁不重要,行为本身才重要。

Jdk 8 之后,函数式编程出头了,lambda 表达式出现了,既然对象是谁不重要,那么我们也就不用匿名了,干脆只传行为就行了。

你看这行代码:

1
button.addActionListener(event -> System.out.println("按钮被点击了"));

这一行代码的意思是:在小括号内,我传入了一个行为,而且我只传入了一个行为。

我对于 lambda 表达式的解释到此为止,我建议再去看看这篇知乎问答:《Lambda 表达式有何用处?如何使用?》,写得行云流水,逻辑上非常舒畅。如果看后面的内容觉得云里雾里,可以再回头看一下这篇回答。


当我要开始写 lambda 表达式具体如何使用时,内容分成了两部分:

  1. lambda 表达式怎么使用(语法)
  2. lambda 表达式用在哪里

实际上,我一直都对 lambda 表达式的语法不存疑惑,它在语法上就已经足够让人看懂了,即使是不会写,但也能大致看懂别人写的代码想做什么。我一直以来不理解的地方在于,lambda 表达式到底要用在哪里,当我写哪些代码的时候,可以使用 lambda 表达式?

所以我们先来看,lambda 表达式要用在哪里,lambda 表达式的类型究竟是什么。


我们先搞清楚,lambda 表达式的类型是什么。

在 Java 中,lambda 表达式依旧是一个类,它是一个接口的实现类。

我们来举一个例子,一个列表排序的例子。

  1. 一个排序的场景

    我们自定义一个 Person 类(代码略),该类有两个属性:姓名、年龄。我们实例化三个 Person 对象,然后把这三个 Person 对象放进一个列表里。

    1
    2
    3
    4
    Person person1 = new Person("张三", 21);
    Person person2 = new Person("李四", 20);
    Person person3 = new Person("王五", 22);
    List<Person> personList = Arrays.asList(person1, person2, person3);

    此时这个列表里面有三个对象,它们的排列顺序是往里添加的顺序,也就是:

    1
    // [Person{name='张三', age=21}, Person{name='李四', age=20}, Person{name='王五', age=22}]

    我们现在希望,这个列表里面的三个 Person 对象,能够按照年龄的大小排序。我们可以通过 lambda 表达式来实现排序(暂且先看着,不要理语法):

    1
    2
    3
    personList.sort((a, b) -> a.getAge() - b.getAge());

    // [Person{name='李四', age=20}, Person{name='张三', age=21}, Person{name='王五', age=22}]

    你看,此时这个列表里面,就按照年龄排序了。

  2. 拆解看实现过程

    上面通过 lambda 表达式实现了按照年龄排序,只用了一行代码。实际上这是两行代码合了起来。

    1
    2
    3
    4
    personList.sort((a, b) -> a.getAge() - b.getAge());
    // ↓↓↓ (实际上,上面这行代码,就是下面这两行代码的组合)
    Comparator<Person> comparator = (a, b) -> a.getAge() - b.getAge();
    personList.sort(comparator);

    你观察那句 lambda 表达式,即 (a, b) -> a.getAge() - b.getAge(),它被赋给了 Comparator,而 Comparator 实际上就是一个接口,一个负责排序的接口。

    那么这行代码:Comparator<Person> comparator = (a, b) -> a.getAge() - b.getAge();,它的意思是说:

    • 我们写了一句 lambda 表达式。
    • 这句 lambda 表达式,实际上是 Comparator 接口的一个实现类。

我们来思考一下,通过 lambda 表达式,到底做了一件什么事情。

在通常情况下,某一个方法需要一个接口的具体实现类的对象,那我们就要自己去写一个类,让该类继承接口,实现接口的方法,从而获得一个实现了接口的类,就像下面这样:

1
2
3
4
5
6
7
public class MyComparator implements Comparator {

@Override
public int compare(Object o1, Object o2) {
// 重写方法
}
}

(如果觉得这样子很繁琐,也可以通过匿名内部类的方式去实现。)

而通过 lambda 表达式,我们不再需要自己写一个类出来,让这个类去继承接口实现方法,这非常麻烦,而且会模糊掉我们的目的。我们不再新建一个类,而是使用 lambda 表达式,lambda 表达式本身,就是一个接口的实现类。

这样做最大的好处(在我看来),就是我们无视了对象,直视行为本身。

你重新来看这行排序代码:

1
personList.sort((a, b) -> a.getAge() - b.getAge());

你不用管它是什么意思,实现了什么功能,你是不是能非常直观地看出,小括号里面【在做某件事】,括号里面它是一个很清晰可知的行为。我们通过拆解代码能知道,括号里面的这句 lambda 表达式,实际上是一个类,但是在代码的直观性上,它是什么不重要,在这里,它就代表了一个行为。

简洁,直观。


我再次把 lambda 表达式的类型表述一遍:

lambda 表达式是函数式接口的一个实例,但 Lambda 表达式本身不包含它要实现的函数式接口的信息,这要由它所在的上下文推断出来。

这句话摘自《What is the type of a lambda expression?》。这句话表达了两件事:

  1. lambda 表达式的类型是一个函数式接口
  2. lambda 表达式本身不含接口信息,接口信息是编译器根据上下文猜出来的。

第一条多了一点信息,它说 lambda 表达式的类型不光是接口,还是函数式接口。这个我觉得顺带一提就可以了,函数式接口的意思是,只包含一个抽象方法声明的接口。你设想一下,lambda 表达式的作用是传入行为,一个方法对应一个行为,如果接口中有多个抽象方法,那岂不是就有多个可传入的行为,那一条 lambda 表达式肯定做不到。我们这里是反推了,其实函数式接口的概念应该先于 lambda 表达式。

这第二条,就要说到 lambda 表达式的语法了。


lambda 表达式的基本样板长这样子:

1
2
(      ) -> 
( ) -> { }

左边的小括号放参数,右边要不然没有大括号,放表达式;要不然有大括号,放执行语句。

1
2
3
4
5
()              5
x 2 * x
(x, y) -> x + y
(int x) { return 2 * x; }
(String s) System.out.println(s)

以上是几个例子,大致观察一下就能看出,lambda 表达式是一种崇尚简洁的表达式,大小括号、参数类型、返回体等等都是能省则省,总之能让编译器分析上下文猜出来的,都尽量不写。如果原接口声明了参数类型,那么就可以不在 lambda 表达式中写清楚数据类型,如果原接口声明了返回值类型,并且比较简单,那么在 lambda 表达式中就可以只写一句表达式,而无需写执行语句,编译器会猜到我们想返回这个表达式。

例如,对于这句 lambda 表达式:

1
(x, y) -> x + y

你可以写成这样:

1
(int x, int y) -> { return x + y; }

但是当所有的信息(包括很多冗余信息)都写出来时,会让代码丧失掉直观性,你可能无法看一眼就知道这句 lambda 表达式想做什么。因此在编译器能够分析出来的前提下,应当尽量少写,让 lambda 表达式想传入的行为变得尽可能直观。

本篇就写到这里。