Java 对函数式编程的支持

本文介绍了Java如何通过Stream类、Lambda表达式和函数接口支持函数式编程。Stream类支持级联操作,Lambda简化代码编写,函数接口则允许函数作为参数传递。通过实例代码解析了这些概念,帮助读者理解Java中的函数式编程思想。
摘要由CSDN通过智能技术生成

我们前面讲到,实现面向对象编程不一定非得使用面向对象编程语言,同理,实现函数式编程也不一定非得使用函数式编程语言。现在,很多面向对象编程语言,也提供了相应的语法、类库来支持函数式编程

接下来,我们就看下 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 对函数式编程的语法支持,我想,最开始给到的那个函数式编程的例子,现在你应该能轻松看懂了吧?

总结

  • 有时候函数式编程写起来比面向对象要简单

参考

83 | 开源实战三(下):借Google Guava学习三大编程范式中的函数式编程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值