JDK 8 可以说是自 Java 发布以来的最大的一次改动, 而这里一系列的的改动中 lambda 可以说是让Java这个
一板一眼的中年大叔变成了油腻男,少一些古板,多了一些灵活。这也是对吐槽 Java 语法繁杂,不够灵活的有力回击。
以下是本文目录,按需食用。
什么是Lambda,它解决了什么问题?
再讲 Lambda之前,我们需要先回顾一个概念,匿名内部类
:
没有名称的实现了某个接口的类成为匿名内部类
。通常用在某个方法里面通过new
关键字创建,一般
来讲,这种类没有太过复杂的聚合逻辑,只为了解决某个单一的逻辑问题,比如排序、线程。
我们通过一个例子,看看Java在处理这一场景下的演进。
- 内部类
private static class IntegerComparator implements Comparator {
@Override
public int compare(Integer a, Integer b) {
return b - a;
}
}
List list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
Collections.sort(list, new IntegerComparator());
log.info("Sorted of list: {}", list);
通常来讲我们不会这样使用,因为 IntegerComparator
只是一次性使用, 没有复用性的需求,所以这么操作,
又点笨重,所以才有了匿名内部类
- 匿名内部类
List list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
Collections.sort(list, new Comparator() {
@Override
public int compare(Integer a, Integer b) {
return b - a;
}
});
log.info("Sorted of list: {}", list);
但是相比内部静态类,只是内联了代码,本质上并没有减少代码量,所以这也是 Lambda 推出的主要原因。
- Lambda
List list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
//list.sort(Integer::compareTo);
list.sort((a, b) -> b - a);
log.info("Sorted of list: {}", list);
可以看出,Lambda 语法之前,其实 Java 已经在优化匿名内部类的场景下的使用体验,只不过,在借鉴了
其他语言的语法特性之后,Lambda 表达式做的更加彻底,它通过剔除样本代码,仅保留了逻辑代码,大幅提升了书写和阅读的体验。
所以 Lambda 主要改进了 匿名内部类的书写体验。
Lambda 的基本概念
在数学和计算机科学中, Lambda 表达式(维基百科也称为高阶函数)
是指满足下列至少一个条件的函数:
- 接收一个或多个函数作为输入
- 输出一个函数
在 Java 8 之前,是没有办法写一个独立的函数,方法经常被用来替代函数,但是他们总是作为对象或者方法的
一部分。现在 Lambda 表达式提供了一种更为接近独立函数概念的方法。作为替代 匿名内部类,它具有如下优点:
- 语法简介紧凑,允许省略
访问修饰符
、返回类型
、参数类型
等。 - 参数行为话,提升代码复用性
- 大量开箱即用的
函数式工具
,为 Lambda 带来通用场景下的开箱即用的工具
语法
Lambda 表达式由 参数列表和方法体组成,用 ->
隔开,所以下面这些都是合法的
//单个参数
p -> p.translate(1,1)
i -> new Point(i, i+1)
//一个或者多个参数,用()表示与普通方法一致
() -> 16
(x, y) -> x +y
//设置参数类型,通常可以省略,因为可以从其实现的接的方法声明中推断出来,
(int x, int y) -> x +y
//jdk 11 中增强了 var可以在 lambda中使用
(var x, var y) -> x +y
如果我们用抽象语法来表述 lambda 表达式,是这样的:
args -> expr
//有时候如果 方法由返回值且实现逻辑较为复杂,无法使用单行表示,可以这样写
args -> { return expr;}
函数式接口
要使用 lambda 表示,必须要继承 函数式接口
,即只有一个抽象方法的接口,当某个接口满足这样的条件,就可以使用 Lambda 表达式,
在 JDK 8 中 在接口上添加注解 @FunctionalInterface
来表示某个接口是一个函数式接口
接口。我们可以在 java.util.function
包下查看所以 JDK 8 开箱即用的函数式接口。
函数接口 | 表达式 | 解释 |
---|---|---|
Function |
T -> R | 接受参数T,生成结果R |
BiFunction |
(T, U) -> R | 接受参数T,U,生成结果R |
Predicate |
T ->Boolean | 接受参数T,返回布尔值 |
Supplier |
() ->T | 生产者模式 |
Consumer |
T ->() | 消费者模式 |
除了这些函数式接口,还有其他很多衍生接口,不过大多数是为了提升原始类型参数的性能而专门创建的一些接口,这里主要
介绍一下 Supplier
, Consumer
的使用场景,以了解 Lambda的设计思想。
- Consumer
@Slf4j
public class ConsumerDemo {
public static void main(String[] args) {
//打印参数
Consumer printer = t -> System.out.println(t);
//正如Consumer的文档所说, 它是通过副作用来操作的。
// 这里展示通过副作用操作对集合中的所有元素进行处理
Consumer> odd = list -> {
for (int i = 0; i < list.size(); i++)
list.set(i, 2 * list.get(i));
};
printer.accept("Hello,");
//可以作为参数传递到其他方法中,这对于提升和弥补现有的 Method 的复用性有很大改善。
debug("Word", printer);
var list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
odd.accept(list);
log.info("{}", list);
}
public static void debug(String msg, Consumer consumer) {
consumer.accept(msg);
}
}
- Supplier
这个例子通过Supplier 创建一个随机数生成器
@Slf4j
public class SupplierDemo {
//RandomGenerator
public static final Random random = new Random(10086L);
public static void main(String[] args) {
//打印随机数
Consumer printer = t -> log.info("{}", t);
//生成随机数
Supplier randomGenerator = () -> random.nextInt(0, 100);
//直接使用
printer.accept(randomGenerator.get());
//通过方法调用
printer.accept(randomInt(randomGenerator));
printer.accept(randomInt(randomGenerator));
}
public static Integer randomInt(Supplier randomGenerator) {
return randomGenerator.get();
}
}
方法和构造器引用
Lambda 相比匿名内部类已经足够简介了,但是我们可以更加简洁
//打印
Consumer> printer = t -> System.out.println(t);
//方法引用
Consumer> printer = System.out::println;
var list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
//对列表进行由小到大排序
list.sort((o1, o2) -> o1- o2);
//方法引用
list.sort(Integer::compare);
通过代码,我们确实发现 Lambda 表达式更加简洁了,这种写法有一个名字,称之为 方法引用
,是随着 JDK 8 一起引入的。 方法引用
可以分为以下四类。
名称 | 语法 | Lambda 等同于发 |
---|---|---|
静态方法引用 | RefType::staticMethod | (args)->RefType.staticMethod(args) |
实例方法引用 | expr::instMethod | (args)->expr.instMethod(args) |
未绑定的事例方法引用 | RefType::staticMethod | (arg0, rest)-> arg0.instMethod(rest) |
构造器引用 | ClsName::new | (args)-> new ClsName(args) |
#### 静态方法引用
静态方法引用只需要将 class
和 static method()
使用 ::分隔开,比如
ToIntFunction toInt = str -> Integer.valueOf(str);
//方法引用
ToIntFunction toInteger = Integer::valueOf;
var list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
list.sort((o1, o2) -> o2 - o1);
//方法引用
list.sort(Integer::compare);
实例方法引用
实力方法引用,分为绑定实例方法引用
和未绑定的事例方法引用
。
绑定实例方法引用
和静态方法非常接近,使用 ObjectReference::Identifier
代替 ReferType::Identifier
str -> System.out.print(str)
//方法引用
System.out::print
````
非绑定方法,在上下文中不需要绑定一个接收器(上例中`System.out`对象)
user -> user.getName()
//方法引用
user::getName
#### 构造器引用
```java
Stream stringStream = Stream.of("a.txt", "b.txt", "c.txt");
Stream fileStream = stringStream.map(str-> new File(str))
//引用方法
Stream fileStream = stringStream.map(File::new);
```
## Lambda参数行为化
在 Java 8 之前,方法参数是无法传递方法,只能传递状态,因此很多行为相似的代码由于无法复用,而导致了大量的代码重复, 而 Lambda 的
引入则解决了这个问题,通过将重复和相近的代码逻辑抽象到精心设计和实现的函数式接口中,可以实现方法的复用。
这里我们以文章发布作为最后综合例子,展示一下 Lambda 参数行为化所带来的更进一步的逻辑服用。
```java
@Slf4j
public class PredicateDemo {
// 不再需要了
public void publish(Long articleId) {
var article = get(articleId);
if (Objects.equals(article.getPublished(), Boolean.FALSE)) {
log.info("Publish article: {}", article.getId());
} else {
// Error log or just omit
}
}
//不再需要了
public void unpublish(Long articleId) {
var article = get(articleId);
if (Objects.equals(article.getPublished(), Boolean.TRUE)) {
log.info("UnPublish article: {}", article.getId());
} else {
// Error log or just omit
}
}
// Lambda 的参数行为化带来API实现层面的统一的可能
//通过 Predicate 来处理 文章的发布和取消发布逻辑,以及异常处理
public void doPublish(Long articleId, Boolean status, Predicate predicate) {
var article = get(articleId);
if (predicate.test(article)) {
article.setPublished(status);
log.info("Set article status to :{}", status);
} else {
log.error("Illegal Status of Article {} to set:{} ", article, status);
}
}
public static void main(String[] args) {
PredicateDemo predicateDemo = new PredicateDemo();
//正确的测试用例
predicateDemo.doPublish(1L, Boolean.TRUE, p -> Objects.equals(p.getPublished(), Boolean.FALSE));
predicateDemo.doPublish(2L, Boolean.FALSE, p -> Objects.equals(p.getPublished(), Boolean.TRUE));
//失败的测试用例
predicateDemo.doPublish(1L, Boolean.TRUE, p -> Objects.equals(p.getPublished(), Boolean.TRUE));
predicateDemo.doPublish(2L, Boolean.FALSE, p -> Objects.equals(p.getPublished(), Boolean.FALSE));
}
public Article get(Long id) {
return findAll().get(id);
}
public static Map findAll() {
return Map.of(
1L, new Article(1L, "测试1", false),
2L, new Article(2L, "测试2", true)
);
}
}
@Getter
@Setter
@AllArgsConstructor
@ToString
class Article {
private Long id;
private String name;
private Boolean published;
}
```
## 总结
到这里,Lambda 的介绍就讲完了,剩下的就是需要各位理解 Lambda的 思维方式,并且熟悉 `java.util.function`下的所有函数式接口,然后在自己项目中
小范围的使用起来,理解并掌握了 Lambda 的思维方式,可以显著的提升使用 Java 编程的愉悦感。
同时由于 `Lambda` 表达式由于在改进 `匿名内部类` 所带来的巨大提升,使得在 Java 8 中 引入 `Stream` 成为可能,不过那是另一话题了。
## 函数式接口附录
| Current interface | Perferred interface |
| ------------------------ | -------------------- |
| Function | IntFunction |
| Function | LongFunction |
| Function | DoubleFunction |
| Function | DoubleToIntFunction |
| Function | DoubleToLongFunction |
| Function | LongToDoubleFunction |
| Function | LongToIntFunction |
| Function | ToIntFunction |
| Function | ToLongFunction |
| Function | ToDoubleFunction |
| Function | UnaryOperator |
| BiFunction | BinaryOperator |
| Consumer | IntConsumer |
| Consumer | DoubleConsumer |
| Consumer | LongConsumer |
| BiConsumer | ObjIntConsumer |
| BiConsumer | ObjLongConsumer |
| BiConsumer | ObjDoubleConsumer |
| Predicate | IntPredicate |
| Predicate | DoublePredicate |
| Predicate | LongPredicate |
| Supplier | IntSupplier |
| Supplier | DoubleSupplier |
| Supplier | LongSupplier |
| Supplier | BooleanSupplier |
| UnaryOperator | IntUnaryOperator |
| UnaryOperator | DoubleUnaryOperator |
| UnaryOperator | LongUnaryOperator |
| BinaryOperator | IntBinaryOperator |
| BinaryOperator | LongBinaryOperator |
| BinaryOperator | DoubleBinaryOperator |
| Function | Predicate |