Lambda表达式实战分析 上篇
lambda表达式是Java 8中最大语言改变了,允许我们将函数当作参数传递给其他方法,简而言之就是实现了:行为参数化。
lambda表达式是函数式编程里重要的概念。现在我尝试通过一个例子来说明Lambda表达式的演化过程
一个果农的需求变化
有个应用程序是帮助果农了解自己的库存的。这位果农可能想有一个查找库存中所有绿色苹果的 功能。但到了第二天,他可能会告诉你:其实我还想找出所有重量超过150克的苹果。又过了两天,果农又跑回来补充道:要是我可以找出所有既是绿色,重量也超过150克的苹果,那就太棒了。
你要如何应对这样不断变化的需求?
需求变更过程
1. 果农查找库存中所有绿色苹果
实现代码如下:
/**
* 筛选绿苹果
* @param rawAppleList
* @return
*/
private static List<Apple> filterGreenApples(List<Apple> rawAppleList){
List<Apple> result = new ArrayList<>();
if(rawAppleList == null || rawAppleList.isEmpty()){
return result;
}
for(Apple apple : rawAppleList){
if("Green".equalsIgnoreCase(apple.getColor())){
result.add(apple);
}
}
return result;
}
2. 果农查找库存中所有红色苹果
需求从查找绿苹果变成了查找红苹果,可以推测之后农户有可能要查询黄苹果,因此新增一个颜色入参
/**
* 筛选任意一颜色的苹果
* @param rawAppleList
* @param color
* @return
*/
private static List<Apple> filterApplesWithColor(List<Apple> rawAppleList, String color){
List<Apple> result = new ArrayList<>();
if(rawAppleList == null || rawAppleList.isEmpty()){
return result;
}
for(Apple apple : rawAppleList){
if(color.equalsIgnoreCase(apple.getColor())){
result.add(apple);
}
}
return result;
}
3. 果农查找库存中超过150克的重苹果
方法的入参增加一个形参:重量
/**
* 筛选出大苹果
* @param rawAppleList
* @param weight
* @return
*/
private static List<Apple> filterApplesWithWeight(List<Apple> rawAppleList, int weight){
List<Apple> result = new ArrayList<>();
if(rawAppleList == null || rawAppleList.isEmpty()){
return result;
}
for(Apple apple : rawAppleList){
if(weight < apple.getWeight()){
result.add(apple);
}
}
return result;
}
4. 果农查找库存中的绿苹果和超过150克的重苹果
引入表示重量和颜色的入参
/**
* 通过颜色和重量筛选苹果
* @param rawAppleList
* @param color
* @param weight
* @return
*/
private static List<Apple> filterApplesByProperty(List<Apple> rawAppleList, String color, int weight){
List<Apple> result = new ArrayList<>();
if(rawAppleList == null || rawAppleList.isEmpty()){
return result;
}
for(Apple apple : rawAppleList){
if(color != null && weight > 0) {
if (weight < apple.getWeight() && color.equalsIgnoreCase(apple.getColor())) {
result.add(apple);
}
}
}
return result;
}
果农之后想对苹果的不同属性做筛选,比如大小、形状、产地,品种等,怎么办?
如果这位果农想多个属性组合查询,做更复杂的查询,比如红色的来自福建的大苹果,又该怎么办?
应对这些需求,需要给方法里添加很多参数,有没有更好的封装?
接口
上述过程,复制了大部分的代码来实现遍历查询。如果你想要改变筛选遍历方式来提升性能呢? 那就得修改所有方法的实现,而不是只改一个,这代价太大了。我们需要复用,我们需要抽象。
提取抽象要确定抽象的部分,确定要求我们对需求的变更和未来的演化过程有清晰的认识,基于这种认识来确定抽象。
使用接口定义一个抽象方法,不同的判断条件交给不同的实现类来完成。一次判断条件看作一种行为,不同的实现类表达不同的筛选行为。
/**
* 筛选苹果
* @param rawAppleList
* @param predicate
* @return
*/
private static List<Apple> filterApples(List<Apple> rawAppleList, IApplePredicate predicate){
List<Apple> result = new ArrayList<>();
if(rawAppleList == null || rawAppleList.isEmpty()){
return result;
}
for(Apple apple : rawAppleList){
if(predicate.test(apple)){
result.add(apple);
}
}
return result;
}
此时,filterApples方法的行为的差异取决于我们传给IApplePredicate的实现类的行为,filterApples行为被参数化了。
匿名内部类
在使用过程中,发现如果判断条件有很多种,会生成多个实现类。使用匿名内部类能化简代码,减少啰嗦
List<Apple> apples = filterApples(RAW_APPLE_LIST, new IApplePredicate() {
@Override
public boolean test(Apple apple) {
if ("Red".equalsIgnoreCase(apple.getColor())
&& 150 < apple.getWeight()
&& "福建".equals(apple.getProducingArea())) {
return true;
}
return false;
}
});
果农需要查询什么,我传入不同筛选条件的匿名内部类。
使用Lambda表达式
使用匿名内部类存在一段模板代码,我们希望连这样的模板代码都没有,让代码更简洁些。
然后,简洁性和易读性这两者看起来存在矛盾,简洁就意味着代码量少,往往也带来不易读。
Oracle工程师提出了一种处理方式: Lambda表达式来化解简洁性和易读性的冲突。
filterApples(RAW_APPLE_LIST, (Apple apple) -> {
if("Green".equalsIgnoreCase(apple.getColor())
&& 150 > apple.getWeight()){
return true;
}
return false;
});
lambda表达式详解
lambda表达式组成部分
使用匿名内部类和lambda表达式对比
private static void testFilterAnonymous(){
filterApples(RAW_APPLE_LIST, new IApplePredicate() {
@Override
public boolean test(Apple apple) {
return "Green".equalsIgnoreCase(apple.getColor()) && 150 > apple.getWeight();
}
});
}
private static void testFilterApplesLambda(){
filterApples(RAW_APPLE_LIST, (Apple apple) -> "Green".equalsIgnoreCase(apple.getColor()) && 150 > apple.getWeight());
}
lambda表达式由参数,箭头和方法体三部分组成.
一个方法有入参,返回值和方法实现。匿名内部类的里的test方法的入参,作为lambda箭头左侧部分,(当入参达到两个以上时使用圆括号),test方法实现作为了lambda箭头右侧部分,test方法的返回 值用右侧表达式返回值来表达。当需要多条语句时,需要用花括号{},并显示声明返回的类型
apple -> {
println("");
return "Green".equalsIgnoreCase(apple.getColor()) && 150 > apple.getWeight();
};
lambda表达式是有返回类型的,是对应的接口名
Predicate<Apple> filter = (Apple apple) -> "Green".equalsIgnoreCase(apple.getColor()) && 150 > apple.getWeight();
什么地方使用lambda表达式
有函数式接口的地方就可以使用lambda表达式。
函数式接口:只定义了一个抽象方法的接口。@FunctionalInterface
我们熟悉的Runnable,Callback,Comparator, Comparable都是函数式接口
写lambda表达式时,入参数量,各个参数的类型以及表达式返回类型怎么确定呢?这些信息,需要熟悉对应的函数式接口。
Java8引入了一些新的函数式接口,常见的如下
- Predicate<T>
boolean test(T t);
- Consumer<T>
void accept(T t);
- Supplier<T>
T get();
- Function<T, R>
R apply(T t);
- BiFunction
R apply(T t, U u);
- UnaryOperator<T>
public interface UnaryOperator<T> extends Function<T, T> {}
- BinaryOperator<T>
public interface BinaryOperator<T> extends BiFunction<T,T,T> {}
- Comparator
int compare(T o1, T o2);
说明:Java库提供的这些接口,没有异常处理,因此,要么我们自己定义函数式接口抛出一场,要么在使用这些系统提供的函数式接口写Lambda表达式时,自己捕获异常
lambda的相关概念
目标类型
lambda表达式返回类型,也即函数式接口
函数描述符
比如,上文的Predicate的抽象方法的函数描述符为:
(Apple) -> Boolean
Lambda表达式的类型检查
Java编译器如何检查Lambda表达式的类型?
- 在调用lambda表达式处,找到目标类型,即函数式接口
- 找到在函数式接口里,抽象方法函数名,入参,返回值,生成函数描述符号。
- 抽象方法的函数描述符和lambda表达式的签名比对
同样的Lambda表达式,可以有不同返回类型
看下面的例子
Predicate<Apple> appleFilter = (Apple apple) -> "Green".equalsIgnoreCase(apple.getColor()) && 150 > apple.getWeight();
Function<Apple, Boolean> appleFilter1 = (Apple apple) -> "Green".equalsIgnoreCase(apple.getColor()) && 150 > apple.getWeight();
相同的Apple->Boolean 返回类型可以是Predicate,也可以是Function
类型推断
利用泛型的类型推断,可以省略lambda表达式入参的类型声明,比如
Predicate<Apple> filter = apple ->
"Green".equalsIgnoreCase(apple.getColor()) && 150 > apple.getWeight();
filterApples(RAW_APPLE_LIST, apple -> "Green".equalsIgnoreCase(apple.getColor()) && 150 > apple.getWeight());
方法引用
方法引用能有效的简化lambda表达式,我们来举个例子
创建一个草莓类,Strawberry
public class Strawberry {
private int weight;
private String color;
public Strawberry() {
}
public Strawberry(int weight) {
this.weight = weight;
}
public Strawberry(int weight, String color) {
this.weight = weight;
this.color = color;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
@Override
public String toString() {
return "Strawberry{" +
"weight=" + weight +
", color='" + color + '\'' +
'}';
}
}
public class TransferUtils {
public static int doubleValue(int value){
return 2 * value;
}
public void tripleOver(int a){
System.out.println(String.valueOf(3 * a));
}
}
构造函数的方法引用
无参构造
Supplier<Strawberry> strawberrySupplier = Strawberry::new;
Strawberry strawberry = strawberrySupplier.get();
一个参数的构造
IntFunction<Strawberry> f = Strawberry::new;
Strawberry strawberry = f.apply(10);
两个参数的构造
BiFunction<Integer, String, Strawberry> biFunction = Strawberry::new;
Strawberry strawberry = biFunction.apply(20, "RED");
静态方法
类名::方法名
/**
* 静态方法引用
*/
private static void testMRStatic(){
List<Integer> list = Arrays.asList(1, 2, 3, 4);
List<Integer> collect = list.stream()
.map(TransferUtils::doubleValue)
.collect(Collectors.toList());
println(collect);
}
实例的方法引用
- 无入参的实例方法,格式同静态方法引用
String::length
这种使用会有一个疑问:实例的方法引用为什么能用类名来引用?其实这里还是调用了实例的方法,上文的实例是字符串对象。
/**
* 实例方法引用无参方法
*/
private static void testMRObject(){
Arrays.asList(new Strawberry(21), new Strawberry(23)).forEach(Strawberry::displayWeight);
}
- 带入参的实例方法
类实例名::方法名
/**
* 实例方法引用有参方法
*/
private static void testMRObject2(){
TransferUtils transferUtils = new TransferUtils();
Arrays.asList(1, 2, 3, 4).forEach(transferUtils::tripleOver);
}
练习题
有一箱东港九九草莓,找到其中最大的一颗?(可模拟所需的所有数据)
小结
lambda演化路径
接口 –》匿名内部类–》Lambda表达式
问题回顾
- 从Java7到Java8,lambda表达式经历了怎样的逻辑演化?
- lambda表达式能用在哪些场景?
- lambda表达式和匿名内部类是什么关系?
- 方法引用如何简化lambda表达式?
lambda表达式还可以复合使用,还可以在策略模式,模板方法,观察模式(接口回调)等设计模式中使用
相关的代码,已上传到Github 代码传送门
相关阅读
【阅读】Java8函数式编程
参阅的资料
【译】Java 8的新特性—终极版 www.jianshu.com
书籍:Java8实战 book.douban.com
书籍:Java8函数式编程 book.douban.com
原文地址: https://zhuanlan.zhihu.com/p/32834555
本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
相关文章