3530 字
18 分钟
Java函数式接口

https://www.cnblogs.com/dgwblog/p/11739500.html

https://juejin.cn/post/6844903892166148110

1. Supplier<T> - 数据的供给者 🎁#

接口定义@FunctionalInterface public interface Supplier<T> { T get(); }

核心作用Supplier 接口的核心职责是生产或提供数据,它不接受任何参数,但会返回一个 T 类型的结果。你可以把它想象成一个“工厂”或者“源头”,当你需要一个特定类型的对象时,就调用它的 get() 方法。

方法详解

  • T get(): 这是 Supplier 接口中唯一的抽象方法。调用它时,会执行你提供的 Lambda 表达式或方法引用所定义的逻辑,并返回一个结果。

常见应用场景

  • 延迟加载/创建对象:当某个对象的创建成本较高,或者并非立即需要时,可以使用 Supplier 来推迟其创建,直到真正使用时才调用 get()
  • 生成默认值或配置信息:提供一个默认对象或从某个源(如配置文件、数据库)获取配置。
  • 生成随机数据:如示例中的随机数生成器。
  • 作为工厂方法:在更复杂的场景中,Supplier 可以作为创建对象的简单工厂。

您的示例代码分析 (SupplierExample.java):

import java.util.Random;
import java.util.function.Supplier;
public class SupplierExample {
// 示例方法1: 接收一个 Supplier 来获取随机整数
public static Integer getRandomNumber(Supplier<Integer> randomNumberSupplier) {
// 调用 randomNumberSupplier 的 get 方法来执行其提供的逻辑
return randomNumberSupplier.get();
}
// 示例方法2: 接收一个 Supplier 来创建问候语字符串
public static String createGreetingMessage(Supplier<String> greetingSupplier) {
return greetingSupplier.get();
}
public static void main(String[] args) {
// 场景1: 获取随机数
// Lambda 表达式实现 Supplier: () -> new Random().nextInt(100)
// 这个 Lambda 不接受参数,返回一个 0-99 的随机整数
Supplier<Integer> randomIntSupplier = () -> new Random().nextInt(100);
Integer num = getRandomNumber(randomIntSupplier); // 传递行为
System.out.println("随机数: " + num);
// 场景2: 获取固定数字
// Lambda 表达式实现 Supplier: () -> 42
// 这个 Lambda 总是返回固定的数字 42
Supplier<Integer> fixedIntSupplier = () -> 42;
Integer fixedNum = getRandomNumber(fixedIntSupplier);
System.out.println("固定数字: " + fixedNum);
// 场景3: 创建不同的问候语
Supplier<String> englishGreeting = () -> "Hello, World!";
System.out.println(createGreetingMessage(englishGreeting)); // 输出: Hello, World!
Supplier<String> spanishGreeting = () -> "¡Hola, Mundo!";
System.out.println(createGreetingMessage(spanishGreeting)); // 输出: ¡Hola, Mundo!
}
}

代码解读

  • getRandomNumbercreateGreetingMessage 方法本身并不关心数字或字符串是如何产生的,它们只依赖传入的 Supplier 来提供结果。这体现了行为参数化——方法接受行为(通过函数式接口)作为参数。
  • main 方法中:
    • randomIntSupplier: 定义了一个行为——“生成一个0到99的随机整数”。
    • fixedIntSupplier: 定义了另一个行为——“总是提供数字42”。
    • englishGreetingspanishGreeting: 定义了不同的行为来提供特定的字符串。
  • 通过将不同的 Supplier 实现传递给同一个方法 (getRandomNumbercreateGreetingMessage),我们可以获得不同的结果,而无需修改方法本身。

关键益处

  • 灵活性:可以轻松替换不同的供给逻辑。
  • 解耦:数据的使用者和数据的生产者解耦。
  • 可测试性:可以方便地传入 mock 的 Supplier 进行单元测试。

2. Function<T, R> - 数据的转换器/映射器 🔄#

接口定义@FunctionalInterface public interface Function<T, R> { R apply(T t); }

核心作用Function 接口的核心职责是将一个类型 T 的输入参数转换或映射成另一个类型 R 的输出结果。它就像一个数据处理管道中的一个环节,接收数据,进行处理,然后传递给下一个环节。

