这样理解 Java 中的函数式编程就对了
在众多的编程范式中,大多数开发人员比较熟悉的是面向对象编程范式。一方面是由于面向对象编程语言比较流行,与之相关的资源比较丰富,比如Java,c++等。
另外一方面是由于大部分学校和培训机构的课程设置,都选择流行的面向对象编程语言。面向对象编程范式的优点在于其抽象方式与现实中的概念比较相近。比如,学生、课程、汽车和订单等这些现实中的概念,在抽象成相应的类之后,我们很容易就能理解类之间的关联关系。这些类中所包含的属性和方法可以很直观地设计出来。
函数式编程范式则相对较难理解。这主要是由于函数所代表的是抽象的计算,而不是具体的实体。因此比较难通过类比的方式来理解。
举例来说:
在一个学生信息管理系统中,可能会需要找到一个班级的某门课程的最高分数;在一个电子商务系统中,也可能会需要计算一个订单的总金额。看似风马牛不相及的两件事情,其实都包含了同样的计算在里面。
那么他们的共同点是什么呢?
对一个可迭代的对象进行遍历,同时在遍历的过程中执行自定义的操作。在计算最高分数的场景中,在遍历的同时需要保存当前已知最高分数,并在遍历过程中更新该值;在计算订单总金额的场景中,在遍历的同时需要保存当前已累积的金额,并在遍历过程中更新该值。
上面的话还是太抽象了,用直观的代码来表示:
计算学生的最高分数的代码
计算订单的总金额的代码
你可能还会问题,这两段代码不一样啊,好了,用我们面向对象的抽象来提取下上面两段代码的共性:
该计算模式由 3 个部分组成:
保存计算结果的状态,有初始值。
遍历操作。
遍历时进行的计算,更新保存计算结果的状态值。
下面,我们把这 3 个元素提取出来,用代码表示
了解函数式编程的读者应该已经看出来了,这就是常用的 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表达式)上。
如定义了一个函数式接口如下:
那么就可以使用Lambda表达式来表示该接口的一个实现(注:JAVA 8 之前一般是用匿名类实现的):
GreetingService greetService1 = message -> System.out.println("Hello " + message);
函数式接口可以对现有的函数友好地支持 lambda。
内容太多,篇幅原因待续
评论