函数式接口 FunctionalInterface

249 阅读10分钟

在 JDK 8 中,函数式接口首次被引入,允许将函数作为一等公民使用。这些接口的设计意图是简化代码结构,尤其是对于那些需要传递行为或操作的场景,减少匿名类的冗余。

函数式接口也是接口,只不过这个接口只有一个抽象方法,既然是接口,也就需要实现类,在函数式编程中,这个实现类一般使用 Lambda表达式或方法引用来实现。

Lambda表达式

Lambda 表达式是一种编写匿名函数的简洁语法,它能极大简化代码的编写,尤其是在需要传递行为(即函数作为参数)的场景下。

匿名函数(Anonymous Function)是一种没有名字的函数,它可以直接在代码中定义并使用,而无需事先声明一个具名的函数。匿名函数通常用于需要临时使用函数时,比如作为参数传递给其他方法。

一个完整的方法由 方法修饰符、返回类型、方法名、参数列表、方法体组成,如下:

public void sayHi{
    System.out.println("Hello, World!");
}

Lambda作为匿名函数,顾名思义就是没有方法名,没有方法名也不需要显式调用,也就不需要方法修饰符,那返回类型呢?可以根据类型推断推断出来,也不需要有,那方法就可以简化成参数列表+方法体,如下为Lambda表达式语法结构:

 (parameters) -> { statements; }

这其实还可以继续简化,如果没有参数列表则可以简化成:

 () -> { statements; }

如果参数列表只有一个参数,可以简化成:

parameter -> { statements; }

如果方法体中只有一条语句,可以简化成:

 (parameters) -> statement; 

总之,在单个参数,或单条方法体语句的情况下,小括号或大括号都可以省略。

上面的 sayHi 方法,如果采用 Lambda语法则是:

()->System.out.println("Hello, World!");

是不是非常简单!

了解了 Lambda 语法,就能实现函数式接口了,下面看看函数式接口长什么样子。

函数式接口

一个函数式接口是一个仅包含一个抽象方法的接口,这样的接口可以用来表示一个单一的方法(即函数)。

Lambda 表达式之所以能实现函数式接口,就在于函数式接口只有一个待实现的方法,方法的参数类型,返回值类型都在接口中定义了,所以这些东西 Lambda 都可以省略。

Java 标准库中提供了一些常见的函数式接口,这些接口位于 java.util.function 包中。下面就详细介绍一下。

函数式接口的定义使用 @FunctionalInterface 注解来标识,这个注解是可选的,但推荐使用,因为它可以帮助编译器和开发者明确该接口是一个函数式接口。

消费型接口 Consumer

消费型接口用于对给定的输入参数进行处理(消费),但不会返回任何结果。它的特点是执行某些操作而不产生输出。

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

Consumer 接口通常用于执行某些操作(如打印、记录、更新等),而不需要返回值,如下打印一句话:

// 创建一个 Consumer 实现,接收一个字符串并将其打印 
Consumer<String> printConsumer = s -> System.out.println(s); 

// 调用 accept 方法,执行操作 
printConsumer.accept("Hello, World!"); // 输出: Hello, World!

除了基本的 Consumer 接口,Java 还提供了其他变体的消费型接口:

BiConsumer<T, U> :接受两个输入参数,不返回结果

DoubleConsumer :接收一个double值,不返回结果

IntConsumer :接收一个int值,不返回结果

ObjDoubleConsumer<T>:接收一个对象和一个double值,不返回结果

⨳ ...

以适配常用的消费场景。

除此之外,Consumer 接口包括其他变体的消费型接口都有一个default 方法:

@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); };
    }

}

andThen(Consumer<? super T> after),允许组合多个 Consumer 成为一个 Consumer ,按顺序消费传入的参数。

Consumer<String> greetConsumer = s -> System.out.println("Hello, " + s); 
Consumer<String> goodbyeConsumer = s -> System.out.println("Goodbye, " + s); 