方法详解

  • R apply(T t): 这是 Function 的核心方法。它接受一个 T 类型的参数 t,对其执行Lambda表达式或方法引用中定义的转换逻辑,并返回一个 R 类型的结果。

常见应用场景

  • 数据转换:例如,将字符串转换为整数,将日期对象格式化为字符串,或者如示例中计算字符串长度、数字平方。
  • 对象属性提取:从一个复杂对象中提取某个特定属性的值。例如,Person -> String (person.getName())
  • 链式操作Function 接口提供了 andThen()compose() 默认方法,可以方便地将多个 Function 串联起来形成一个处理流水线。

您的示例代码分析 (FunctionExample.java):

import java.util.function.Function;
public class FunctionExample {
// 示例方法1: 接收一个 Function 来计算字符串长度
public static Integer getStringLength(String text, Function<String, Integer> lengthCalculator) {
// 调用 lengthCalculator 的 apply 方法,传入 text,执行其转换逻辑
return lengthCalculator.apply(text);
}
// 示例方法2: 接收一个 Function 来计算数字的平方
public static Integer squareNumber(Integer number, Function<Integer, Integer> squareFunction) {
return squareFunction.apply(number);
}
public static void main(String[] args) {
// 场景1: 计算字符串长度
String myString = "Java Functional";
// Lambda 表达式实现 Function: s -> s.length()
// 这个 Lambda 接受一个 String s,返回其长度 (Integer)
Function<String, Integer> lengthLambda = s -> s.length();
Integer length = getStringLength(myString, lengthLambda);
System.out.println("字符串 '" + myString + "' 的长度是: " + length);
// 使用方法引用 (Method Reference) 实现 Function: String::length
// String::length 等价于 s -> s.length(),更为简洁
Integer lengthUsingMethodRef = getStringLength("Test", String::length);
System.out.println("字符串 'Test' 的长度是: " + lengthUsingMethodRef);
// 场景2: 计算数字平方
Integer num = 5;
// Lambda 表达式实现 Function: n -> n * n
// 接受一个 Integer n,返回 n 的平方 (Integer)
Function<Integer, Integer> squareLambda = n -> n * n;
Integer squared = squareNumber(num, squareLambda);
System.out.println(num + " 的平方是: " + squared);
Integer anotherNum = 10;
// 多行 Lambda 表达式
Function<Integer, Integer> verboseSquareLambda = x -> {
System.out.println("正在计算 " + x + " 的平方..."); // Lambda 可以包含多条语句
return x * x;
};
Integer squaredAgain = squareNumber(anotherNum, verboseSquareLambda);
System.out.println(anotherNum + " 的平方是: " + squaredAgain);
}
}

代码解读

  • getStringLengthsquareNumber 方法定义了操作的框架,但具体的转换逻辑由传入的 Function 对象决定。
  • main 方法中:
    • s -> s.length()String::length 都是 Function<String, Integer> 的实例,它们定义了“从字符串到其长度整数”的转换。
    • n -> n * nFunction<Integer, Integer> 的实例,定义了“从整数到其平方整数”的转换。
    • 多行 Lambda verboseSquareLambda 展示了更复杂的转换逻辑可以被封装。
  • 这种方式使得我们可以为同一个通用方法(如 getStringLength)提供不同的转换策略。

关键益处

  • 代码复用:通用的转换逻辑可以被封装成 Function 并在多处使用。
  • 可组合性:通过 andThencompose 可以构建复杂的转换流。
  • 清晰性:将数据转换的意图明确表达出来。

3. BiConsumer<T, U> - 双参数的消费者/执行者 🤝#

接口定义@FunctionalInterface public interface BiConsumer<T, U> { void accept(T t, U u); }

核心作用BiConsumer 接口的核心职责是对两个不同类型(或相同类型)的输入参数 TU 执行某个操作或产生某种副作用,但它不返回任何结果 (void)。你可以把它看作是需要两个输入才能完成其工作的“执行者”。

方法详解

  • void accept(T t, U u): 这是 BiConsumer 的核心方法。它接受两个参数 tu,并对它们执行 Lambda 表达式或方法引用中定义的操作。由于返回类型是 void,它通常用于执行有副作用的操作,如打印、修改集合、更新数据库等。

