写点什么

这样理解 Java 中的函数式编程就对了

用户头像
jerry
关注
发布于: 2020 年 05 月 07 日
这样理解Java中的函数式编程就对了

在众多的编程范式中,大多数开发人员比较熟悉的是面向对象编程范式。一方面是由于面向对象编程语言比较流行,与之相关的资源比较丰富,比如Java,c++等。



另外一方面是由于大部分学校和培训机构的课程设置,都选择流行的面向对象编程语言。面向对象编程范式的优点在于其抽象方式与现实中的概念比较相近。比如,学生、课程、汽车和订单等这些现实中的概念,在抽象成相应的类之后,我们很容易就能理解类之间的关联关系。这些类中所包含的属性和方法可以很直观地设计出来。



函数式编程范式则相对较难理解。这主要是由于函数所代表的是抽象的计算,而不是具体的实体。因此比较难通过类比的方式来理解。

举例来说:



在一个学生信息管理系统中,可能会需要找到一个班级的某门课程的最高分数;在一个电子商务系统中,也可能会需要计算一个订单的总金额。看似风马牛不相及的两件事情,其实都包含了同样的计算在里面。



那么他们的共同点是什么呢?



对一个可迭代的对象进行遍历,同时在遍历的过程中执行自定义的操作。在计算最高分数的场景中,在遍历的同时需要保存当前已知最高分数,并在遍历过程中更新该值;在计算订单总金额的场景中,在遍历的同时需要保存当前已累积的金额,并在遍历过程中更新该值。



上面的话还是太抽象了,用直观的代码来表示:

计算学生的最高分数的代码



int maxMark = 0;
for (Student student : students) {
if (student.getMark() > maxMark) {
maxMark = student.getMark();
}
}

计算订单的总金额的代码



BigDecimal total = BigDecimal.ZERO;
for (LineItem item : order.getLineItems()) {
total = total.add(item.getPrice().multiply(new BigDecimal(item.getCount())));
}

你可能还会问题,这两段代码不一样啊,好了,用我们面向对象的抽象来提取下上面两段代码的共性:

该计算模式由 3 个部分组成:



  1. 保存计算结果的状态,有初始值。

  2. 遍历操作。

  3. 遍历时进行的计算,更新保存计算结果的状态值。



下面,我们把这 3 个元素提取出来,用代码表示



reduce(students, (mark, student) -> {
return Math.max(student.getMark(), mark);
}, 0);
reduce(order.lineItems, (total, item) -> {
return total.add(item.getPrice().multiply(new BigDecimal(item.getCount())))
}, BigDecimal.ZERO);

了解函数式编程的读者应该已经看出来了,这就是常用的 reduce 函数。



Java中的函数式编程



作为面向对象的编程语言,Java 中使用接口来表示函数。直到 Java 8,Java 才提供了内置标准 API 来表示函数,增加了java.util.function 包



Function<T, R>



Function<T, R>定义在java.util.function 包。



Function<T, R> 表示接受一个参数的函数,输入类型为 T,输出类型为 R。



Function 接口只包含一个抽象方法 R apply(T t),也就是在类型为 T 的输入 t 上应用该函数,得到类型为 R 的输出。



除了接受一个参数的 Function 之外,还有接受两个参数的接口 BiFunction<T, U, R>,T 和 U 分别是两个参数的类型,R 是输出类型。BiFunction 接口的抽象方法为 R apply(T t, U u)。超过 2 个参数的函数在 Java 标准库中并没有定义。



除了 Function 和 BiFunction 之外,Java 标准库还提供了几种特殊类型的函数:



  • Consumer<T>:接受一个输入,没有输出。抽象方法为 void accept(T t)。

  • Supplier<T>:没有输入,一个输出。抽象方法为 T get()。

  • Predicate<T>:接受一个输入,输出为 boolean 类型。抽象方法为 boolean test(T t)。

  • UnaryOperator<T>:接受一个输入,输出的类型与输入相同,相当于 Function<T, T>。

  • BinaryOperator<T>:接受两个类型相同的输入,输出的类型与输入相同,相当于 BiFunction<T,T,T>。

  • BiPredicate<T, U>:接受两个输入,输出为 boolean 类型。抽象方法为 boolean test(T t, U u)。



@FunctionalInterface



函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。

有意思的是,这个抽象方法的方法名无所谓,因为函数式接口可以被隐式转换为 lambda 表达式,lambda 是不需要方法名的。



Lambda 表达式和方法引用(实际上也可认为是Lambda表达式)上。

如定义了一个函数式接口如下:



@FunctionalInterface
interface GreetingService
{
void sayMessage(String message);
}

那么就可以使用Lambda表达式来表示该接口的一个实现(注:JAVA 8 之前一般是用匿名类实现的):



GreetingService greetService1 = message -> System.out.println("Hello " + message);

函数式接口可以对现有的函数友好地支持 lambda。



内容太多,篇幅原因待续



用户头像

jerry

关注

还未添加个人签名 2019.06.26 加入

还未添加个人简介

评论

发布
暂无评论
这样理解Java中的函数式编程就对了