2023-11-16  阅读(32)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/series/article/1841959591

在文章 Lambda 表达式 提过,Lambda 能够简化的一个依据就是函数式接口,这篇文章我们就来深入了解函数式接口。

什么是函数式接口

函数式接口是一个只有一个抽象方法的接口,最开始的时候也叫做 SAM 类型的接口(Single Abstract Method)。它具有两个特点:

  1. 只包含一个抽象方法:函数式接口只能有一个抽象方法,但可以包含多个默认方法或静态方法。
  2. **@FunctionalInterface**注解标记:该注解不强制,但通常会使用它来标记该接口为函数式接口。这样做可以让编译器检查接口是否符合函数式接口的定义,以避免不必要的错误。

Java 引入函数式接口的主要目的是支持函数式编程范式,也就是 Lambda 表达式。在函数式编程语言中,函数被当做一等公民对待,Lambda 表达式的类型是函数,它可以像其他数据类型一样进行传递、赋值和操作。但是在 Java 中,“一切皆对象”是不可违背的宗旨,所以 Lambda 表达式是对象,而不是函数,他们必须要依附于一类特别的对象类型:函数式接口。

所以,从本质上来说 Lambda 表达式就是一个函数式接口的实例。这就是 Lambda 表达式和函数式接口的关系。简单理解就是只要一个对象时函数式接口的实例,那么该对象就可以用 Lambda 表达式来表示。

自定义函数式接口

根据函数式接口的定义和特点,我们可以自定义函数式接口:

@FunctionalInterface
public interface FunctionInterface {

    /**
     * 抽象方法
     */
    void doSomething();

    /**
     * 默认方法
     * @param s
     */
    default void defaultMethod(String s) {
        System.out.println("默认方法:" + s);
    }

    /**
     * 静态方法
     * @param s
     */
    static void staticMethod(String s) {
        System.out.println("静态方法:" + s);
    }
}

FunctionInterface 是一个自定义函数式接口,它只包含一个抽象方法 doSomething(),还包含一个默认方法 defaultMethod(String s) 和一个静态方法 staticMethod(String s),这两个方法都是可选的。

@FunctionalInterface 注解是可写可可不写的,但是我们一般都推荐写,写上他可以让编译器检查接口是否符合函数式接口的定义,以避免不必要的错误,比如:

上面接口定义了两个抽象方法,它会明确告诉你错误了。

使用如下:

        FunctionInterface functionInterface = () -> {
            System.out.println("死磕 Java 就是牛...");
        };

        // 调用抽象方法
        functionInterface.doSomething();
        // 调用默认方法
        functionInterface.defaultMethod("死磕 Netty 就是牛...");
        // 调用静态方法
        FunctionInterface.staticMethod("死磕 Java 并发就是牛...");

执行如下:

常用函数式接口

其实在 Java 8 之前就已经有了大量的函数式接口,我们最熟悉的就是 java.lang.Runnable接口了。Java 8 之前已有的函数式接口:

  • java.lang.Runnable
  • java.util.concurrent.Callable
  • java.security.PrivilegedAction
  • java.util.Comparator
  • java.io.FileFilter
  • java.nio.file.PathMatcher
  • java.lang.reflect.InvocationHandler
  • java.beans.PropertyChangeListener
  • java.awt.event.ActionListener
  • javax.swing.event.ChangeListener

而在 Java 8 中,新增的函数式接口都在 java.util.function 包中,里面有很多函数式接口,用来支持 Java 的函数式编程,从而丰富了 Lambda 表达式的使用场景。我们使用最多的也是最核心的函数式接口有四个:

  • java.util.function.Consumer:消费型接口
  • java.util.function.Function:函数型接口
  • java.util.function.Supplier:供给型接口
  • java.util.function.Predicate:断定型接口

下面我们就来看这四个函数式接口的使用方法

Consumer 接口

Consumer 代表这一个接受一个输入参数并且不返回任何结果的操作。它包含一个抽象方法 accept(T t),该方法接受一个参数 t,并对该参数执行某种操作:

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

由于 Consumer 接口中包含的抽象方法不返回结果,所以它通常用于对对象进行一些操作,如修改、输出、打印等。它的使用方法也比较简单,分为两步。

  1. 创建一个 Consumer 对象:使用 Lambda 表达式来创建一个对象,定义在 accept(T t) 中要执行的操作。
Consumer<String> consumer = str -> System.out.println(str);
  1. 使用 Consumer 对象
consumer.accept("死磕 Java 就是牛...");

// 输出结果...

死磕 Java 就是牛...

在 Consumer 接口中还有一个默认方法 andThen(),该方法接受一个 Consumer 实例对象 after,它允许我们将两个 Consumer 对象组合在一起,形成一个新的 Consumer 对象,该新对象按照顺序执行这两个 Consumer 对象的操作。先执行调用andThen()接口的accept(),然后再执行andThen()参数after中的accept()

