随着 Java 8 的发布,引入了多个函数式接口。我们可以在JDK源代码中找到它们,因为它们带有@FunctionalInterface注解。注解@FunctionalInterface有两个目的。
- 首先,它用作文档目的。它让开发人员知道它是一个函数式接口,并且可以为该接口实现 lambda 表达式。
- 其次,它通过提供编译时安全性来避免编程错误。例如,下面给出了编译时错误。
kotlin
代码解读
复制代码
@FunctionalInterface interface IFoo { // 编译时错误,因为没有抽象方法 }
除非遵循FunctionalInterface 准则,否则上述接口定义将无法编译,即它必须具有一个抽象方法。
这里需要注意的一件事是,任何没有使用 @FunctionalInterface 注解标记的接口,只要它只有一个抽象方法,仍然会被视为函数式接口。
例如,下面是一个函数式接口,并且可以为该接口实现 lambda 表达式。
arduino
代码解读
复制代码
interface IFoo { int foo(String s1); }
注解@FunctaionalInterface是可选的,强烈建议使用该注解来标记函数式接口。
现在,我们来看看Java 8中提供的内置函数式接口。
内置函数式接口
作为 Java 8 版本的一部分,引入了多个函数式接口。所有这些功能接口在使用 Java 集合处理数据时都有不同的用途。除此之外,旧的类(如 Runnable、Callable 和其他具有单个抽象方法的接口)也使用 @FunctionalInterface 注解。
下面是Java 8中引入的函数式接口
- Consumer
- Predicate
- Function
- Supplier
- BiFunction
- BiConsumer
- UnarayOperator
- BinaryOperator
所有这些接口都与 List、Set、Map 等集合一起使用。我们将看到如何实现这些接口。
Consumer
Consumer 接口是最常用的接口。对于集合对象上的 forEach() 的每次使用,都会有 Consumer 接口。例如,看看下面的代码。
整理了一份Java面试题。包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处】即可免费获取
ini
代码解读
复制代码
List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); ints.forEach(i -> System.out.println(i));
换句话说,无论我们向 forEach() 方法提供什么 lambda 表达式作为参数,该 lambda 表达式都是 Consumer 接口的实现。
这是为什么?我们只需查看 forEach 方法接受的参数即可。下面是 Iterable 接口的 forEach() 方法定义。
javascript
代码解读
复制代码
default void forEach(Consumer<? super T> action) { Objects.requireNonNull(action); // 检查null for (T t : this) { action.accept(t); } }
可以看到 forEach() 方法是接受 Consumer 作为参数的默认方法。这个 forEach() 方法位于 Java 中每个集合接口都扩展的 Iterable 接口中
现在让我们看看 Consumer 接口的定义。
scss
代码解读
复制代码
@FunctionalInterface public interface Consumer<T> { void accept(T t); default Consumer<T> andThen(Consumer<? super T> after) { Objects.requireNonNull(after); return (T t) -> { accept(t); after.accept(t); }; } }
可以看到Consumer接口有一个名为accept()的抽象方法。还有一个名为 andThen() 的默认方法,它本身返回 Consumer 对象。 andThen() 方法使我们能够通过将一个 Consumer 与另一个 Consumer 链接来实现强大的复合 lambda 表达式。
我们已经了解了 forEach 方法的 lambda 表达式:i -> System.out.println(i)。现在我们将了解如何得到这个 lambda 表达式。
第一步是编写一个匿名内部类,如下所示。(我知道前面已经讲过,因为要照顾到初学者,所以反复解释如何得出 lambda 表达式。当我们练习并进行一些实践时,我们可以直接编写该 lambda 表达式。)
这里是 forEach 方法的匿名内部类。
typescript
代码解读
复制代码
List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); ints.forEach(new Consumer<Integer>() { @Override public void accept(Integer i) { System.out.println(i); } });
可以将上面的匿名内部类转换为 lambda 表达式,如下所示。
ini
代码解读
复制代码
List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); ints.forEach((Integer i) -> System.out.println(i));
上一篇已经说过,我们甚至可以忽略数据类型,因为编译器会推断它,并且当只有一个参数时我们甚至可以使用括号。
ini
代码解读
复制代码
List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); ints.forEach(i -> System.out.println(i));
如果我们现在想要打印集合中每个整数的平方,而不是简单地打印元素,该怎么办?
css
代码解读
复制代码
ints.forEach(i -> System.out.println(i * i));
立方呢?
css
代码解读
复制代码
ints.forEach(i -> System.out.println(i * i * i));
现在能看到 lambda 表达式的美妙之处以及使用匿名内部类编写这些表达式是多么痛苦?
现在我们知道Consumer 类有一个名为accept(T t) 的抽象方法,我们需要实现该方法;作为匿名内部类或作为 lambda 表达式,我们需要实现该方法。
在上面的所有示例中,我们直接将 lambda 表达式传给 forEach 方法。我们甚至可以将Lambda赋给一个变量,将这个变量传给forEach,如下所示。
ini
代码解读
复制代码
List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); Consumer<Integer> printConsumer = i -> System.out.println(i); ints.forEach(printConsumer);
这就是函数式编程的意义。在一个示例中,我们直接将 lambda(函数表达式或简单函数)作为方法参数发送。在第二个示例中,我们将该函数分配给 Consumer 类型的变量。
当我们必须使用像 andThen() 这样的辅助函数来链接多个 Consumer 对象时,将函数分配给变量会派上用场。下一节将对此进行说明。
Consumer的 andThen()方法
Consumer 接口定义有一个默认方法 andThen()。这个方法使我们能够实现Consumer链。在某些情况下,我们可能需要对集合中的每个元素执行两个操作,一个接一个。例如,我们想要为每个元素依次计算平方和立方。此类场景可以使用 andThen() 方法非常轻松地实现。
ini
代码解读
复制代码
List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); Consumer<Integer> c1 = i -> System.out.println(i * i); Consumer<Integer> c2 = i -> System.out.println(i * i * i); ints.forEach(c1.andThen(c2));
我们甚至可以将该 lambda 分配给另一个变量,并将该新变量发送给 forEach 方法,如下所示。
ini
代码解读
复制代码
Consumer<Integer> c1 = i -> System.out.println(i * i); Consumer<Integer> c2 = i -> System.out.println(i * i * i); Consumer<Integer> c3 = c1.andThen(c2); ints.forEach(c3);
为什么我们不能像下面这样在单个Consumer表达式中实现它?
css
代码解读
复制代码
ints.forEach(i -> { System.out.println(i * i); System.out.println(i * i * i); });
我们可以实现,它给出了完全相同的结果,并且看起来比 andThen 链接更简单。在这里有两个问题。
- 这陷入了命令式编程模式。 如果除了平方和立方之外我们还必须实现多个操作,那么这看起来非常必要且笨拙。
- 通过 andThen 链接,使代码是声明性的并且更具可读性。
通过命令式编程,我们为特定任务指定指令。通过声明式编程,我们声明操作而不是过于具体,框架会完成其余的工作。
命令式编程与声明式编程
为了从现实世界的角度来看这个问题,让我们以吃饭 的场景为例。我们去一家餐馆点一份炒面,但我们对它的制作方法太具体了。我们告诉厨师我们想要的——少点葱、辣椒放多点等等。
现在,如果餐厅老板设计了一份将这些选项组合在一起的炒面菜单,如下所示。
1、少葱
2、多辣
3、不加香菜
您只需走进餐厅,查看菜单并选择最好的选择,厨师就会为您制作。这是声明性的。我们指定我们想要这个。厨师已经有了它实现框架。我们不必告诉他所有的指令。
现在,在 Java 编程世界中,推动因素是什么?函数式接口和 lambda 表达式。
这就是 Consumer 接口及其相应的 lambda 实现的全部内容。在下一篇文章中,我们将了解 BiConusmer。
总结
注解@FunctionalInterface有两个目的。
- 首先,它用作文档目的,因为它让开发人员知道它是一个函数式接口,并且可以为该接口实现 lambda 表达式。
- 其次,它通过提供编译时安全性来避免编程错误。例如,下面给出了编译时错误。
- Consumer 接口具有名为accept() 的抽象方法。还有一个名为 andThen() 的默认方法,它本身返回 Consumer 对象。
- andThen() 方法使我们能够通过将一个 Consumer 与另一个链接来实现强大的复合 lambda 表达式。