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 表达式具体如何使用时,内容分成了两部分:
- lambda 表达式怎么使用(语法)
- lambda 表达式用在哪里
实际上,我一直都对 lambda 表达式的语法不存疑惑,它在语法上就已经足够让人看懂了,即使是不会写,但也能大致看懂别人写的代码想做什么。我一直以来不理解的地方在于,lambda 表达式到底要用在哪里,当我写哪些代码的时候,可以使用 lambda 表达式?
所以我们先来看,lambda 表达式要用在哪里,lambda 表达式的类型究竟是什么。
我们先搞清楚,lambda 表达式的类型是什么。
在 Java 中,lambda 表达式依旧是一个类,它是一个接口的实现类。
我们来举一个例子,一个列表排序的例子。
一个排序的场景
我们自定义一个 Person 类(代码略),该类有两个属性:姓名、年龄。我们实例化三个 Person 对象,然后把这三个 Person 对象放进一个列表里。
1
2
3
4Person 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
3personList.sort((a, b) -> a.getAge() - b.getAge());
// [Person{name='李四', age=20}, Person{name='张三', age=21}, Person{name='王五', age=22}]你看,此时这个列表里面,就按照年龄排序了。
拆解看实现过程
上面通过 lambda 表达式实现了按照年龄排序,只用了一行代码。实际上这是两行代码合了起来。
1
2
3
4personList.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 | public class MyComparator implements Comparator { |
(如果觉得这样子很繁琐,也可以通过匿名内部类的方式去实现。)
而通过 lambda 表达式,我们不再需要自己写一个类出来,让这个类去继承接口实现方法,这非常麻烦,而且会模糊掉我们的目的。我们不再新建一个类,而是使用 lambda 表达式,lambda 表达式本身,就是一个接口的实现类。
这样做最大的好处(在我看来),就是我们无视了对象,直视行为本身。
你重新来看这行排序代码:
1 | personList.sort((a, b) -> a.getAge() - b.getAge()); |
你不用管它是什么意思,实现了什么功能,你是不是能非常直观地看出,小括号里面【在做某件事】,括号里面它是一个很清晰可知的行为。我们通过拆解代码能知道,括号里面的这句 lambda 表达式,实际上是一个类,但是在代码的直观性上,它是什么不重要,在这里,它就代表了一个行为。
简洁,直观。
我再次把 lambda 表达式的类型表述一遍:
lambda 表达式是函数式接口的一个实例,但 Lambda 表达式本身不包含它要实现的函数式接口的信息,这要由它所在的上下文推断出来。
这句话摘自《What is the type of a lambda expression?》。这句话表达了两件事:
- lambda 表达式的类型是一个函数式接口
- lambda 表达式本身不含接口信息,接口信息是编译器根据上下文猜出来的。
第一条多了一点信息,它说 lambda 表达式的类型不光是接口,还是函数式接口。这个我觉得顺带一提就可以了,函数式接口的意思是,只包含一个抽象方法声明的接口。你设想一下,lambda 表达式的作用是传入行为,一个方法对应一个行为,如果接口中有多个抽象方法,那岂不是就有多个可传入的行为,那一条 lambda 表达式肯定做不到。我们这里是反推了,其实函数式接口的概念应该先于 lambda 表达式。
这第二条,就要说到 lambda 表达式的语法了。
lambda 表达式的基本样板长这样子:
1 | ( ) -> |
左边的小括号放参数,右边要不然没有大括号,放表达式;要不然有大括号,放执行语句。
1 | () 5 |
以上是几个例子,大致观察一下就能看出,lambda 表达式是一种崇尚简洁的表达式,大小括号、参数类型、返回体等等都是能省则省,总之能让编译器分析上下文猜出来的,都尽量不写。如果原接口声明了参数类型,那么就可以不在 lambda 表达式中写清楚数据类型,如果原接口声明了返回值类型,并且比较简单,那么在 lambda 表达式中就可以只写一句表达式,而无需写执行语句,编译器会猜到我们想返回这个表达式。
例如,对于这句 lambda 表达式:
1 | (x, y) -> x + y |
你可以写成这样:
1 | (int x, int y) -> { return x + y; } |
但是当所有的信息(包括很多冗余信息)都写出来时,会让代码丧失掉直观性,你可能无法看一眼就知道这句 lambda 表达式想做什么。因此在编译器能够分析出来的前提下,应当尽量少写,让 lambda 表达式想传入的行为变得尽可能直观。
本篇就写到这里。