常见应用场景

  • 处理键值对:非常适合用于迭代 Map 的条目,如 Map.forEach() 方法就接受一个 BiConsumer<K, V>
  • 同时操作两个相关对象:当一个操作需要两个输入,并且不产生新的独立结果时。例如,将一个对象的属性设置到另一个对象上。
  • 配置或初始化:使用两个参数来配置某个组件。

您的示例代码分析 (BiConsumerExample.java):

import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;
public class BiConsumerExample {
// 示例方法1: 接收 BiConsumer 来打印键和值
public static <K, V> void printMapEntry(K key, V value, BiConsumer<K, V> entryPrinter) {
// 调用 entryPrinter 的 accept 方法,传入 key 和 value
entryPrinter.accept(key, value);
}
// 示例2 在 main 中直接演示了更常见的 Map 操作方式
// 辅助内部类,如果 BiConsumer 需要一次性接收多个信息 (在此示例中未直接用于核心 BiConsumer 演示)
// static class Pair<F, S> {
// F first; S second;
// Pair(F f, S s) { this.first = f; this.second = s; }
// }
public static void main(String[] args) {
// 场景1: 使用 printMapEntry 打印键值
// Lambda 表达式实现 BiConsumer: (k, v) -> System.out.println("键: " + k + ", 值: " + v)
// 接受一个 String k 和一个 Integer v,然后打印它们
BiConsumer<String, Integer> simplePrinter = (k, v) -> System.out.println("键: " + k + ", 值: " + v);
printMapEntry("年龄", 30, simplePrinter);
printMapEntry("数量", 100, simplePrinter);
// 场景2: 使用 BiConsumer 来填充 Map
Map<String, String> config = new HashMap<>();
// Lambda 表达式实现 BiConsumer: (key, value) -> config.put(key, value)
// 这个 Lambda 捕获了外部的 'config' Map 对象。
// 它接受 String key 和 String value,并将它们放入 config Map 中。
BiConsumer<String, String> mapPutter = (key, value) -> config.put(key, value);
mapPutter.accept("user.name", "Alice"); // 执行操作:config.put("user.name", "Alice")
mapPutter.accept("user.role", "Admin"); // 执行操作:config.put("user.role", "Admin")
System.out.println("配置Map: " + config);
// 场景3: Map.forEach() 的典型用法
// Map 的 forEach 方法直接接受一个 BiConsumer<K, V>
System.out.println("遍历Map:");
config.forEach((key, value) -> { // 这里的 (key, value) -> {...} 就是一个 BiConsumer
System.out.println("配置项 - " + key + ": " + value);
});
}
}

代码解读

  • printMapEntry 方法接受一个键、一个值和一个 BiConsumer,该 BiConsumer 定义了如何处理这对键值。
  • main 方法中:
    • simplePrinter: 定义了一个行为——“接收一个键和一个值,并将它们打印到控制台”。
    • mapPutter: 定义了一个行为——“接收一个键和一个字符串值,并将它们存入外部的 config Map”。这里 Lambda 表达式捕获了外部变量 config,这是一种常见的用法。
    • config.forEach(...): 这是 BiConsumer 最经典的用例之一。forEach 方法遍历 Map 中的每个条目,并对每个键值对执行提供的 BiConsumer 逻辑。

关键益处

  • 处理成对数据:专门设计用于需要两个输入的场景。
  • 与集合(尤其是Map)的良好集成Map.forEach 是一个很好的例子。
  • 封装副作用操作:可以将对两个参数的副作用操作(如修改、打印)封装起来。

4. Consumer<T> - 数据的消费者/执行者 🍽️#

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

核心作用Consumer 接口的核心职责是对单个输入参数 T 执行某个操作或产生某种副作用,它不返回任何结果 (void)。你可以把它看作是数据的“终点”或某个动作的执行者,它“消费”数据但不产生新的输出数据。

方法详解

  • void accept(T t): 这是 Consumer 的核心方法。它接受一个 T 类型的参数 t,并对其执行 Lambda 表达式或方法引用中定义的操作。因为返回 void,它主要用于执行那些为了副作用而进行的操作(如打印、修改对象状态、写入文件等)。