Consumer<String> consumer1 = str -> System.out.println("consumer1:" + str);
Consumer<String> consumer2 = str -> System.out.println("consumer2:" + str);

consumer1.andThen(consumer2).accept("死磕 Java 就是牛..");

// 输出结果...

consumer1:死磕 Java 就是牛..
consumer2:死磕 Java 就是牛..

Function 接口

Function 代表一个接受一个输入参数并且产生一个输出结果的函数。它包含一个抽象方法 R apply(T t),该方法接受一个参数 t(类型为 T),并返回一个结果(类型为 R),我们可以理解为根据一个数据类型 T ,经过一系列的操作后得到类型 R。Function 接口是非常通用的,应该是他们四个当中使用最为广泛的。

用途一:函数转换

Function 可以用于将一个类型的值转换为另一个类型的值。它可以用于各种转换操作,如类型转换、数据映射等。

Function<String,Integer> function = str -> Integer.parseInt(str);
int result = function.apply("456");
// 输出结果...
456

用途二:数据处理

Function 可用于对输入数据进行处理并生成输出结果。它可以用于执行各种操作,如过滤、计算、提取、格式化等。

Function<List<String>, String> function = list -> {
    StringBuilder result = new StringBuilder();
        for (String str : list) {
            if (str.startsWith("李")) {
                result.append(str).append(",");
            }
        }

        return result.toString();
    };
List<String> list = Arrays.asList("张三","李四","李武","李柳");
System.out.println(function.apply(list));
// 输出结果...
李四,李武,李柳,   

andThen():方法链式调用

andThen() 接受一个 Function 作为参数,并返回一个新的 Function,该新函数首先应用当前函数,然后将结果传递给参数函数。这种方法链的方式可以用于将多个函数组合在一起,以执行一系列操作。

Function<String,Integer> function1 = t -> Integer.parseInt(t);
Function<Integer,Integer> function2 = t -> t * 10;
System.out.println(function1.andThen(function2).apply("20"));

先将 String 转换为 Integer,然后再 * 10,利用 andThen() 我们可以进行一系列复杂的操作。

compose():顺序执行

compose()andThen()相反,它首先应用参数函数,然后再应用当前函数,这种可能更加好理解些,常用于一些顺序执行。

Function<String,Integer> function1 = t -> {
  System.out.println("function1");
  return Integer.parseInt(t);
};
Function<Integer,Integer> function2 = t -> {
  System.out.println("function2");
  return t * 10;
};
Function<Integer,String> function3 = t -> {
  System.out.println("function3");
  return t.toString();
};
System.out.println(function3.compose(function2.compose(function1)).apply("20"));
        
// 输出结果...
function1
function2
function3
200


从输出结果中可以更加直观地看清楚他们的执行顺序。

identity()恒等函数

identity() 返回一个恒等函数,它仅返回其输入值,对输入值不进行任何操作。源码如下:

    static <T> Function<T, T> identity() {
        return t -> t;
    }

一看感觉 identity() 没啥用处,其实它在某些场景大有用处,例如

  • 作为默认函数

identity() 可以作为函数组合链中的起点或默认函数。当我们想构建一个函数组合链时,可以使用 identity 作为初始函数,然后使用 andThen()compose() 方法添加其他函数。这种方式允许您以一种优雅的方式处理链的起点。

Function<String,String> function1 = Function.identity();
Function<String,String> function2 = str -> str.toUpperCase();
Function<String,String> function3 = str -> str + " WORLD!!!";

System.out.println(function3.compose(function2.compose(function1)).apply("hello"));
  • 保持一致性

在某些情况下,我们可能需要一个函数,但不需要对输入进行任何操作。使用 identity() 可以确保函数的签名(输入和输出类型)与其他函数一致。

Supplier 接口

Supplier 是一个代表生产(或供应)某种结果的接口,它不接受任何参数,但能够提供一个结果。它定义了一个 get() 的抽象方法,用于获取结果。

接口定义简单,使用也简单:

Supplier<LocalDate> supplier = () -> LocalDate.now();
LocalDate localDate = supplier.get();

Supplier 接口通常用于惰性求值,只有在需要结果的时候才会执行 get() 。这对于延迟计算和性能优化非常有用。

Predicate 接口

Predicate 表示一个谓词,它接受一个输入参数并返回一个布尔值,用于表示某个条件是否满足。抽象方法为 test(),使用如下:

Predicate<String> predicate = str -> str.length() > 10;
boolean result = predicate.test("www.skjava.com");

判断某个字符长度是否大于 10。

and():表示两个 Predicate 的 与操作

Predicate<Integer> predicate1 = x -> x > 10;
Predicate<Integer> predicate2 = x -> x % 2 == 0;
boolean result = predicate1.and(predicate2).test(13);

