0%

stream and lambda(1) - lambda 简介

java8 开始,lambda 正式在 JAVA 中出现。有人说它类似于其他函数式编程语言中的闭包,有人说它是一种语法糖。让我们来一步一步了解它。

lambda 表达式的结构

lambda 表达式的结构大致如下,同时遵循偷懒原则:能省则省,能不写就不写。
(类型)(参数1, 参数2, …) -> {方法实现}

  • 类型一般情况下可以自动推导出来,此时可以省略不写。如果存在多个类型满足条件,则需指定类型。
  • 可以有零个或多个参数,参数写在圆括号内。
  • 没有参数时只需写 (),只有一个参数且类型可以推导时可以不写圆括号。
  • 参数的类型可以省略不写,根据上下文来推断。例如:(int a)->{}(a)->{}a->{} 效果相同。
  • 方法实现可以有零条或者多条语句,方法实现写在花括号内。
  • 如果方法实现只有一条语句,则花括号可以省略。如:a -> return a>1
  • 如果方法实现只有一条语句,return 关键字可以省略。如:a -> a==1
  • 如果方法实现有多条语句,则需写在花括号内。
  • 如果方法是静态方法、实例方法或者构造器,可简写。如:(int a) -> {return String.valueOf(a)} 等效于 a -> String.valueOf(a),也等效于 String::valueOf

从策略模式开始演化

先给定场景:A市有一批人提交了落户申请,但是A市不像某经济特区,来了还不是A市人,要满足一定的条件才可以落户。

1
2
3
4
5
6
7
//定义居民对象,包含姓名、年龄、社保月数、认证证书级别
public class Citizen {
private String name;
private int age;
private int socialInsuranceMonth;
private int certificateLevel;
}

提供初始化数据方法

1
2
3
4
5
6
7
8
9
10
List<Citizen> initCitizens() {
List<Citizen> citizens = new ArrayList<>();
citizens.add(new Citizen("张三", 23, 11, 0));
citizens.add(new Citizen("李四", 24, 13, 0));
citizens.add(new Citizen("王五", 25, 15, 2));
citizens.add(new Citizen("赵六", 26, 8, 3));
citizens.add(new Citizen("刘七", 35, 12, 3));
citizens.add(new Citizen("宋八", 41, 0, 2));
return citizens;
}

定义数据处理框架方法及处理接口

1
2
3
4
5
6
7
8
9
10
11
12
13
List<Citizen> filterApply(List<Citizen> citizenList, SettleStrategy strategy) {
List<Citizen> resList = new ArrayList<>();
for (Citizen citizen : citizenList) {
if (strategy.test(citizen)) {
resList.add(citizen);
}
}
return resList;
}

public interface SettleStrategy {
boolean test(Citizen citizen);
}

策略模式

假设落户有两种方式:

  1. 不超过40岁且有国家认证的高于2级的证书
  2. 不超过40岁且累计交满12个月社保

定义策略实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CertificateSettleStrategy implements SettleStrategy {
@Override
public boolean test(Citizen citizen) {
return citizen.getAge() <=40 && citizen.getCertificateLevel() > 2;
}
}

public class SocialInsuranceSettleStrategy implements SettleStrategy {
@Override
public boolean test(Citizen citizen) {
return citizen.getAge() <=40 && citizen.getSocialInsuranceMonth() >= 12;
}
}

则使用策略模式的写法大致如下:

1
2
3
4
5
6
7
8
9
public void testStrategy() {
//证书达标的
List<Citizen> citizens = filterApply(initCitizens(), new CertificateSettleStrategy());
System.out.println(citizens);

//社保达标的
citizens = filterApply(initCitizens(), new SocialInsuranceSettleStrategy());
System.out.println(citizens);
}

点评:

从上述代码可以看出,使用策略模式时,要求必须有指定的策略类。

虽然好处是有新策略可以直接添加一个策略处理类,无需反复修改处理逻辑。

但是仍然面临着策略变化,必须增加新策略类的问题。

匿名内部类

使用匿名内部类,可无需再定义策略处理类。可能你在某些业务地方用过,比如往线程池里丢一个 Runnable,你可能不会再单独定义一个子类。

1
2
3
4
5
6
7
8
9
10
public void testAnonymous() {
List<Citizen> citizens = filterApply(initCitizens(), new SettleStrategy() {
@Override
public boolean test(Citizen citizen) {
return citizen.getAge() <=40 && citizen.getSocialInsuranceMonth() >= 12;
}
});

System.out.println(citizens);
}

点评:

从上述代码可以看出,使用匿名内部类时,可以允许自己不再定义单独的类,但还需显式的 new 子类并 override

相比策略模式,代码更集中,也不必在项目里维护太多的类。缺点则是当处理逻辑较复杂时,写法不优雅,看起来会杂乱。

此时,比如在 intellij idea 里,应该会提示你代码可以 replace with lambda

lambda 表达式

lambda 的写法会简洁不少,遵循上面说的偷懒原则,省略类型、省略参数、省略括号。

1
2
3
4
5
public void testLambda() {
List<Citizen> citizens = filterApply(initCitizens(),
citizen -> citizen.getAge() <=40 && citizen.getSocialInsuranceMonth() >= 12);
System.out.println(citizens);
}

点评:

相比匿名内部类,lambda 更近一步。此时,不用再出现 SettleStrategy 字样,不必再出现 test 方法字样。

既然如此,那么,此时就剩最后一个问题了:为什么还要定义 SettleStrategy 接口呢?

stream

stream 提供了一系列的便捷处理方法,java8 也提供了一系列的通用的函数式接口,让我们免去定义一些不必要的接口类。当然,stream 与函数式接口不在本篇的内容范围内。

通过 stream 的写法,可以解决 lambda 遗留的那个问题。

1
2
3
4
5
6
7
public void testStream() {
Citizen first = initCitizens().stream()
.filter(citizen -> citizen.getAge() < 35)
.findFirst()
.orElse(null);
System.out.println(first);
}

看,链式调用,是不是很简洁易读?

lambda 表达式与匿名内部类的区别

从上面的介绍可以看出,其实 lambda 表达式和匿名内部类很相似,相似到只是写法稍作改变。如果从上面所说的偷懒原则来看,lambda 表达式就是匿名实现了函数式接口,并把匿名内部类一步步的偷懒简化。但是二者从本质上来讲,又是不同的。

匿名内部类,本质还是一个类,实例化的是一个对象;lambda 表达式从根本上来说,是一种编译器会特定处理的语法。

从使用上来看,匿名内部类还是要 new 出来一个实例化对象的,而 this 关键字在匿名内部中使用时,指的是内部类,而非外部类;对于 lambda 表达式,this 关键字则代表的是外部类(lambda所在的类)。从编译输出来看,当使用匿名内部类时,会编译出一个单独的内部类的 class;而 lambda 表达式则不会编译出单独的 class 出来。

总结

本文总结了 lambda 表达式的结构,同时从策略模式说起,一步步的简化,逐步引出了 lambda 表达式,并比较不同写法之间的差异性,同时稍稍介绍了 stream。

lambda 表达式可以让我们不用去费力的给一个方法、或者一个类命名,让我们可以以匿名的方式去使用它。但是如果匿名方法里的处理逻辑太复杂,则不建议使用 lambda 表达式去写,lambda 所表达的内容应该是简单易读的。

注:本文配套代码可在 github 查看:stream-and-lambda