自Java8以来,改动最大的就是添加了lambda表达式新特性。它最大的作用就是保证我们代码的简洁性,提高了可读性。但是在函数式中,也充斥着一些语法糖,我们只有理解这些所谓魔法背后的原理,才能更好的运用这些新特性,快速完成工作,节约我们的生命。
简述函数式
函数式接口,即只有一个声明方法的接口。我们知道的是在Java8之前,如果我们要实现一个筛选功能的接口,可以使用一个传递匿名内部类的方式,如下例子所示:
假设我们定义了一个接口去实现筛选的功能
@FunctionalInterface
interface MyFunction {
boolean test(Apples apple);
}
在实现的类中使用了一个匿名内部类
public List<Apples> filterApple(List<Apples> applesList) {
List<Apples> targetApples = new ArrayList<>();
findApple(applesList, new MyFunction() {
@Override
public boolean test(Apples apple) {
return apple.getWeight() > 100;
}
});
return targetApples;
}
private List<Apples> findApple(List<Apples> applesList, MyFunction myFunction) {
List<Apples> appleList = new ArrayList<>();
for (Apples apple : applesList) {
if (myFunction.test(apple)) {
appleList.add(apple);
}
}
return appleList;
}
可以看到我们在使用自定义接口的时候,在之上添加了一个注解,即@FunctionalInterface
函数式接口,它没有任何实际意义。它仅仅是表明了这个接口为函数式接口。
函数式接口的明确定义是,只有一个抽象方法的接口
。它还有一些其他特性:
- 可以包含多个static和default方法。
- 可以从Object中继承的抽象方法。
- @FunctionalInterface作为可选可不选。
针对以上特性来说,我们可以简化之前的代码:
public List<Apples> filterApple(List<Apples> applesList) {
List<Apples> targetApples = new ArrayList<>();
findApple(applesList, apple -> apple.getWeight() > 100);
return targetApples;
}
可以看到,原来臃肿的代码,瞬间变得清爽了许多。这里解释下lambda这个神奇的魔法。在findApple()
这个方法中,传入的第二个参数使用了lambda表达式,因为MyFunction
中只有一个抽象方法,所以它很自然的匹配到了其中的 test
方法,apple
参数即方法的入参,符合方法签名,返回参数为boolean
型,也符合函数签名。所以这时候其中的魔法也就不言而喻了。
另外,如果我们打开lambda的字节码,可以看到它背后原理
private static synthetic lambda$filterApple$0(Lcom/wzn/lambda/Apples;)Z
可以看到,函数式执行的方式,实际上是自己生成了一个方法。这个方法是static
的,然后还有一个关键词是synthetic
,它表示JVM合成的方式。如果有兴趣,可以打开类Modifier
,来查看其中的控制访问符和关键字。
另外还有函数式接口的调用使用的是INVOKEDYNAMIC
指令调用,它的含义是动态调用,说明了lambda的调用方式是运行时调用。
但是,这时候又有了新的问题,为什么很多时候我们没有自己写过函数式接口,但是我们却经常在使用函数式代码呢?
这时候,我们应该能想到的是,肯定有人在背后为我们做了这件事情,以至于,我们可以直接使用它,而不必在重复的去定义它。
四大函数式接口
Consumer | Function | Predicate | Supplier |
---|---|---|---|
void accept(T t); | R apply(T t); | boolean test(T t); | T get(); |
依据之上表格中的函数接口,在Java中组合出来了很多的函数式接口供我们使用。
可以看到,在Consumer
中,接收了一个泛型参数,不返回值。使用匿名内部类我们可以写这样的代码
new Consumer<Apples>(){
@Override
public void accept(Apples o) {
System.out.println("123");
}
};
使用lambda表达式我们可以这样写代码
Consumer<Apples> consumer = (apples) -> System.out.print(apples);
其他的四大函数式接口的表达方式都可以有类似的使用方式。
函数式接口的一些特性
如果写之下的代码看起来是一个正常的流程,但实际上它是有误的。
//错误的代码
int i =0;
Consumer<Apples> consumer = (apples) -> i++;
在lambda表达式中的变量实际上是变量的拷贝。
在idea中,如果鼠标悬浮你会看到如下的提示,它表示表达式应当被声明为final。
![](https://img.haomeiwen.com/i4926805/8247846c4b83806c.png)
可以根据idea的提示修改代码为
AtomicInteger i = new AtomicInteger();
Consumer<Apples> consumer = (apples) -> i.getAndIncrement();
这和多线程的可见性有相同的特性。
方法引用
有时候我们会发现,当lambda表达式中{}
的内容过长,会对可读性有很大的影响,比如:
applesList.forEach((apple -> {
System.out.println(apple.getColor());
System.out.println(apple.getWeight());
}));
当在大括号中有更多的业务逻辑其实在用Lambda是对我们程序可读性的一种伤害,这时候就得使用提供公有方法的重构手法,把中间的逻辑提取出来。
applesList.forEach((apple -> {
printFields(apple);
}));
之上的代码同样等价于
applesList.forEach((Apples::printFields))
这样是不是更加简洁清晰了,我们都可以看到,在foreach中的方法实现使用了Consumer
参数
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
Consumer
接口的方法是传递一个参数,不返回值。还有个问题是
private void printFields() {
System.out.println(this.getColor());
System.out.println(this.getWeight());
}
但这个方法并没有传入参数?实际上,在Java中所有的实例方法都隐藏了一个this参数。
上面的代码即
private void printFields(Apples this) {
System.out.println(this.getColor());
System.out.println(this.getWeight());
}
方法引用的核心是方法可以自动被转化为函数式接口。有了它,我们的代码将更加的简洁。
网友评论