Java | 函数式编程
更新: 12/7/2025 字数: 0 字 时长: 0 分钟
Java 不支持单独定义函数,但可以把静态方法视为独立的函数,把实例方法视为自带
this参数的函数。函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数。
函数式编程将函数作为基本运算单元,函数可作为变量传递,也可作为返回值。
函数式接口
- 函数式接口是指仅有一个抽象方法的接口,除了抽象方法外,允许包含多个默认方法或静态方法。
- 可以通过
@FunctionalInterface注解明确标识接口为函数式接口。 - 若接口满足单一抽象方法的条件,即便不加注解,编译器也会自动识别为函数式接口。
- 函数式接口是 Java 8 引入的概念,用于支持 Lambda 表达式和方法引用。
核心概念
在理解这些具体接口之前,需要先理解函数式接口的概念。
一个函数式接口是只包含一个抽象方法的接口。Java 8 引入了 @FunctionalInterface 注解,你可以用它来标记一个接口,但这并不是强制的,编译器会根据接口的结构自动判断。
函数式接口是 Lambda 表达式的目标类型。Lambda 表达式可以被看作是实现了这个接口的匿名类的实例。
Supplier<T>
- 中文解释: 提供者,无中生有
() -> 结果 - 英文名称:
Supplier(提供者) - 抽象方法:
T get() - 作用:
Supplier代表一个不接受任何参数但返回一个结果的函数。它通常用于延迟计算,或者当你需要一个值但这个值不是立即可用,或者需要按需生成时。 - Lambda 表达式形式:
() -> value或() -> expression - 应用场景:
- 延迟初始化/计算: 只有当真正需要这个值时才执行耗时的操作。
- 生成默认值: 当某个操作失败时提供一个备用值。
- 工厂方法模式: 创建对象的工厂。
Optional.orElseGet()方法就接受一个Supplier。
示例:
import java.util.function.Supplier;
import java.time.LocalDateTime;
public class SupplierExample {
public static void main(String[] args) {
// Supplier<String>: 提供一个 String 类型的结果
Supplier<String> helloSupplier = () -> "Hello from Supplier!";
System.out.println(helloSupplier.get()); // 输出:Hello from Supplier!
// Supplier<LocalDateTime>: 提供当前时间
Supplier<LocalDateTime> currentTimeSupplier = () -> LocalDateTime.now();
System.out.println("Current time: " + currentTimeSupplier.get());
// 延迟计算的例子
// 假设这是一个耗时操作
Supplier<String> expensiveOperationSupplier = () -> {
System.out.println("Executing expensive operation...");
try {
Thread.sleep(2000); // 模拟耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "Expensive Result";
};
System.out.println("Ready to get expensive result...");
// 只有调用 get() 方法时,耗时操作才会被执行
String result = expensiveOperationSupplier.get();
System.out.println("Got expensive result: " + result);
}
}Function<T, R>
- 中文解释: 函数,一个参数一个结果
(参数) -> 结果 - 英文名称:
Function(函数) - 抽象方法:
R apply(T t) - 作用:
Function代表一个接受一个类型为T的参数并返回一个类型为R的结果的函数。它是最常见的转换接口。 - Lambda 表达式形式:
(param) -> result或param -> result - 应用场景:
- 数据转换: 将一种类型的数据转换为另一种类型。
- 映射操作: 例如在 Stream API 中
map()方法就接受一个Function。 - 链式操作:
andThen()和compose()方法可以组合Function。
示例:
import java.util.function.Function;
public class FunctionExample {
public static void main(String[] args) {
// Function<String, Integer>: 接受一个 String,返回一个 Integer
Function<String, Integer> stringLengthFunction = s -> s.length();
System.out.println("Length of 'hello': " + stringLengthFunction.apply("hello")); // 输出:5
// Function<Integer, String>: 接受一个 Integer,返回一个 String
Function<Integer, String> intToStringFunction = i -> "Number: " + i;
System.out.println(intToStringFunction.apply(123)); // 输出:Number: 123
// 组合 Function: andThen()
// 先计算长度,再转换为字符串
Function<String, String> lengthToStringFunction = stringLengthFunction.andThen(intToStringFunction);
System.out.println(lengthToStringFunction.apply("world")); // 输出:Number: 5
// 组合 Function: compose() (与 andThen() 顺序相反)
// 先转换为字符串,再计算长度 (这里不太合理,只是演示 compose)
Function<Integer, Integer> composeExample = stringLengthFunction.compose(intToStringFunction);
System.out.println(composeExample.apply(456)); // intToStringFunction("456") -> "Number: 456" -> stringLengthFunction("Number: 456") -> 10
}
}BiFunction<T, U, R>
- 中文解释: 两个参数一个结果
(参数1, 参数2) -> 结果 - 英文名称:
BiFunction(双函数) - 抽象方法:
R apply(T t, U u) - 作用:
BiFunction代表一个接受两个类型分别为T和U的参数并返回一个类型为R的结果的函数。 - Lambda 表达式形式:
(param1, param2) -> result - 应用场景:
- 聚合操作: 将两个值组合成一个新值。
- 计算两个输入之间的关系。
Map.merge()和Map.compute()方法就接受一个BiFunction。
示例:
import java.util.function.BiFunction;
public class BiFunctionExample {
public static void main(String[] args) {
// BiFunction<Integer, Integer, Integer>: 接受两个 Integer,返回一个 Integer
BiFunction<Integer, Integer, Integer> addFunction = (a, b) -> a + b;
System.out.println("Sum of 5 and 3: " + addFunction.apply(5, 3)); // 输出:8
// BiFunction<String, String, String>: 接受两个 String,返回一个 String
BiFunction<String, String, String> concatFunction = (s1, s2) -> s1 + " " + s2;
System.out.println("Concatenated: " + concatFunction.apply("Hello", "World")); // 输出:Hello World
// BiFunction<String, Integer, String>: 接受 String 和 Integer,返回 String
BiFunction<String, Integer, String> repeatString = (text, times) -> {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < times; i++) {
sb.append(text);
}
return sb.toString();
};
System.out.println(repeatString.apply("Ha", 3)); // 输出:HaHaHa
}
}Consumer<T>
- 中文解释: 消费者,一个参数没结果
(参数) -> void - 英文名称:
Consumer(消费者) - 抽象方法:
void accept(T t) - 作用:
Consumer代表一个接受一个类型为T的参数但不返回任何结果(void)的操作。它通常用于执行副作用,例如打印、修改状态等。 - Lambda 表达式形式:
(param) -> { // do something }或param -> System.out.println(param) - 应用场景:
- 遍历集合并对每个元素执行操作: 例如 Stream API 中的
forEach()方法就接受一个Consumer。 - 执行回调操作。
- 打印输出。
- 遍历集合并对每个元素执行操作: 例如 Stream API 中的
示例:
import java.util.function.Consumer;
import java.util.Arrays;
import java.util.List;
public class ConsumerExample {
public static void main(String[] args) {
// Consumer<String>: 接受一个 String,没有返回值
Consumer<String> printConsumer = s -> System.out.println("Processing: " + s);
printConsumer.accept("apple"); // 输出:Processing: apple
// Consumer<Integer>: 接受一个 Integer,修改外部变量 (不推荐直接修改,但演示了副作用)
final int[] counter = {0}; // 数组是为了让 final 变量可变
Consumer<Integer> incrementCounter = i -> counter[0] += i;
incrementCounter.accept(10);
System.out.println("Counter after increment 10: " + counter[0]); // 输出:10
incrementCounter.accept(5);
System.out.println("Counter after increment 5: " + counter[0]); // 输出:15
// Stream API 中的 forEach 示例
List<String> fruits = Arrays.asList("apple", "banana", "cherry");
System.out.println("--- Fruits List ---");
fruits.forEach(printConsumer); // 对每个元素应用 printConsumer
// 等价于 fruits.forEach(s -> System.out.println("Processing: " + s));
}
}BiConsumer<T, U>
- 中文解释: 两个参数没结果
(参数1, 参数2) -> void - 英文名称:
BiConsumer(双消费者) - 抽象方法:
void accept(T t, U u) - 作用:
BiConsumer代表一个接受两个类型分别为T和U的参数但不返回任何结果(void)的操作。 - Lambda 表达式形式:
(param1, param2) -> { // do something } - 应用场景:
- 遍历 Map:
Map.forEach()方法就接受一个BiConsumer来处理键值对。 - 同时处理两个相关的输入。
- 批量更新或操作两个相关的数据。
- 遍历 Map:
示例:
import java.util.function.BiConsumer;
import java.util.HashMap;
import java.util.Map;
public class BiConsumerExample {
public static void main(String[] args) {
// BiConsumer<String, Integer>: 接受一个 String 和一个 Integer,没有返回值
BiConsumer<String, Integer> printKeyValue = (key, value) ->
System.out.println("Key: " + key + ", Value: " + value);
printKeyValue.accept("Age", 30); // 输出:Key: Age, Value: 30
// Map.forEach 示例
Map<String, Double> prices = new HashMap<>();
prices.put("Apple", 1.20);
prices.put("Banana", 0.80);
prices.put("Orange", 1.50);
System.out.println("--- Product Prices ---");
prices.forEach(printKeyValue); // 对 Map 中的每个键值对应用 printKeyValue
// 等价于 prices.forEach((key, value) -> System.out.println("Key: " + key + ", Value: " + value));
// 另一个 BiConsumer 例子:更新两个相关的值
final Map<String, Integer> stock = new HashMap<>();
stock.put("Laptop", 10);
stock.put("Mouse", 50);
BiConsumer<String, Integer> updateStock = (product, quantity) -> {
stock.merge(product, quantity, (oldValue, newValue) -> oldValue + newValue);
System.out.println("Updated stock for " + product + ": " + stock.get(product));
};
updateStock.accept("Laptop", 5); // 增加 Laptop 库存
updateStock.accept("Keyboard", 20); // 添加 Keyboard
}
}总结表格
| 接口名称 | 描述 | 抽象方法签名 | 参数数量 | 返回值类型 | Lambda 形式 | 典型应用场景 |
|---|---|---|---|---|---|---|
Supplier<T> | 无参数,返回一个结果 | T get() | 0 | T | () -> result | 延迟计算,按需生成值 |
Consumer<T> | 一个参数,无返回值 | void accept(T t) | 1 | void | param -> { ... } | 执行副作用,forEach |
BiConsumer<T, U> | 两个参数,无返回值 | void accept(T t, U u) | 2 | void | (p1, p2) -> { ... } | 遍历 Map,处理两个值 |
Function<T, R> | 一个参数,返回一个结果 | R apply(T t) | 1 | R | param -> result | 数据转换,map |
BiFunction<T, U, R> | 两个参数,返回一个结果 | R apply(T t, U u) | 2 | R | (p1, p2) -> result | 聚合两个值,复杂计算 |
Runnable | 无参数,无返回值 | void run() | 0 | void | () -> { ... } | 执行线程任务 |
Callable<V> | 无参数,返回一个结果 | V call() | 0 | V | () -> result | 带返回值的异步任务,它是 Runnable 的增强版 |
Comparator<T> | 两个参数,返回一个结果 | int compare(T o1, T o2) | 2 | int | (o1, o2) -> result | 对象排序,根据返回的整数值确定相对顺序 |
为什么引入这些接口?
- 支持 Lambda 表达式和方法引用: 它们是 Lambda 表达式的类型,使得代码更简洁、可读。
- 函数式编程范式: 更好地支持函数作为一等公民,允许将行为(函数)作为参数传递,或从方法中返回。
- Stream API 的基石: Java 8 Stream API 大量使用了这些函数式接口来定义各种操作(如
map,filter,forEach,reduce等)。 - 代码复用和抽象: 将常见的操作模式(如提供值、消费值、转换值)抽象成接口,提高了代码的复用性。
- 减少样板代码: 避免了为每个简单操作都创建匿名内部类的繁琐。
理解并熟练使用这些函数式接口是掌握 Java 8 及更高版本新特性的关键。
Lambda 表达式
基本概念
- Lambda 表达式是对匿名内部类的一种简化,用于实现函数式接口。
- 在 Lambda 表达式出现之前,为了实现一个接口,通常需要定义一个匿名内部类,这会导致生成额外的
.class文件。 - 使用 Lambda 表达式,我们可以避免显式编写类定义,简化代码,并且不会生成额外的
.class文件。 - Lambda 表达式在 Java 中并不需要显式地通过
class关键字定义一个类,就能实现函数式接口的功能。 - Lambda 表达式允许我们不必为接口编写实现类,直接将实现传递给方法,减少代码冗余。
使用方式
- 语法:
(参数) ‐> { 方法体 } - 和匿名内部类不同,Lambda 表达式仅支持接口,不支持抽象类。
- 如果一个方法的参数是函数式接口类型,那么可以直接使用 Lambda 表达式来实现该接口。
点击查看案例演示
javanew Thread(new Runnable() { @Override public void run() { System.out.println("传统方式"); } }).start();javanew Thread(() -> System.out.println("Lambda 方式")).start();- 语法:
使用细节
- 如果参数只有一个,那么可以省去小括号。
- 如果方法体中只有一个返回语句,可以直接省去花括号和
return关键字。 - 编译器会自动推断参数类型和返回值类型,避免显式声明。
点击查看案例演示
javaString[] array = {"Apple", "Orange", "Banana", "Lemon"}; // 传统匿名内部类写法 Arrays.sort(array, new Comparator<String>() { @Override public int compare(String s1, String s2) { return s1.compareTo(s2); } });javaString[] array = {"Apple", "Orange", "Banana", "Lemon"}; // 基础 Lambda 表达式(显式参数类型) Arrays.sort(array, (String s1, String s2) -> { return s1.compareTo(s2); });javaString[] array = {"Apple", "Orange", "Banana", "Lemon"}; // 省略参数类型(编译器自动推断) Arrays.sort(array, (s1, s2) -> { return s1.compareTo(s2); });javaString[] array = {"Apple", "Orange", "Banana", "Lemon"}; // 省略大括号和 return(仅当方法体为单行返回语句) Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));javaString[] array = {"Apple", "Orange", "Banana", "Lemon"}; // 方法引用替代 Lambda(参数与调用方法完全匹配) Arrays.sort(array, String::compareTo);
方法引用
基本概念
- 方法引用就是将一个已实现的方法,直接作为函数式接口中抽象方法的实现。当然前提是方法定义得一样才行。
- 方法引用是一种简化 Lambda 表达式的语法糖,允许你通过名称直接引用已有的方法。
- 当 Lambda 表达式仅仅调用一个已存在的方法时,可以直接用方法名替代完整的 Lambda 写法,使代码更简洁、更具可读性。
使用方式
- 如果某个方法的签名与函数式接口的抽象方法签名一致,就可以使用该方法进行引用。
- 方法签名只关心参数类型和返回类型,不考虑方法名或类的继承关系。
方法引用的优势
- 简洁性:比 Lambda 表达式更简洁,减少了冗余代码。
- 可读性:直接通过方法名表达了意图,使代码更易理解。
- 复用性:能够复用已有的方法实现,避免重复编写逻辑。
方法引用的类型
类型 语法 示例 Lambda 等效形式 参数映射规则 静态方法引用 类名::静态方法名Integer::sum(a, b) -> Integer.sum(a, b)参数直接传递 构造方法引用 类名::newString::new() -> new String()参数与构造方法参数匹配 未绑定实例方法引用 类名::实例方法名String::compareTo(s1, s2) -> s1.compareTo(s2)第一个参数为方法调用者 已绑定实例方法引用 对象::实例方法名System.out::printlns -> System.out.println(s)参数作为实例方法的参数 各类型的区别
- 静态方法引用:不需要对象,直接通过类名引用静态方法。
- 构造方法引用:构造方法的参数与 Lambda 表达式的参数匹配。
- 未绑定实例方法引用:实例方法需要对象调用,但这里没有明确指定的对象,则编译器会将第一个参数作为方法调用的目标对象。
- 已绑定实例方法引用:直接使用预先存在的对象作为方法调用者,函数式接口的参数直接作为方法参数。
静态方法引用
点击查看案例演示
函数式接口定义:
java@FunctionalInterface public interface Tools { int sum(int a, int b); // 待实现的求和方法 }使用匿名内部类实现:
javapublic class Main { public static void main(String[] args) { Tools tools = new Tools() { @Override public int sum(int a, int b) { return a + b; } }; System.out.println(tools.sum(1, 2)); } }也可以使用 Lambda 表达式:
javapublic class Main { public static void main(String[] args) { Tools tools = (a, b) -> a + b; System.out.println(tools.sum(1, 2)); } }只不过还能更简单,因为
Integer类中默认提供了求两个int值之和的静态方法:java// Integer 类中就已经有对应的实现了 public static int sum(int a, int b) { return a + b; }此时,我们可以直接将已有方法的实现作为函数式接口的实现:
javapublic class Main { public static void main(String[] args) { Tools tools = (a, b) -> Integer.sum(a, b); // 直接使用 Integer 为我们写好的求和方法 System.out.println(tools.sum(1, 2)); } }对应的匿名内部类的方式:
javapublic class Main { public static void main(String[] args) { Tools tools = new Tools() { @Override public int sum(int a, int b) { return Integer.sum(a, b); // 直接使用 Integer 为我们写好的求和方法 } }; System.out.println(tools.sum(1, 2)); } }我们发现,
Integer.sum的参数和返回值,跟我们在Tools接口中定义的完全一样,所以说我们可以直接使用方法引用:javapublic class Main { public static void main(String[] args) { Tools tools = Integer::sum; System.out.println(tools.sum(1, 2)); } }构造方法引用
点击查看案例演示
如果要把一个
List<String>转换为List<Person>,应该怎么办?javaclass Person { String name; public Person(String name) { this.name = name; } } List<String> names = List.of("Bob", "Alice", "Tim"); List<Person> persons = ???传统的做法是先定义一个
ArrayList<Person>,然后用for循环填充这个List:javaList<String> names = List.of("Bob", "Alice", "Tim"); List<Person> persons = new ArrayList<>(); for (String name : names) { persons.add(new Person(name)); }要更简单地实现
String到Person的转换,我们可以引用Person的构造方法:java// 引用构造方法 import java.util.*; import java.util.stream.*; public class Main { public static void main(String[] args) { List<String> names = List.of("Bob", "Alice", "Tim"); List<Person> persons = names.stream().map(Person::new).collect(Collectors.toList()); System.out.println(persons); } } class Person { String name; public Person(String name) { this.name = name; } public String toString() { return "Person:" + this.name; } }后面我们会讲到
Stream的map()方法。现在我们看到,这里的map()需要传入的FunctionalInterface的定义是:java@FunctionalInterface public interface Function<T, R> { R apply(T t); }把泛型对应上就是方法签名
Person apply(String),即传入参数String,返回类型Person。而
Person类的构造方法恰好满足这个条件,因为构造方法的参数是String,而构造方法虽然没有return语句,但它会隐式地返回
this实例,类型就是Person,因此,此处可以引用构造方法。构造方法的引用写法是
类名::new,因此,此处传入Person::new。未绑定实例方法引用
点击查看案例演示
javaimport java.util.Arrays; public class Main { public static void main(String[] args) { String[] array = {"Apple", "Orange", "Banana", "Lemon"}; Arrays.sort(array, String::compareTo); System.out.println(String.join(", ", array)); } }上面的代码可以编译通过,这说明
String.compareTo()方法符合 Lambda 定义。观察
String.compareTo()的方法定义:javapublic final class String { public int compareTo(String anotherString) { ... } }这个方法的签名只有一个参数,为什么和
int Comparator<String>.compare(String, String)能匹配呢?因为实例方法有一个隐含的
this参数,String类的compareTo()方法在实际调用的时候,第一个隐含参数总是传入this,相当于静态方法:javapublic static int compareTo(String this, String anotherString);所以,
String.compareTo()方法也可作为方法引用传入。已绑定实例方法引用
点击查看案例演示
Consumer<T>是 Java 内置的函数式接口,定义如下:java@FunctionalInterface public interface Consumer<T> { void accept(T t); // 抽象方法:接受一个参数 t,无返回值 }Consumer<String>的抽象方法accept(String)和System.out.println(String)的方法签名完全一致,因此可以直接通过方法引用
System.out::println来实现Consumer<String>接口。javaimport java.util.function.Consumer; public class Main { public static void main(String[] args) { Consumer<String> consumer = System.out::println; consumer.accept("Hello, World!"); // 直接通过方法引用调用实例方法 } }
