如何理解函数式接口

先说函数:y=f(x) 这个还是很好理解的吧,就是给你一个值 x,通过函数 f 进行一通运算,最终返回一个结果给你(不一定有结果哈)。
比如说:

  • y=2 * x (1)
  • y=3 + x (2)
  • y = 2 x + 3 z (3)
  • y = a * b (4)

上面列举了四个例子,可以看出来其实是分成了两类

  • (1)(2)是一类,因为这两个函数都是给定了一个参数,然后输出一个值
  • (3)(4)是一类,因为者两个函数都是给定了两个参数,然后输出一个值

看出点什么东东没有?其实这里分类的标准就是按照输入参数的个数输出参数的个数(当然在 java 中其实还有类型的概念,即使参数个数相同,但是类型不一致也是不同的函数)。

概念

说了这么多,要进入正题了,就是要接下来要讲的 java8 中出现的函数式接口。先说一下什么是函数式接口

函数式接口 ( Functional Interface ) 就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。 函数式接口可以被隐式转换为 lambda 表达式。

  • 大多数函数式接口在 java.util.function 这个包里面(比如 Runnable 也是函数式接口,就不是在这个包)
  • 函数式接口都使用 @FunctionalInterface 注解来修饰。

    函数式接口的分类

    说到分类,我们最开始的例子就说了分类的方法,当然 java8 中的函数式接口不仅仅是看参数的个数、返回值,还需要看参数的类型。先看下给出的分类,后面再仔细介绍。主体上可以分成下面几类,Predicate (英文断言的意思,就是判断真假)是属于 有参数,也有返回值类型,但是这个很一般的有参数有返回值不同,这是返回真假的接口,所以单独列出来。 函数式接口 - 图1

上面是一个大类,我们可以看一下 java.util.function 这个包里面的接口,虽然有很多接口,但是都是依据这个分类来的。
image.png
当然上面已经说过,Runnable 这种很早之前就存在的接口就不在这个包里面了,但也是一种函数式接口的类型。下面我们每一种类型都看一个案例。

函数式接口的调用

接口是干什么用的,接口只是为了定义标准,我们最终还是要去实现这个接口并且调用接口中的方法(实现接口的这个对象)。就比如下面这个 Function 接口类型,其实最终我们还是要创建 该接口的实现类,并且调用其中的 apply() 方法(当然调用接口中其他方法也是一样的意思)

Function 类型

  1. @FunctionalInterface
  2. public interface Function<T, R> {
  3. R apply(T t);
  4. default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
  5. Objects.requireNonNull(before);
  6. return (V v) -> apply(before.apply(v));
  7. }
  8. default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
  9. Objects.requireNonNull(after);
  10. return (T t) -> after.apply(apply(t));
  11. }
  12. static <T> Function<T, T> identity() {
  13. return t -> t;
  14. }
  15. }
  • 该接口虽然有多个方法,但是根据函数式接口的定义我们只需要去寻找没有默认实现的那个方法就好(后面会看到其他的默认方法就是对这个抽象方法的一些聚合操作)。
  • 我们说 Function 类型是有参数有返回值,Function 接口就是最基本的 Function 类型了,按照最少参数来配置的(一个参数,一个返回值)
  • 后面看其他 Function 类型的其他 接口的时候,就会发现,其他接口要么是参数具体化,要么就是参数更多了,可以认为 Function 接口就是他所在类别的元接口(就是其他这种类型的接口是根据这个接口做进一步的修改得到的)

下面直接来看一个示例(这个不给示例了,相信你看懂了下面的示例,这个就更不是问题了)

BiFunction

案例说明:给定两个 Integer 类型的数字,计算他们的和,并转换成 String 类型进行返回。

这个案例是为了符合 BiFunction 接口的规范,就是给定两个参数,返回一个参数(参数类型我们自己给定)

  1. @FunctionalInterface
  2. public interface BiFunction<T, U, R> {
  3. // 重点就是看这个没有实现的方法咯
  4. R apply(T t, U u);
  5. default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after) {
  6. Objects.requireNonNull(after);
  7. return (T t, U u) -> after.apply(apply(t, u));
  8. }
  9. }

下面就来看一下我们在使用的过程中的几种形式。

匿名对象方式

  1. public static void main(String[] args) {
  2. BiFunction<Integer, Integer, String> biFunction = new BiFunction<Integer, Integer, String>() {
  3. @Override
  4. public String apply(Integer a, Integer b) {
  5. return String.valueOf(a + b);
  6. }
  7. };
  8. // 调用方法,并且输出
  9. System.out.println(biFunction.apply(3, 5));
  10. }

