Java 8 的函数式编程是在吸收了其他语言的精髓后得到的产物,在原有的基础上新增了更多抽象的函数式接口,便于开发者实现高效优雅的编码。
此前常见的函数式接口包括 Runnable,Callable,Comparator 等,新增的函数式接口定义在 java.util.function 包中,常见的基本接口主要包括 predicate,Consumer,Supplier 和 Function,他们的含义以及使用特征大致概括如下表:
| 接口名 | 含义 | 入参出参形式 |
|---|---|---|
| Predicate | 谓词型,又称断言型 | 接收一个入参,返回一个布尔值 |
| Consumer | 消费型 | 接收一个入参,无出参 |
| Supplier | 供给型 | 无入参,返回一个出参 |
| Function | 功能型 | 接收一个入参,返回一个出参 |
在 JDK8 之前,实现函数式接口的方式主要是通过重新匿名内部类,最经典的例子就是 Comparator 接口中 compare方法的重写。自 JDK8 开始,我们就可以通过 lambda 表达式更加简洁地实现函数式接口,优化了原来的匿名内部类的形式。
通过查看源码发现,在函数式接口下共有三种类型的方法:
- 唯一的抽象方法。接口内如果存在其他方法,都是基于这个抽象方法进行拓展的。
- 使用 default 定义的普通方法(默认方法)。如果接口中的默认方法不能满足某个实现类需要,那么实现类可以覆盖默认方法。签名跟接口 default 方法一致,但是不能再使用 default 修饰符。
- 使用 static 修饰的静态方法,可直接调用。
如果想要定义一个函数式接口,可以使用注解 @FunctionInterface,只定义一个抽象方法即可。
应用实例函数式接口在实际应用中,其本质就是将一个函数的表达当作一个参数进行传递和处理,也就是对方法进行引用,因此使用 lambda 表达进行实例化能更加高效地进行编码。
Predicate谓词型(断言型)接口用来判断入参的泛型 T 对象是否符合特定条件,如果符合则返回 true,否则返回 false。该接口源码中定义了一个抽象方法 test:
boolean test(T t);
另外还提供了诸如与、或、非操作的方法,通过入参传入其他 predicate 方法,实现多个条件相结合:
default Predicateand(Predicate super T> other) { Objects.requireNonNull(other); return (t) -> test(t) && other.test(t); } default Predicate or(Predicate super T> other) { Objects.requireNonNull(other); return (t) -> test(t) || other.test(t); } default Predicate negate() { return (t) -> !test(t); } static Predicate isEqual(Object targetRef) { return (null == targetRef) ? Objects::isNull : object -> targetRef.equals(object); } static Predicate not(Predicate super T> target) { Objects.requireNonNull(target); return (Predicate )target.negate(); }
由于接口不能直接实例化,所以可以通过内部匿名类或者 lambda 表达式进行实例化。下面以 lambda 表达式为例:
// 满足给定条件的字符串将其打印出来,否则打印"nothing"
void demoPredicate() {
String str = "demo string for predicate test";
printResult(str, s -> length(s) > 20); // 长度大于20才打印
}
void printResult(String str, Predicate predicate) {
predicate.test(str) ? System.out.println(str) : System.out.println("nothing");
}
在 java.util.function 包中还定义了针对特定基本类型的断言型接口,其用法跟上述基本一致,因此不再作详细说明。
Consumer消费型接口顾名思义,消耗接收的一个泛型 T 入参,执行操作后,不返回结果。其源码定义了一个抽象方法 accept 以及一个 andThen 方法:
void accept(T t); default ConsumerandThen(Consumer super T> after) { Objects.requireNonNull(after); return (T t) -> { accept(t); after.accept(t);}; }
我们把上面的例子做一点修改,将打印字符串这个操作作为一个入参:
void demoConsumer() {
String str = "demo string for consumer test";
consumeAction(str, s -> System.out.println(s));
}
void consumeAction(String str, Consumer consumer) {
consumer.accrpt(str);
}
Supplier
供给型可以生成一个泛型 T 对象进行返回,无需接收入参。其源码定义了一个抽象方法 get:
T get();
以获取当前系统时间作为例子进行说明:
void demoSupplier() {
Long time = supply(() -> System.currentTimeMillis());
}
T supply(Supplier supplier) {
return supplier.get();
}
Function
功能型接口,该单词本身也是函数的意思,其接收一个泛型 T 对象,返回一个泛型 R 对象。其源码定义了一个基础的抽象方法 apply 以及其他延伸方法:
R apply(T t); // 先执行before对象的apply方法,再对结果执行当前对象的apply方法。 defaultFunction compose(Function super V, ? extends T> before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v)); } // 先执行当前对象的apply方法, 再对结果执行after对象的apply方法。 default Function andThen(Function super R, ? extends V> after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t)); } // 返回一个函数对象,其执行了apply()方法只会返回入参。 static Function identity() { return t -> t; }
以对字符串添加指定前缀后再获取长度作为例子进行说明:
void demoFunction() {
String str = "demo string for function test";
System.out.println(func(str, s -> "Content: " + s), String::length);
}
R func(T t, Function f1, Function f2) {
// 先执行f1(添加前缀),后执行f2(获取长度)
return f1.andThen(f2).apply(t);
}
在 Function 接口之下,还有有一些常用的子接口,如 UnaryOperator 和 BinaryOperator,在一定程度上拓展了 Function 接口的功能。
UnaryOperator 是一元操作符的意思,该接口接收一个泛型 T 对象,返回相同类型的 T 对象。从源码中可以发现,UnaryOperator 继承了 Function 接口,定义了一个方法 identity:
staticUnaryOperator identity() { return t -> t; }
BinaryOperator 是二元操作符的意思,该接口接收两个相同的泛型 T 对象,返回一个泛型 R 对象。从源码中可以看出,其继承了 BiFunction 接口,并定义了 apply 方法:
R apply(T t, U u);
由于这两个接口继承了 Function 接口,因此同样可以使用 apply 方法。



