从代码重构角度聊一聊Java8的函数式接口
前言
Hi,大家好,我是麦洛。今天我们以主人公阿呆的视角。来看看他如何将业务代码一步步重构,最后使用函数式接口达到灵活实现。希望对大家理解lambda
表达式和函数式接口有所帮助.
很久很久以前,大约是21世纪时候。有一个天才程序员,名字叫阿呆。大学毕业以后,顺利被一家知名电商网站录取,开始了自己的伟大之路。
时间过得很快,不知不觉,他入职已经两周了。这天,老板让他对接一个客户。在交谈中,阿呆得知这位客户是做水果生意,主要经营各种瓜。想要开发一款电商小程序来做线上业务。经过简单沟通之后,客户起身离开。回到工作岗位的阿呆很快设计下面的类来定义瓜 Melon
类:
/**
* 瓜
* @author milogenius
* @date 2020-05-29 13:21
*/
public class Melon {
/**品种*/
private final String type;
/**重量*/
private final int weight;
/**产地*/
private final String origin;
public Melon(String type, int weight, String origin) {
this.type = type;
this.weight = weight;
this.origin = origin;
}
// getters, toString(), and so on omitted for brevity
}
经过一个月奋战,阿呆成功上线了这个项目。
第一次 按类型筛选瓜类
有一天,客户向阿呆提了一个需求,能够按瓜类型对瓜进行过滤。阿呆脑袋一想,这不很简单吗?于是,阿呆创建了一个 Filters
类, 实现了一个filterByType
方法
/**
* @author milogenius
* @date 2020-05-29 13:25
*/
public class Filters {
/**
* 根据类型筛选瓜类
* @param melons 瓜类
* @param type 类型
* @return
*/
public static List<Melon> filterByType(List<Melon> melons, String type) {
List<Melon> result = new ArrayList<>();
for (Melon melon: melons) {
if (melon != null && type.equalsIgnoreCase(melon.getType())) {
result.add(melon);
}
}
return result;
}
}
搞定,我们来测试一下
public static void main(String[] args) {
ArrayList<Melon> melons = new ArrayList<>();
melons.add(new Melon("羊角蜜", 1, "泰国"));
melons.add(new Melon("西瓜", 2, "三亚"));
melons.add(new Melon("黄河蜜", 3, "兰州"));
List<Melon> melonType = Filters.filterByType(melons, "黄河蜜");
melonType.forEach(melon->{
System.out.println("瓜类型:"+melon.getType());
});
}
没毛病,下班了,溜了溜了
第二次 按重量筛选瓜类
过了几天,客户又提了一个需求,要求按重量筛选瓜类(例如:所有1200克的瓜)。阿呆心想:天天提需求,天天改,就不能一次提完啊?上次我已经实现了按类型筛选瓜类,那我给他copy
一份改改吧!如下所示:
/**
* 按照重量过滤瓜类
* @param melons
* @param weight
* @return
*/
public static List<Melon> filterByWeight(List<Melon> melons, int weight) {
List<Melon> result = new ArrayList<>();
for (Melon melon: melons) {
if (melon != null && melon.getWeight() == weight) {
result.add(melon);
}
}
return result;
}
public static void main(String[] args) {
ArrayList<Melon> melons = new ArrayList<>();
melons.add(new Melon("羊角蜜", 1, "泰国"));
melons.add(new Melon("西瓜", 2, "三亚"));
melons.add(new Melon("黄河蜜", 3, "兰州"));
List<Melon> melonType = Filters.filterByType(melons, "黄河蜜");
melonType.forEach(melon->{
System.out.println("瓜类型:"+melon.getType());
});
List<Melon> melonWeight = Filters.filterByWeight( melons,3);
melonWeight.forEach(melon->{
System.out.println("瓜重量:"+melon.getWeight());
});
}
[]:
虽然这个需求对应阿呆很简单,他也很快就搞定了,但是作为一个有追求的程序员,他觉得不开心。因为他发现filterByWeight()
与 filterByType()
非常相似,就是过滤条件不同。阿呆心想,如果客户技术这样提需求,那么 Filters
将会有很多这样类似的方法,也就是说写了很多样板代码(代码冗余但又不得不写);
第三次 按类型和重量筛选瓜
果不其然,事情变得越来越糟。客户又要求我们添加一个新的过滤方式,该过滤方式可以按类型和重量过滤瓜类。为了满足客户需求,阿呆很快写了如下的代码
/**
* 按照类型和重量来筛选瓜类
* @param melons
* @param type
* @param weight
* @return
*/
public static List<Melon> filterByTypeAndWeight(List<Melon> melons, String type, int weight) {
List<Melon> result = new ArrayList<>();
for (Melon melon: melons) {
if (melon != null && type.equalsIgnoreCase(melon.getType()) && melon.getWeight() == weight) {
result.add(melon);
}
}
return result;
}
在阿呆看来,这是不能接受的。如果客户急需添加新的过滤条件,则代码将变得难以维护且容易出错。
第四次 将行为作为参数传递
做完第三次需求上线之后,阿呆心想,他不能在这样去添加更多的过滤条件。理论上Melon
类的任何属性都有可能作为过滤条件,这样的话我们的Filter类将会有大量的样板代码,而且有些方法会非常复杂。
经过一番研究,阿呆发现我们在样板代码中具有不同的行为。因此,我们只需要编写一次样板代码 并将行为作为参数传递。我们可以将任何过滤条件定型为行为,然后作为参数进行传递。这样代码将变得更加清晰,灵活,易于维护并且具有更少的参数。阿呆给它取了一个名字:行为参数化,在下图中进行了说明(左侧显示了我们现在拥有的;右侧显示了我们想要的):
如果我们将过滤条件视为一种行为,那么将每种行为视为接口的实现是非常直观的。阿呆经过分析认为所有这些行为都有一个共同点:过滤条件和boolean
类型的返回 。于是阿呆写下了如下的代码:
public interface MelonPredicate {
boolean test(Melon melon);
}
此外,他还编写了一个实现 GacMelonPredicate
。例如,过滤 Gac
瓜可以这样写:
public class GacMelonPredicate implements MelonPredicate {
@Override
public boolean test(Melon melon) {
return "gac".equalsIgnoreCase(melon.getType());
}
}
以此类推,可以过滤掉所有重于5000g
的瓜:
public class HugeMelonPredicate implements MelonPredicate {
@Override
public boolean test(Melon melon) {
return melon.getWeight() > 5000;
}
}
其实熟悉设计模式的同学应该知道这就是:策略设计模式。主要思想就是让系统在运行时动态选择需要调用的方法。所以我们可以认为 MelonPredicate
接口统一了所有专用于筛选瓜类的算法,并且每种实现都是一种策略,我们也可以把它理解为一种行为。
目前,我们已经有了策略,但是没有任何方法可以接收 MelonPredicate
参数。于是阿呆定义了 filterMelons()
方法,如下图所示:
public static List<Melon> filterMelons(List<Melon> melons, MelonPredicate predicate) {
List<Melon> result = new ArrayList<>();
for (Melon melon: melons) {
if (melon != null && predicate.test(melon)) {
result.add(melon);
}
}
return result;
}
大功告成,阿呆舒了一口气,然后测了一番,果然比以前好用很多
List<Melon> gacs = Filters.filterMelons(melons, new GacMelonPredicate());
List<Melon> huge = Filters.filterMelons(melons, new HugeMelonPredicate());
第五次 一次性加了100个过滤条件
就在阿呆沾沾自喜时候,客户又给他泼了一盆冷水。这家伙最近和一个东南亚大鳄打上了关系,成功引进各种类型东南亚瓜类,浩浩荡荡列了100个过滤条件让阿呆开发。阿呆心里一万个草泥马在奔腾啊!虽然经过上次改造,我们有足够的灵活性来完成此任务,但是我们仍然需要编写100个策略类来实现 每一个过滤标准。然后我们需要将策略传递给 filterMelons()
方法。
有没有不需要创建这些类的办法那?聪明的阿呆很快发现可以使用java
匿名内部类。如下所示
List<Melon> europeans = Filters.filterMelons(melons, new MelonPredicate() {
@Override
public boolean test(Melon melon) {
return "europe".equalsIgnoreCase(melon.getOrigin());
}
});
虽然我们向前跨了一大步,但好像无济于事。我们还是需要编写大量的代码实现此次需求。
有时候,匿名内部类看这比较复杂,我们可以用lambda表达式来简化它
List<Melon> europeansLambda = Filters.filterMelons(
melons, m -> "europe".equalsIgnoreCase(m.getOrigin()));
果然看这帅多了!!!,就这样,阿呆又一次成功完成了任务.
第六次 提取列表类型
随着客户成功搭上东南亚大鳄的女儿,东南亚大鳄对他越来越放心。将他旗下的瓜果生意都交给他做。客户端的日子好过,阿呆的日子就不好过了。果不其然,他又提需求了。他提出了需要销售除了瓜之外的其他水果,但是我们的MelonPredicate
仅支持 Melon
实例。
这家伙怎么搞?说不定哪天他要买蔬菜、海参可怎么搞,总不能给他再创建好多类似MelonPredicate
的接口吧
于是阿呆想到了泛型;
我们首先定义一个新接口,然后 Predicate
将Melon
其命名(从名称中删除 ):
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
接下来,我们重写该 filterMelons()
方法并将其重命名为 filter()
:
public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
List<T> result = new ArrayList<>();
for (T t: list) {
if (t != null && predicate.test(t)) {
result.add(t);
}
}
return result;
}
现在,我们可以这样过滤瓜类 :
List<Melon> watermelons = Filters.filter(
melons, (Melon m) -> "Watermelon".equalsIgnoreCase(m.getType()));
同样的,我们也可以对数字做同样的事情:
List<Integer> numbers = Arrays.asList(1, 13, 15, 2, 67);
List<Integer> smallThan10 = Filters.filter(numbers, (Integer i) -> i < 10);
现在我们回过头来看看,从哪里开始我们的代码发送巨大变化?发现是使用Java 8
函数式接口和lambda
表达式后,两者之间发生巨大的变化。不知道细心的伙伴有没有发现我们上面的 Predicate
接口上面多了一个@FunctionalInterface
上的注解,它就是标记函数式接口。
从概念上讲,函数式接口仅具有一个抽象方法。
★jdk 8 中有另一个新特性:default, 被 default 修饰的方法会有默认实现,不是必须被实现的方法,所以不影响 Lambda 表达式的使用。
”
而且,你会发现我们定义的Predicate
接口已经在Java 8
中作为java.util.function.Predicate
接口存在 。该 java.util.function
包下包含40多个此类接口。因此,在定义一个新的函数式接口之前,建议先检查该包的内容。大多数情况下,六个标准的内置函数式接口可以完成任务。这些列出如下:
Predicate<T>
Consumer<T>
Supplier<T>
Function<T, R>
UnaryOperator<T>
BinaryOperator<T>
函数式接口和lambda
表达式组成了一个强大的团队。Lambda
表达式支持直接内联函数式接口的抽象方法的实现。基本上,整个表达式被视为函数式接口的具体实现的一个实例,比如:
Predicate<Melon> predicate = (Melon m)
-> "Watermelon".equalsIgnoreCase(m.getType());
简而言之Lambda
lambda
表达式由三部分组成,如下图所示:
以下是lambda
表达式各部分的描述:
-
在箭头的左侧,是在 lambda
主体中使用的lambda
参数。这些是FilenameFilter.accept (File folder, String fileName)
方法的参数 。 -
在箭头的右侧,是 lambda
主体,在上面的例子中,该主体检查文件夹是否可读以及文件是否以.pdf
后缀结尾 。 -
箭头只是 lambda
参数和主体的分隔符。
此lambda
的匿名类版本如下:
FilenameFilter filter = new FilenameFilter() {
@Override
public boolean accept(File folder, String fileName) {
return folder.canRead() && fileName.endsWith(".pdf");
}
};
现在,如果我们查看lambda
及其匿名类版本,可以从下面四方面来描述lambda
表达式:
我们可以将 lambda
表达式定义为一种 简洁、可传递的匿名函数,首先我们需要明确 lambda
表达式本质上是一个函数,虽然它不属于某个特定的类,但具备参数列表、函数主体、返回类型,甚至能够抛出异常;其次它是匿名的,lambda
表达式没有具体的函数名称;lambda
表达式可以像参数一样进行传递,从而简化代码的编写。
Lambda
支持行为参数化,在前面的例子中,我们已经证明这一点。最后,请记住,lambda
只能在函数式接口的上下文中使用。
总结
在本文中,我们重点介绍了函数式接口的用途和可用性,我们将研究如何将代码从开始的样板代码现演变为基于函数式接口的灵活实现。希望对大家理解函数式接口有所帮助,谢谢大家。