// 组合两个 Consumer,先执行 greetConsumer,然后执行 goodbyeConsumer 
Consumer<String> combinedConsumer = greetConsumer.andThen(goodbyeConsumer); 
combinedConsumer.accept("Alice"); // 输出: Hello, Alice 和 Goodbye, Alice

在 JDK 8 之前,所有接口中的方法必须是抽象的,无法提供方法实现。如果要向接口中添加新方法,所有现有的实现类都需要更新并实现该新方法。而 default 方法允许在接口中定义方法的默认实现,从而打破了接口只能声明方法、不能有方法实现的传统约定。这一特性使接口能够在不破坏已有实现类的情况下扩展新方法。

供给型接口 Supplier

供给型接口和消费者接口的作用完全相反,供给型接口提供(或生成)一个结果,不接受任何输入参数。

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

当需要在某个时刻提供一个值,但该值的计算可能很耗时或不一定每次都需要时,可以使用 Supplier。如下:

// 创建一个 Supplier,供给一个随机数 
Supplier<Double> randomSupplier = () -> Math.random(); // 每次调用 get() 都会生成一个新的随机数 

System.out.println(randomSupplier.get()); // 输出一个随机数 
System.out.println(randomSupplier.get()); // 输出一个不同的随机数

如果方法体只有一个 return 语句,连 return 都省略掉了。

Consumer 接口一样,Java 还提供了其他变体的供给型接口:BooleanSupplierDoubleSupplierIntSupplierLongSupplier... 按需使用就行了。

断言型接口 Predicate

断言型接口用于对输入参数进行条件测试,并返回一个布尔值(truefalse)。

@FunctionalInterface
public interface Predicate<T> { 
    boolean test(T t);
    // ...
}

通常用于过滤、匹配、验证等逻辑判断场景。

// 创建一个 Predicate 实例,判断整数是否为偶数 
Predicate<Integer> isEven = n -> n % 2 == 0; // 测试断言 
System.out.println(isEven.test(4)); // 输出: true 
System.out.println(isEven.test(7)); // 输出: false

断言接口提供了比消费接口更多用于组合的 defaul 方法:

and(Predicate<T> other) :与操作,两个条件都为 true 时结果为 true

or(Predicate<T> other) :或操作,只要一个条件为 true 结果为 true

negate() :非操作,将结果取反。

基本的逻辑运算(与、或、非)都有了,那组合成更复杂的逻辑判断也不就水到渠成了。

Supplier 接口一样,Java 还提供了其他变体的断言型接口:BiPredicateDoublePredicateIntPredicateLongPredicate... 按需使用就行了。

功能型接口 Function

功能型接口的主要作用是接收一个输入参数,并返回一个结果。也就说它最全能,最没特殊的函数式接口。

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
    // ...
}    

Function<T, R> 常用于将输入值进行转换、映射或处理,并产生输出值的场景。

// 创建一个 Function 实例,将字符串转换为其长度 
Function<String, Integer> lengthFunction = s -> s.length(); 
// 调用 apply 方法 
int length = lengthFunction.apply("Hello, World!"); 
System.out.println(length); // 输出: 13

功能型接口也可以使用 defaul 方法进行组合,比起消费型接口只能通过 andThen 方法进行前后组合,还可以使用 compose 方法进行后前组合。

前后组合的意思是:consumer1.andThen(consumer2) 组成的 Consumer 调用 accept 方法,会先执行 consumer1 的 accept 方法,再执行 consumer2 的 accept 方法。后前组合执行顺序与此相反。

Function 同样也有很多变体,大家去 java.util.function 下去找就行,以 FunctionOperator 结尾的都是。

Runnable 接口是什么类型的函数式接口呢?

方法引用

前面说过,函数式接口可以使用 Lambda 或 方法引用 实现,Lambda 已经够简单的了,JDK 8 还同时引入方法引用干啥,难道这个方法引用还能比 Lambda 更简洁?

比如说实现消费型接口打印一句话吧,Lambda 实现方式如下:

Consumer<String> printLambda = s -> System.out.println(s);

使用方法引用如下:

Consumer<String> printMethodRef = System.out::println;