常见应用场景

  • 迭代集合并处理元素List.forEach() 方法接受一个 Consumer<T>,对列表中的每个元素执行指定操作。
  • 打印/日志记录:将信息输出到控制台、文件或其他日志系统。
  • 更新对象状态:修改传入对象的属性。
  • 回调:在某个异步操作完成后执行一个 Consumer 定义的动作。

您的示例代码分析 (ConsumerExample.java):

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
public class ConsumerExample {
// 示例方法1: 接收 Consumer 来展示单个项目
public static <T> void displayItem(T item, Consumer<T> itemDisplayer) {
// 调用 itemDisplayer 的 accept 方法,传入 item,执行其消费逻辑
itemDisplayer.accept(item);
}
// 示例方法2: 接收 Consumer 来处理列表中的每个项目
public static <T> void processListItems(List<T> list, Consumer<T> itemProcessor) {
for (T item : list) {
itemProcessor.accept(item); // 对列表中的每个 item 执行 itemProcessor 的逻辑
}
}
public static void main(String[] args) {
// 场景1: 使用 displayItem 打印信息
// Lambda 表达式实现 Consumer: message -> System.out.println("消息: " + message)
// 接受一个 String message,然后打印它
Consumer<String> consolePrinter = message -> System.out.println("消息: " + message);
displayItem("你好,函数式接口!", consolePrinter);
// 多行 Lambda 实现 Consumer,进行更复杂的打印
Consumer<Integer> detailedPrinter = number -> {
System.out.println("--- 数字详情 ---");
System.out.println("值: " + number);
System.out.println("是否偶数: " + (number % 2 == 0));
System.out.println("----------------");
};
displayItem(10, detailedPrinter);
displayItem(7, System.out::println); // 方法引用: System.out::println 等价于 x -> System.out.println(x)
// 场景2: 使用 processListItems 处理列表
List<String> names = Arrays.asList("爱丽丝", "鲍勃", "查理");
System.out.println("\n打印名字:");
// Lambda: name -> System.out.println("你好, " + name + "!")
// 对列表中的每个名字,执行打印问候语的操作
processListItems(names, name -> System.out.println("你好, " + name + "!"));
System.out.println("\n将名字转换为大写并打印 (仅打印,不修改原列表):");
// Lambda: name -> System.out.println(name.toUpperCase())
// 对列表中的每个名字,先转大写,然后打印
processListItems(names, name -> System.out.println(name.toUpperCase()));
// Consumer 也可以有副作用,比如修改外部状态 (通常需谨慎使用以避免复杂性)
StringBuilder allNames = new StringBuilder();
// Lambda: name -> allNames.append(name).append(" ")
// 这个 Consumer 修改了外部的 allNames 对象
processListItems(names, name -> allNames.append(name).append(" "));
System.out.println("\n拼接所有名字: " + allNames.toString().trim());
// List.forEach 的典型用法
System.out.println("\n使用 List.forEach 打印名字(大写):");
names.forEach(name -> System.out.println(name.toUpperCase())); // name -> System.out.println(...) 是一个Consumer
}
}

代码解读

  • displayItem 方法接受一个项目和一个 Consumer,该 Consumer 定义了如何“消费”或处理这个项目。
  • processListItems 方法遍历列表,并对每个元素应用传入的 Consumer 逻辑。这与 List.forEach() 的行为非常相似。
  • main 方法中:
    • consolePrinterdetailedPrinter 定义了不同的打印行为。System.out::println 是一个简洁的方法引用,用于直接打印。
    • 在处理 names 列表时,通过传递不同的 ConsumerprocessListItems,实现了不同的处理逻辑(简单问候、转换为大写打印、追加到 StringBuilder)。
    • allNames.append(...) 的例子展示了 Consumer 如何产生副作用(修改外部对象的状态)。虽然强大,但在复杂系统中应谨慎使用副作用,以保持代码的可预测性。
    • names.forEach(...) 直接使用了 List 接口内置的 forEach 方法,该方法就接受一个 Consumer

关键益处

  • 执行动作:非常适合表示对数据执行的无返回值的操作。
  • 迭代与处理:与集合框架(如 List.forEach)完美配合,简化迭代代码。
  • 封装副作用:将有副作用的操作(如I/O、UI更新)封装到 Consumer 中,使得代码意图更清晰。