Java 8 函数式编程

函数式编程

优点:减少工作量,减少 Bug,提高效率

减少工作量:做同样一件事,普通的方法实现,可能需要几十行代码,而函数式编程只需要几行代码

减少 Bug:代码写的越少,Bug 自然也变的越少

提高效率:别人还在吭哧坑次的写几十上百行代码时,你已经写完,顺便刷了个知乎

函数式编程深入浅出

你可能对我的话不信,那咱们就来看看实际的例子

假如给定一个用户列表信息,现在要你分别获取id 为偶数的用户姓周的用户姓王的用户,你会如何实现这个需求。

大多数人会写出如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 过滤 id 为偶数的用户
public static List<User> filterUsersWithEvenId(List<User> users) {
List<User> result = new ArrayList<>();
for (User user : users) {
if (user.id % 2 == 0) {
result.add(user);
}
}
return result;
}

// 过滤姓周的用户
public static List<User> filterZhouUsers(List<User> users) {
List<User> result = new ArrayList<>();
for (User user : users) {
if (user.name.startsWith("周")) {
result.add(user);
}
}
return result;
}

// 过滤姓王的用户
public static List<User> filterWangUsers(List<User> users) {
List<User> result = new ArrayList<>();
for (User user : users) {
if (user.name.startsWith("王")) {
result.add(user);
}
}
return result;
}

代码达到了我们需要的效果,但是你仔细观察,会发现重复的代码太多了,不同的地方仅仅是 if 中的条件判断,这个时候就需要思考一下如何简化这些代码。稍微细心的人就会发现,过滤姓张的用户,和过滤姓王的用户可以抽取成一个方法,只需要把姓氏当做参数传给函数就行了,如下

1
2
3
4
5
6
7
8
9
10
// 过滤指定姓氏的用户
public static List<User> filterUsersWithLastName(List<User> users,String lastName){
List<User> result = new ArrayList<>();
for (User user : users) {
if (user.name.startsWith(lastName)) {
result.add(user);
}
}
return result;
}

这样的确简化了部分代码,但是仅此而已吗?难道过滤id为偶数的用户 方法,就不能跟其它方法合并了吗?

这个时候你可能会想,要是可以传条件就好了,这样方法就能根据条件,过滤我们需要的用户信息。的确,这种方法确实可行,那要怎么去实现呢?既然传递的都是自己「实现」的条件,那么很容易想到「接口」这个东西。所以我们可以根据要求写一个接口,并提供一个方法,让其他实现该接口的类,自己定义方法的实现,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

public static List<User> filterUsersWithCondition(List<User> users,Condition condition) {
List<User> result = new ArrayList<>();
for (User user : users) {
if (condition.isSatisfied(user)) {
result.add(user);
}
}
return result;
}

interface Condition {
boolean isSatisfied(User user);
}

static class ZhouUser implements Condition {
@Override
public boolean isSatisfied(User user) {
return user.name.startsWith("周");
}
}

static class WangUser implements Condition {
@Override
public boolean isSatisfied(User user) {
return user.name.startsWith("王");
}
}

static class UsersWithEvenId implements Condition {
@Override
public boolean isSatisfied(User user) {
return user.id % 2 == 0;
}
}

我们使用了一个接口,以及三个类,每个类对应不同的实现。编写一个根据条件过滤的方法 filterUsersWithCondition,我们传入接口类,然后进行条件判断。

但是实际上 Java 已经帮我们定义好了这些通用接口,不用我们再去定义,在 java.util.function包中,例如:

1
2
3
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);

他与我们自己定义的 Condition 接口很像,方法都是接收一个参数,返回一个布尔值。因此,我们完全可以把自己定义的接口,替换成官方提供的。这样又减少了一点代码量,以 ZhouUser类举例

1
2
3
4
5
6
static class ZhouUser implements Predicate {
@Override
public boolean test(User user) {
return user.name.startsWith("周");
}
}

这个时候我们得 filterUsersWithCondition 方法,更改如下

1
2
3
4
5
6
7
8
9
10

public static List<User> filterUsersWithCondition(List<User> users,Predicate<User> predicate) {
List<User> result = new ArrayList<>();
for (User user : users) {
if (predicate.test(user)) {
result.add(user);
}
}
return result;
}

实际上,做到这样已经很不错了,但是我们还能精简。每次调用 filterUsersWithCondition 方法的时候,都要传入一个实现类,并且每次有新需求,都要生成一个新类实现 Predicate 接口,这样代码量还是很多。

我们可以这样,直接使用匿名类完成操作,这样我们就不用频繁的实现接口了。如下