or():表示两个 Predicate 的或操作

Predicate<Integer> predicate1 = x -> x > 10;
Predicate<Integer> predicate2 = x -> x % 2 == 0;
boolean result = predicate1.or(predicate2).test(13);

negate():表示 Predicate 的逻辑非操作

Predicate<Integer> predicate1 = x -> x > 10;
boolean result = predicate1.negate().test(14);

其他函数式接口

除了上面四个常用的函数式接口外,java.util.function 包下面还定义了很多函数式接口,下面做一个简单的介绍:

接口 说明
BiConsumer<T,U> 表示接受两个不同类型的参数,但不返回任何结果的操作
BiFunction<T,U,R> 表示接受两个不同类型的参数,并返回一个其它类型的结果的操作
BinaryOperator 表示接受两个相同类型的参数,并返回一个同一类型的结果的操作
BiPredicate<T,U> 表示接受两个不同诶行的参数,且返回布尔类型的结果的操作
BooleanSupplier 不接受任何参数,且返回一个布尔类型的结果的操作
DoubleBinaryOperator 表示接受两个double类型的参数,并返回double类型结果的操作
DoubleConsumer 表示接受一个double类型的参数,但不返回任何结果的操作
DoubleFunction 表示接受一个double类型的参数,且返回一个R类型的结果的操作
DoublePredicate 表示一个接受两个double类型的参数,且返回一个布尔类型的结果的操作
DoubleSupplier 表示一个不接受任何参数,但返回布尔类型的结果的操作
DoubleToIntFunction 表示接受两个double类型的参数,但返回一个int类型的结果的操作
DoubleToLongFunction 表示接受两个double类型的参数,但返回一个long类型的结果的操作
DoubleUnaryOperator 表示接受一个double类型的参数,且返回一个double类型的结果的操作
IntBinaryOperator 表示一个接受两个int类型的参数,且返回一个int类型的结果的操作
IntConsumer 表示接受一个int类型的参数,但不返回任何结果的操作
IntFunction 表示接受一个int类型的参数,但返回一个R类型的结果的操作
IntPredicate 表示接受一个int类型的参数,但返回布尔类型的结果的操作
IntSupplier 表示不接受任何参数,但返回一个int类型的结果的操作
IntToDoubleFunction 表示接受一个int类型的参数,但返回一个double类型的结果的操作
IntToLongFunction 表示接受一个int类型的参数,但返回一个long类型的结果的操作
IntUnaryOperator 表示接受一个int类型的参数,且返回一个int类型的结果的操作
LongBinaryOperator 表示接受两个long类型的参数,且返回一个long类型的结果的操作
LongConsumer 表示不接受任何参数,但返回一个long类型的结果的操作
LongFunction 表示接受一个loing类型的参数,但返回一个R类型的结果的操作
LongPredicate 表示接受一个long类型的参数,但返回布尔类型的结果的操作
LongSupplier 表示不接受任何参数,但返回一个long类型的结果的操作
LongToDoubleFunction 表示接受一个long类型的参数,但返回一个double类型的结果的函数
LongToIntFunction 表示接受一个long类型的参数,但返回int类型的结果的函数
LongUnaryOperator 表示接受一个long类型的参数,并返回一个long类型的结果的操作
ObjDoubleConsumer 表示接受两个参数,一个为T类型的对象,另一个double类型,但不返回任何结果的操作
ObjIntConsumer 表示接受两个参数,一个为T类型的对象,另一个int类型,但不返回任何结果的操作
ObjLongConsumer 表示接受两个参数,一个为T类型的对象,另一个double类型,但不返回任何结果的操作
ToDoubleBiFunction<T,U> 表示接受两个不同类型的参数,但返回一个double类型的结果的操作
ToDoubleFunction 表示一个接受指定类型T的参数,并返回一个double类型的结果的操作
ToIntBiFunction<T,U> 表示接受两个不同类型的参数,但返回一个int类型的结果的操作
ToIntFunction 表示一个接受指定类型T的参数,并返回一个int类型的结果的操作
ToLongBiFunction<T,U> 表示接受两个不同类型的参数,但返回一个long类型的结果的操作
ToLongFunction 表示一个接受指定类型的参数,并返回一个long类型的结果的操作
UnaryOperator 表示接受一个参数,并返回一个与参数类型相同的结果的操作

函数式接口使用非常灵活,上面的举例都是很简单的 demo,它需要我们在日常开发过程中多多使用才能灵活地运用它。


大明哥花了两个月时间终于写完了 Java 8 ~ Java 21 所有的重要特性,整个系列共 63 篇文章,11w+ 字。

现在终于将其整理成了 PDF 版本,同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【Java 新特性】 即可免费领取。

阅读全文