可以看到方法引用直接将 Lambda 表达式的参数列表给省略了。

没有参数列表该怎么在方法体中使用这个参数呢?那就不用了呗,如果 Lambda 表达式只是在调用现有方法时,方法引用能够省去不必要的参数和方法体的书写

Lambda 根据类型推断省略了参数类型和方法返回值类型,而方法引用更近一步,连参数列表都省略了。

如果函数式接口的抽象方法的参数和返回值类型与被引用的方法的参数和返回值类型相匹配,那就可以使用方法引用替代Lambda了:

函数式接口的抽象方法的参数列表 必须与 引用的方法的参数列表 对应(数量、顺序、类型要匹配)。

函数式接口的抽象方法的返回值 必须与 引用的方法的返回值类型 相同。

这样看来,方法引用的使用场景还是很苛刻的。

方法引用的语法很简单,就是换个方式调用其他类的方法。

引用静态方法 类名::静态方法名

// 引用了 `String` 类的静态方法 `valueOf(int)`。
Function<Integer, String> converter = String::valueOf;

引用特定对象的实例方法 对象::实例方法名

// 引用了 `System.out` 对象的实例方法 `println(String)`。
Consumer<String> printer = System.out::println;

引用任意对象的实例方法 对象::实例方法名

// 引用了 `String` 类的实例方法 `equals(Object)`;
// Lambda 表达式的第一个参数作为调用 `equals` 方法的对象。
BiPredicate<String, String> equalsChecker = String::equals;

引用构造函数 类名::new

// 引用了 `ArrayList` 类的构造函数,用于创建新的 `ArrayList` 对象。
Supplier<List<String>> listSupplier = ArrayList::new;

每种方法引用都有它特定的使用场景,关键是参数和返回类型要与函数式接口匹配。

JDK 中的函数式编程核心就这些东西,下面看看函数式接口在 Optional 类中的使用。

Optional

Optional 是在 Java 8 中引入的一个容器类,用来表示一个可能存在也可能不存在的值。

public final class Optional<T> {
    private final T value;
    // ...
 }

将一个值用 Optional 包装起来,可以有效避免 NullPointerExceptionOptional直译就是“可选择的”,表示这个值可能会是一个值,也可能是一个空值。

既然Optional是对 value 的包装,那肯定会有包装的方法:

Optional.of(T value) :创建一个包含非 null 值的 Optional 对象。如果传入的值为 null,会抛出 NullPointerException

Optional.ofNullable(T value) :创建一个可能为空的 Optional 对象。如果传入的值为 null,则创建一个空的 Optional 对象。

也得有访问 Optional 对象的值的方法:、

isPresent() :如果值存在,返回 true,否则返回 false

get() :获取 Optional 中的值,如果值不存在,抛出 NoSuchElementException。使用时应确保值存在。

orElse(T other) :如果 Optional 中的值存在,则返回该值;如果不存在,则返回 other

这个类还提供了一些方法,可以让函数式接口来处理这个值 value

ifPresent(Consumer<? super T> action) :如果 Optional 中存在值,它会对该值执行 Consumer 操作。

Optional<String> optional = Optional.of("Hello");
optional.ifPresent(value -> System.out.println(value));  // 输出: Hello

filter(Predicate<? super T> predicate) :如果 Optional 中的值满足 Predicate 的条件,它会返回包含该值的 Optional;否则返回 Optional.empty()

⨳ ...

Optional 只是开胃小菜,下一篇《Stream》才是对函数式接口使用的集大成者。

总结

函数式接口 是在 Java 8 中引入的概念,用于支持 Lambda 表达式和方法引用。

在 Java 中,函数式接口本质就是一个锚点,无论是Lambda 还是 方法引用 都要挂载到这个锚点上才有意义。

由于函数式接口可以用 Lambda 表达式或方法引用实现,开发者不需要编写大量的匿名类实现,从而减少样板代码,简化开发。

Lambda 表达式往往是一次性使用的,很难像普通方法那样进行复用。在需要复杂或可复用的逻辑时,可能还是需要编写常规方法或类。