1
2
3
4
5
List<User> users = new ArrayList<>();
filterUsersWithCondition(users, new Predicate<User>() {
@Override
public boolean test(User user) { return user.name.startsWith("周");}
})

这个时候 IDEA 就会提示你了,按下快捷键 alt + enter 建,将匿名类转化为 Lambda 表达式。于是我们上面的代码就变成这样,代码瞬间简洁了很多。

1
2
List<User> users = new ArrayList<>();
filterUsersWithCondition(users, user -> user.name.startsWith("周"));

这边简单的介绍下 Lambda 表达式:user -> user.name.startsWith("周")它对应的就是过滤姓周用户的匿名内部类 test 方法。-> 左边对应参数,他完整的参数是 (User user),由于 Lambda 可以通过上下文推断出参数类型,因此我们得式子仅仅指定了参数名user-> 右边的就是要执行的语句了,遇到多行执行语句需要用 {}括起来,用分号分隔语句。

Predicate 中的 test 方法,是将一个 User 对象到 boolean 的映射。实际上只要我们写的方法满足object->boolean,都可以自动转化成一个函数接口。比如我们刚写的 Lambda 表达式 user -> user.name.startsWith("周") ,以及我们将要展示的方法引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Main {
public static void main(String[] args) {
List<User> users = new ArrayList<>();
filterUsersWithCondition(users, Main::zhouUser);
}

public static boolean zhouUser(User user){
return user.name.startsWith("周");
}

public static List<User> filterUsersWithCondition(List<User> users, Predicate<User> predicate) {
List<User> result = new ArrayList<>();
for (User user : users) {
if (predicate.test(user)) {
result.add(user);
}
}
return result;
}

}

我们自己编写了一个方法 zhouUser,这个方法也是满足 Object -> boolean  的映射关系,因此他会被转化成相应的函数接口,这时使用方法引用调用这个方法,即可达到相同的目的。方法引用相比 Labmda 表达式的实现,更为清晰易懂,因为有方法名,我们很容易根据方法的名称来了解代码在干什么,对于日常维护开发更为友好。

Java 中的函数接口

先说一个结论:任何只包含一个抽象方法的接口,都可以转化成函数接口,例如我们刚开始使用的 Condition接口。

在 Java 中不只有 Predicate,还有很多默认的实现。像,Consumer是 Object->void 的映射;Supplier是 void->Object 的映射;ToIntFunction 是 Object->int 的映射,等等,如图:

我们可以根据我们得需求使用以上接口,或者自己定义我们所需的接口。

使用 Comparator 实战

当我们需要对数据进行比较的时候,就需要 Comparator

还是上面说到的 User 对象,现在有了新需求,需要对 user 的 id 进行从小到大排序,然后按照年龄从大到小排列。我们可以使用 Collections.sort() 方法,对数据进行排序。可以发现需要传入的参数是一个 Comparator 接口,因此我们可以直接 New 一个 Comparator,并实现他的方法。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
List<User> users = Arrays.asList(
new User(1, 18),
new User(2, 5),
new User(3, 7),
new User(4, 20));

Collections.sort(users, new Comparator<User>() {
@Override
public int compare(User o1, User o2) {
if (o1.id < o2.id) {
return 1;
} else if (o1.id > o2.id) {
return -1;
}
return 0;
}
});

Java 8 之后,JDK 为我们提供了更好的方法。在 Comparator 接口中有一个静态方法 comparing(),我们可以查看代码,发现他需要我们传入一个 Function 接口

1
2
3
4
5
6
7
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor)
{
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}

因此我们传入的参数遵循 Function 接口对应的映射关系就可以了,打开发现 Function 是 T->R 的映射关系,即我们传入的参数转化成另一种数据类型。因此我们可以写成这样,User 类转化成整形,符合映射关系

1
Collections.sort(users,Comparator.comparing(user -> user.id);

我们可以使用方法引用替换 Lambda 表达式,getId 方法表面上没有参数,实际上是有一个隐藏的 this,因次该方法引用也符合映射关系

1
Collections.sort(users,Comparator.comparing(User::getId);

比较了 id 之后我们还需要比较年龄的倒序排列,使用 thenComparing 继续比较,再使用 reversed() 方法将结果倒序排列。

1
Collections.sort(users, Comparator.comparing(User::getId).thenComparing(User::getAge).reversed());

这样就完成了我们得需求,一行代码完成了我们得工作,还减少了 bug,头发又可以少掉几根了,真是幸福。


Java 8 函数式编程
http://wszzf.top/2021/09/20/Java 8函数式编程/
作者
Greek
发布于
2021年9月20日
许可协议