Lambda 表达式形式

怎么理解 Lambda 表达式? 比如我们这个案例中为什么 Lambda 是写成这个样子呢?
前面我们说函数式接口重点要关注的是 那个抽象的方法,而 Lambda 表达式就是针对的那个抽象的方法的。
image.png

  1. public static void main(String[] args) {
  2. BiFunction<Integer, Integer, String> biFunction2 = (a, b) -> {
  3. return String.valueOf(a + b);
  4. };
  5. // 调用方法,并且输出
  6. System.out.println(biFunction2.apply(3, 5));
  7. }

定义方法的形式

怎么理解定义方法的形式呢?
image.png

  • 可以看到所谓的方法只是在我们返回的对象外边包裹了一层而已,可以进行一些额外的处理
  • 上面截图中说(a, b)-> { return “” } 这种形式返回的对象就是 BiFunction 类型呢?这个就是 Lambda 的作用了,所以为什么说函数式接口中抽象方法只能有一个呢?就是为了达到这种一对一的效果,就是你是这种形式,那么你就是这种类型,反过来,你是这种类型,你就肯定是这种形式。由此可知,当参数有两个,返回值有一个的时候就必然是 BiFunction 类型。 ```java public static void main(String[] args) { BiFunction biFunction3 = biFunction(1); String apply = biFunction3.apply(3, 5); // 调用方法,并且输出 System.out.println(apply); }

public static BiFunction biFunction(Integer integer) { System.out.println(“参数” + integer); return (a, b) -> { return String.valueOf(a + b); }; }

  1. <a name="WMQcR"></a>
  2. # Consumer 类型
  3. ```java
  4. @FunctionalInterface
  5. public interface Consumer<T> {
  6. void accept(T t);
  7. default Consumer<T> andThen(Consumer<? super T> after) {
  8. Objects.requireNonNull(after);
  9. return (T t) -> { accept(t); after.accept(t); };
  10. }
  11. }

Consumer 接口类型是给定一个参数,但是没有返回值,同样的 Consumer 接口可以认为是这种接口类型的 元接口,所以看一个这种类型的其他接口。

IntConsumer

  1. @FunctionalInterface
  2. public interface IntConsumer {
  3. void accept(int value);
  4. default IntConsumer andThen(IntConsumer after) {
  5. Objects.requireNonNull(after);
  6. return (int t) -> { accept(t); after.accept(t); };
  7. }
  8. }

IntConsumer 接口类型相对于 Consumer 类型来说就是将参数具体化,表明了参数就是 int 类型。同样的,接下来我们来看一下在使用过程中的几种形式。

匿名对象形式

  1. /**
  2. * @Author 咖啡杯里的茶
  3. * @date 2021/5/13
  4. */
  5. public class FuncMain {
  6. public static void main(String[] args) {
  7. IntConsumer intConsumer = new IntConsumer() {
  8. @Override
  9. public void accept(int value) {
  10. // 对接收的参数进行计算的过程
  11. int result = value * 2;
  12. System.out.println(result);
  13. }
  14. };
  15. // 调用匿名对象的方法
  16. intConsumer.accept(1);
  17. }
  18. }

Lamdbd 表达式形式

  1. /**
  2. * @Author 咖啡杯里的茶
  3. * @date 2021/5/13
  4. */
  5. public class FuncMain {
  6. public static void main(String[] args) {
  7. IntConsumer intConsumer = (value)->{
  8. // 对接收的参数进行计算的过程
  9. int result = value * 2;
  10. System.out.println(result);
  11. };
  12. intConsumer.accept(1);
  13. }
  14. }

定义方法的形式

  1. /**
  2. * @Author 咖啡杯里的茶
  3. * @date 2021/5/13
  4. */
  5. public class FuncMain {
  6. public static void main(String[] args) {
  7. IntConsumer intConsumer = consumerMethod(1, "hello");
  8. intConsumer.accept(4);
  9. }
  10. private static IntConsumer consumerMethod(int i, String s) {
  11. System.out.println(i);
  12. System.out.println(s);
  13. // 其实就是在外面包装了一层方法而已,但是里面的核心不变,肯定还是返回IntConsumer类型的对象
  14. //至于这个对象使用 匿名对象,Lambda形式还是其他什么形式产生的不重要。
  15. return (value) -> {
  16. int result = value * 2;
  17. System.out.println(result);
  18. };
  19. }
  20. }

其他类型的也是类似了,把这两个看懂了,其余的就 no problem 了。