想要理解Lambda的使用,我们先要弄清楚什么是函数式接口和函数式编程。
函数式接口:有且仅有一个抽象方法的接口。
函数式编程:把运算过程写成嵌套函数调用。
总之,函数式编程就是,当我想实现一个功能时,我只想写这个功能的实现过程,即一个函数,其他的我都不想管。这个听起来容易,但是实际操作起来,却往往要多写很多无关紧要时代码。我们现在理解不了没关系,我们先看一个例子。我们模拟一个老板叫一个员工干活的场景:
先有一个员工接口(假设员工此时还没找到,所以只有接口,我们将来还要招聘员工):
public interface Emploee {
void doSomething();
}
然后我们还有一个boss类,boss可以在主方法中调用ask方法叫员工去干活:
public class Boss {
public static void main(String[] args) {
}
public static void ask(Emploee emploee){
emploee.doSomething();
}
}
现在,boss想叫员工去倒杯茶,该怎么实现呢?我们应该先招聘一个员工,再让他学会倒茶这项技能,然后他就能给boss倒茶了。所以我们先实现接口并重写doSomething方法:
public class RealEmploee implements Emploee{
@Override
public void doSomething() {
System.out.println("给我倒杯茶!");
}
}
然后我们在boss类的主函数中实例化该类,并调用doSomething方法:
public class Boss {
public static void main(String[] args) {
Emploee emploee = new RealEmploee();
ask(emploee);
}
public static void ask(Emploee emploee){
emploee.doSomething();
}
}
然后我们就能在控制台上看到输出的给我倒杯茶!
了。我们看到,为了实现小小一个功能,我们写了很多代码。其实,我们可以用匿名内部类进行优化,我们就不直接实现接口了。
public class Boss {
public static void main(String[] args) {
ask(new Emploee() {
@Override
public void doSomething() {
System.out.println("给我倒杯茶!");
}
});
}
public static void ask(Emploee emploee){
emploee.doSomething();
}
}
其实,这个时候代码看起来已经好多了,我们想要完成倒茶这个操作我们只增加了以下代码:
ask(new Emploee() {
@Override
public void doSomething() {
System.out.println("给我倒杯茶!");
}
});
但是,这并不是简约的。如果boss再叫员工去买烟、又或者去拿快递,我们就要再写以下代码。
ask(new Emploee() {
@Override
public void doSomething() {
System.out.println("给我去买烟!");
}
});
ask(new Emploee() {
@Override
public void doSomething() {
System.out.println("给我去拿快递!");
}
});
我们发现,有大量的代码重复,真正在改变的,是doSomething函数的实现。
以下代码都是重复的:
ask(new Emploee() {
@Override
public void doSomething() {
}
});
所以,为了解决这个问题,Lambda来了。用Lambda表达改造后,代码变成了这样:
public class Boss {
public static void main(String[] args) {
ask(()->{
System.out.println("给我倒杯茶!");
});
}
public static void ask(Emploee emploee){
emploee.doSomething();
}
}
初看Lambda表达式也许会很懵逼
我们来看一下Lambda表达式的标准格式:
1. 一些参数
2. 一个箭头
3. 一段代码
格式:
- (参数列表) -> {一些重写方法的代码};
解释说明格式:
- ():接口中抽象方法的参数列表,没有参数,就空着;有参数就写出参数,多个参数使用逗号分隔
- ->:传递的意思,把参数传递给方法体{}
- {}:重写接口的抽象方法的方法体
所以,我们把最重要的部分抽取了出来。我们的最终目的是想要实现函数的功能,现在,我们实现了,这就行了,我们就不去实现接口,重写方法了。当然,在底层,这些都是会实现的,但是,不再需要我们自己写了,这一切都交给编译器,我们只关注函数本身。现在我们再看看函数式编程的定义:
函数式编程:把运算过程写成嵌套函数调用。
所以说,在Java中,函数式编程体现就是Lambda。
这样的话,是不是以后我们想要实现接口的抽象方法都可以这样写呢?当然不是,Lambda的使用是有条件的。
- 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法,即文章开头所说的函数式接口。
想象一下,如果接口中有多个抽象方法,编译器怎么知道我们想要实现哪一个?我们再看一下lambda的格式:(参数列表) -> {一些重写方法的代码};
,我们有指明重写哪一个方法吗?显然没有,所以为了避免找不到方法的情况,要求接口中有且仅有一个抽象方法。 - 使用Lambda必须具有上下文推断。
也就是方法的参数类型必须为Lambda所要求的接口类型,才能使用Lambda作为该接口的实例。
Emploee便是函数式接口。public static void ask(Emploee emploee){ emploee.doSomething(); }
注意事项:
假设boss想叫员工导5杯茶,初学者可能会写出这样的代码:
public class Boss {
public static void main(String[] args) {
for (int i = 1; i <= 5; i++) {
ask(()->{
System.out.println("给我第" + i + "倒杯茶!");
});
}
}
public static void ask(Emploee emploee){
emploee.doSomething();
}
}
但是我们发现编译错误了
我们翻译一下编译错误信息:lambda表达式中使用的变量应该是final或者有效的final。也就是说,lambda表达式中使用的局部变量应该是final修饰的量或者不会再改变的量。
其实,这个约束本来是针对匿名内部类的使用的,由于lambda表达式是由匿名内部类演变而来,所以对lambda表达式也有同样的约束。
解决方法:
- 将变量定义为全局变量:
public class Boss {
static int i;
public static void main(String[] args) {
for (i = 1; i <= 5; i++) {
ask(()->{
System.out.println("给我第" + i + "倒杯茶!");
});
}
}
public static void ask(Emploee emploee){
emploee.doSomething();
}
}
- 给变量加final关键字:
public class Boss {
public static void main(String[] args) {
for (int j = 1; j <= 5; j++) {
final int i = j;
ask(()->{
System.out.println("给我第" + i + "倒杯茶!");
});
}
}
public static void ask(Emploee emploee){
emploee.doSomething();
}
}
注意,其实即使我们在这里不加final关键字,也还是能编译通过并成功运行,因为Java 8 之后,在匿名类或 Lambda 表达式中访问的局部变量,如果不是 final 类型的话,编译器自动加上 final 修饰符。