熊熊出没 发表于 2022-9-16 17:16:22

用好JAVA中的函数式接口,轻松从通用代码框架中剥离掉业务定制逻辑

大家好,又见面了。
今天我们一起聊一聊JAVA中的函数式接口。那我们首先要知道啥是函数式接口、它和JAVA中普通的接口有啥区别?其实函数式接口也是一个Interface类,是一种比较特殊的接口类,这个接口类有且仅有一个抽象方法(但是可以有其余的方法,比如default方法)。
当然,我们看源码的时候,会发现JDK中提供的函数式接口,都会携带一个 @FunctionalFunction注解,这个注释是用于标记此接口类是一个函数式接口,但是这个注解并非是实现函数式接口的必须项。说白了,加了这个注解,一方面可以方便代码的理解,告知这个代码是按照函数式接口来定义实现的,另一方面也是供编译器协助检查,如果此方法不符合函数式接口的要求,直接编译失败,方便程序员介入处理。
https://pics.codingcoder.cn/pics/166020271839328594371.png
所以归纳下来,一个函数式接口应该具备如下特性:

[*]是一个JAVA interface类
[*]有且仅有1个公共抽象方法
[*]有@FunctionalFunction标注(可选)
比如我们在多线程场景中都很熟悉的Runnable接口,就是个典型的函数式接口,符合上面说的2个特性:
@FunctionalInterface
public interface Runnable {
    /**
   * When an object implementing interface Runnable is used
   * to create a thread, starting the thread causes the object's
   * run method to be called in that separately executing
   * thread.
   * <p>
   * The general contract of the method run is that it may
   * take any action whatsoever.
   *
   * @see   java.lang.Thread#run()
   */
    public abstract void run();
}但是,我们在看JDK源码的时候,也会看到有些函数式接口里面有多个抽象方法。比如JDK中的 Comparator接口的定义如下:
@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
    boolean equals(Object obj);
    // 其他方法省略...
}可以看到,Comparator接口里面提供了 compare和 equals两个抽象方法。这是啥原因呢?回答这个问题前,我们可以先来做个试验。
我们自己定义一个函数式接口,里面提供两个抽象方法测试一下,会发现IDEA中直接就提示编译失败了:
https://pics.codingcoder.cn/pics/166020275776021857497.png
同样是这个自定义的函数式接口,我们修改下里面的抽象方法名称,改为 equals方法,会发现这样就不报错了:
https://pics.codingcoder.cn/pics/166020276973122916328.png
在IDEA中可能更容易看出端倪来,在上面的图中,注意到12行代码前面那个 @符号了吗?我们换种写法,改为如下的方式,原因就更加清晰了:
https://pics.codingcoder.cn/pics/166020278077268393056.png
原来,这个 equals方法,其实是继承自父类的方法,因为所有的类最终都是继承自Object类,所以 equals方法只能算是对父类接口的一个覆写,而不算是此接口类自己的抽象方法,所以此方法里面实际上还是只有 1个抽象方法,并没有违背函数式接口的约束条件。
https://pics.codingcoder.cn/pics/202207102124124.gif
函数式接口在JDK中的大放异彩

JDK源码 java.util.function包下面提供的一系列的预置的函数式接口定义:
https://pics.codingcoder.cn/pics/166020274234332314319.png
部分使用场景比较多的函数式接口的功能描述归纳如下:
接口类功能描述Runnable直接执行一段处理函数,无任何输出参数,也没有任何输出结果。Supplier执行一段处理函数,无任务输入参数,返回一个T类型的结果。与Runnable的区别在于Supplier执行完之后有返回值。Consumer执行一段处理函数,支持传入一个T类型的参数,执行完没有任何返回值。BiConsumer与Consumer类型相似,区别点在于BiConsumer支持传入两个不同类型的参数,执行完成之后依旧没有任何返回值。Function执行一段处理函数,支持传入一个T类型的参数,执行完成之后,返回一个R类型的结果。与Consumer的区别点就在于Function执行完成之后有输出值。BiFunction与Function相似,区别点在于BiFunction可以传入两个不同类型的参数,执行之后可以返回一个结果。与BiConsumer也很类似,区别点在于BiFunction可以有返回值。UnaryOperator传入一个参数对象T,允许对此参数进行处理,处理完成后返回同样类型的结果对象T。继承Function接口实现,输入输出对象的类型相同。BinaryOperator允许传入2个相同类型的参数,可以对参数进行处理,最后返回一个仍是相同类型的结果T。继承BiFunction接口实现,两个输入参数以及最终输出结果的对象类型都相同。Predicate支持传入一个T类型的参数,执行一段处理函数,最后返回一个布尔类型的结果。BiPredicate支持传入2个相同类型T的参数,执行一段处理函数,最后返回一个布尔类型的结果。JDK中 java.util.function 包内预置了这么多的函数式接口,很多场景下其实都是给JDK中其它的类或者方法中使用的,最典型的就是Stream了——可以说有一大半预置的函数式接口类,都是为适配Stream相关能力而提供的。也正是基于函数式接口的配合使用,才是使得Stream的灵活性与扩展性尤其的突出。
下面我们一起来看几个Stream的方法实现源码,来感受下函数式接口使用的魅力。
比如,Stream中的 filter过滤操作,其实就是传入一个元素对象,然后经过一系列的处理与判断逻辑,最后需要给定一个boolean的结果,告知filter操作是应该保留还是丢弃此元素,所以filter方法传入的参数就是一个 Predicate函数式接口的具体实现(因为Predicate接口的特点就是传入一个T对象,输出一个boolean结果):
/*** Returns a stream consisting of the elements of this stream that match* the given predicate.*/    Stream filter(Predicate
页: [1]
查看完整版本: 用好JAVA中的函数式接口,轻松从通用代码框架中剥离掉业务定制逻辑