我们前面讲到,实现面向对象编程不一定非得使用面向对象编程语言,同理,实现函数式编程也不一定非得使用函数式编程语言。现在,很多面向对象编程语言,也提供了相应的语法、类库来支持函数式编程
。
接下来,我们就看下 Java 这种面向对象编程语言,对函数式编程的支持,借机加深一下你对函数式编程的理解。我们先来看下面这样一段非常典型的 Java 函数式编程的代码。
public class FPDemo {
public static void main(String[] args) {
Optional<Integer> result = Stream.of("f", "ba", "hello")
.map(s -> s.length())
.filter(l -> l <= 3)
.max((o1, o2) -> o1-o2);
System.out.println(result.get()); // 输出2
}
}
这段代码的作用是从一组字符串数组中,过滤出长度小于等于 3 的字符串,并且求得这其中的最大长度。
如果你不了解 Java 函数式编程的语法,看了上面的代码或许会有些懵,主要的原因是,Java 为函数式编程引入了三个新的语法概念:Stream 类、Lambda 表达式和函数接口(Functional Inteface)
。Stream 类用来支持通过“.”级联多个函数操作的代码编写方式;引入 Lambda 表达式的作用是简化代码编写;函数接口的作用是让我们可以把函数包裹成函数接口,来实现把函数当做参数一样来使用(Java 不像 C 一样支持函数指针,可以把函数直接当参数来使用)。
首先,我们来看下 Stream 类
。假设我们要计算这样一个表达式:(3-1)*2+5
。如果按照普通的函数调用的方式写出来,就是下面这个样子:
add(multiply(subtract(3,1),2),5);
不过,这样编写代码看起来会比较难理解,我们换个更易读的写法,如下所示:
subtract(3,1).multiply(2).add(5);
我们知道,在 Java 中,“.”表示调用某个对象的方法
。为了支持上面这种级联调用方式,我们让每个函数都返回一个通用的类型:Stream 类对象。在 Stream 类上的操作有两种:中间操作和终止操作。中间操作返回的仍然是 Stream 类对象,而终止操作返回的是确定的值结果
。
我们再来看之前的例子。我对代码做了注释解释,如下所示。其中,map、filter 是中间操作,返回 Stream 类对象,可以继续级联其他操作
;max 是终止操作,返回的不是 Stream 类对象,无法再继续往下级联处理了。
public class FPDemo {
public static void main(String[] args) {
Optional<Integer> result = Stream.of("f", "ba", "hello") // of返回Stream<String>对象
.map(s -> s.length()) // map返回Stream<Integer>对象
.filter(l -> l <= 3) // filter返回Stream<Integer>对象
.max((o1, o2) -> o1-o2); // max终止操作:返回Optional<Integer>
System.out.println(result.get()); // 输出2
}
}
其次,我们再来看下 Lambda 表达式。
我们前面讲到,Java 引入 Lambda 表达式的主要作用是简化代码编写。实际上,我们也可以不用 Lambda 表达式来书写例子中的代码。我们拿其中的 map 函数来举例说明一下。
下面有三段代码,第一段代码展示了 map 函数的定义,实际上,map 函数接收的参数是一个 Function 接口,也就是待会儿要讲到的函数接口。第二段代码展示了 map 函数的使用方式。第三段代码是针对第二段代码用 Lambda 表达式简化之后的写法。实际上,Lambda 表达式在 Java 中只是一个语法糖而已,底层是基于函数接口来实现的,也就是第二段代码展示的写法
。
// Stream中map函数的定义:
public interface Stream<T> extends BaseStream<T, Stream<T>> {
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
//...省略其他函数...
}
// Stream中map的使用方法:
Stream.of("fo", "bar", "hello").map(new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return s.length();
}
});
// 用Lambda表达式简化后的写法:
Stream.of("fo", "bar", "hello").map(s -> s.length());
Lambda 表达式语法不是我们学习的重点。我这里只稍微介绍一下。
如果感兴趣,你可以自行深入研究。Lambda 表达式包括三部分:输入、函数体、输出。表示出来的话就是下面这个样子:
(a, b) -> { 语句1; 语句2;...; return 输出; } //a,b是输入参数
实际上,Lambda 表达式的写法非常灵活。我们刚刚给出的是标准写法,还有很多简化写法。比如,如果输入参数只有一个,可以省略 (),直接写成 a->{…};如果没有入参,可以直接将输入和箭头都省略掉,只保留函数体;如果函数体只有一个语句,那可以将{}省略掉;如果函数没有返回值,return 语句就可以不用写了。
如果我们把之前例子中的 Lambda 表达式,全部替换为函数接口的实现方式,就是下面这样子的。代码是不是多了很多?
Optional<Integer> result = Stream.of("f", "ba", "hello")
.map(s -> s.length())
.filter(l -> l <= 3)
.max((o1, o2) -> o1-o2);
// 还原为函数接口的实现方式
Optional<Integer> result2 = Stream.of("fo", "bar", "hello")
.map(new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return s.length();
}
})
.filter(new Predicate<Integer>() {
@Override
public boolean test(Integer l) {
return l <= 3;
}
})
.max(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
最后,我们来看下函数接口。
实际上,上面一段代码中的 Function、Predicate、Comparator 都是函数接口
。我们知道,C 语言支持函数指针,它可以把函数直接当变量来使用。但是,Java 没有函数指针这样的语法。所以,它通过函数接口,将函数包裹在接口中,当作变量来使用。
实际上,函数接口就是接口。不过,它也有自己特别的地方,那就是要求只包含一个未实现的方法
。因为只有这样,Lambda 表达式才能明确知道匹配的是哪个接口。如果有两个未实现的方法,并且接口入参、返回值都一样,那 Java 在翻译 Lambda 表达式的时候,就不知道表达式对应哪个方法了。
我把 Java 提供的 Function、Predicate 这两个函数接口的源码,摘抄过来贴到了下面,你可以对照着它们,理解我刚刚对函数接口的讲解。
@FunctionalInterface
public interface Function<T, R> {
R apply(T t); // 只有这一个未实现的方法
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t); // 只有这一个未实现的方法
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
以上讲的就是 Java 对函数式编程的语法支持,我想,最开始给到的那个函数式编程的例子,现在你应该能轻松看懂了吧?
总结
- 有时候函数式编程写起来比面向对象要简单