Java 函数式编程:Lambda、Stream 与方法引用
引言:为什么现代 Java 需要函数式编程
Java 8 引入的函数式编程特性彻底改变了代码的组织方式。它让我们可以用更简洁、更安全的语法处理集合和业务逻辑,减少样板代码,同时让意图更清晰。本教程将带你从零掌握三大核心工具:Lambda 表达式、Stream API 和方法引用。无论你是刚接触 Java 8 还是希望系统巩固知识,都能在这里找到结构化的学习路径。
环境准备
- JDK 8 及以上版本
- 任意 IDE(IntelliJ IDEA、Eclipse 或 VS Code)
- 基本的 Java 语法知识(类、接口、匿名内部类)
1. Lambda 表达式:把行为当作参数传递
Lambda 的本质是一个可传递的匿名函数——它没有名称,但有参数列表、函数体和返回值。它的出现替代了大部分匿名内部类的冗长写法。
1.1 语法格式
(参数列表) -> { 函数体 }
- 参数列表:可以为空
(),可以省略类型(编译器自动推断) - 箭头:
->分隔参数和实现 - 函数体:若只有一行语句可省略
{}和return
// 无参,无返回值
() -> System.out.println("Hello Lambda")
// 单个参数,省略类型和括号
x -> x * 2
// 多个参数,显式类型
(int a, int b) -> { return a + b; }
1.2 函数式接口:Lambda 的安身之所
Lambda 表达式必须匹配一个函数式接口(仅有一个抽象方法的接口)。Java 自带的典型代表:
| 接口 | 抽象方法 | 用途 |
|---|---|---|
Predicate<T> |
boolean test(T t) |
判断型,过滤等 |
Consumer<T> |
void accept(T t) |
消费型,无返回,如打印 |
Function<T,R> |
R apply(T t) |
转换型,输入 T 输出 R |
Supplier<T> |
T get() |
提供型,无输入有输出 |
// 使用 Predicate 判断字符串长度 > 3
Predicate<String> checkLength = s -> s.length() > 3;
checkLength.test("Java"); // true
// 使用 Consumer 打印元素
Consumer<String> printer = s -> System.out.println(s);
printer.accept("函数式编程");
// 使用 Function 将字符串转换为长度
Function<String, Integer> lengthFunc = String::length; // 方法引用,后面会讲
1.3 从匿名类到 Lambda 的演变
以前写一个线程:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("旧时代的线程");
}
}).start();
用 Lambda 简化:
new Thread(() -> System.out.println("Lambda 线程")).start();
只要接口是函数式接口,就能用 Lambda 替换。这让代码重心从“如何创建对象”转移到“要做什么”。
2. 方法引用:更进一步的简洁
当 Lambda 体仅仅调用一个已存在的方法时,可以进一步简化为方法引用。它是对 Lambda 表达式的语法糖,编译器会将其还原为 Lambda。
2.1 四种形式
| 类型 | 示例 | 等效 Lambda |
|---|---|---|
| 静态方法引用 | ClassName::method |
(x) -> ClassName.method(x) |
| 特定对象的实例方法引用 | instance::method |
(x) -> instance.method(x) |
| 特定类的任意对象实例方法引用 | ClassName::method |
(x,y) -> x.method(y) |
| 构造方法引用 | ClassName::new |
(x) -> new ClassName(x) |
// 静态方法引用:Integer::parseInt
Function<String, Integer> f1 = Integer::parseInt;
// 等效于
Function<String, Integer> f1Lambda = s -> Integer.parseInt(s);
// 特定对象实例方法引用:System.out::println
Consumer<String> printer = System.out::println;
// 类的任意对象实例方法引用:String::length
Function<String, Integer> lengthFunc = String::length;
// lambda: (s) -> s.length()
// 构造方法引用:ArrayList::new
Supplier<List<String>> supplier = ArrayList::new;
List<String> list = supplier.get(); // 得到一个空的 ArrayList
方法引用让代码读起来像是对方法的直接传递,但必须确保参数和返回值类型与函数式接口匹配。
3. Stream API:声明式集合处理
Stream 并不是数据结构,它是对数据源(集合、数组等)的抽象,可以让你以流水线方式对元素进行计算。Stream 操作分为中间操作(返回 Stream)和终端操作(返回结果或副作用)。
3.1 创建 Stream
// 从集合创建
List<String> names = Arrays.asList("Java", "Kotlin", "Scala");
Stream<String> stream1 = names.stream();
// 从数组创建
String[] arr = {"A","B"};
Stream<String> stream2 = Arrays.stream(arr);
// 使用 Stream.of
Stream<Integer> stream3 = Stream.of(1, 2, 3);
// 无限流(注意配合 limit)
Stream<Double> randomStream = Stream.generate(Math::random).limit(5);
Stream<Integer> iterateStream = Stream.iterate(0, n -> n + 2).limit(10);
3.2 中间操作(惰性求值)
常见的中间操作会返回新的 Stream,不会立即执行:
- filter:根据 Predicate 过滤
- map:根据 Function 转换元素
- flatMap:将每个元素转换为 Stream 并扁平化
- distinct:去重
- sorted:排序
- limit / skip:限制数量 / 跳过前 n 个
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> result = numbers.stream()
.filter(n -> n % 2 == 0) // 保留偶数
.map(n -> n * n) // 平方
.skip(1) // 跳过第一个(4*4=16)
.limit(2) // 只要2个
.collect(Collectors.toList());
// 结果:[36] (原始2,4,6 → 平方后4,16,36 → 跳过4 → 16,36 → limit取两个,但只剩两个,所以是[16,36] ?稍等:偶数有2,4,6,平方后4,16,36,跳过第一个4,得到16,36,limit(2)得到两个:16,36。上面注释错误,正确结果[16,36])
// 实际验证:偶数2,4,6 → 平方4,16,36 → skip(1)得到16,36 → limit(2)得到[16,36]。
3.3 终端操作(触发计算)
只有调用终端操作,前面的中间操作才会真正执行。常用终端操作:
- forEach:消费每个元素
- collect:收集到集合、字符串等
- reduce:将元素组合为单一值
- count:计数
- anyMatch / allMatch / noneMatch:匹配判断
- findFirst / findAny:返回 Optional 的结果
// collect 示例:将名字列表连接成字符串
String joined = names.stream()
.filter(name -> name.startsWith("J"))
.collect(Collectors.joining(", "));
// reduce 求和
int sum = numbers.stream()
.reduce(0, Integer::sum); // 等价于 .reduce(0, (a,b) -> a+b)
// 匹配检查
boolean anyEven = numbers.stream().anyMatch(n -> n % 2 == 0); // true
3.4 常用收集器(Collectors)
toList()/toSet():收集到列表或集合(Java 16+ 可直接Stream.toList())toMap():转换为 Map,需指定键值映射groupingBy():分组partitioningBy():按布尔条件分区
Map<Integer, List<String>> groupByLength = names.stream()
.collect(Collectors.groupingBy(String::length));
Map<Boolean, List<Integer>> evenOdds = numbers.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
4. 组合实战案例
假设有一个 Employee 列表,我们要找出工资大于 5000 的员工名字,按字母排序,并取出前两个。
List<Employee> employees = Arrays.asList(
new Employee("张伟", 6000),
new Employee("李芳", 4500),
new Employee("王磊", 7000),
new Employee("陈静", 5500)
);
List<String> topPaidNames = employees.stream()
.filter(e -> e.getSalary() > 5000) // Lambda
.map(Employee::getName) // 方法引用
.sorted() // 自然排序
.limit(2)
.collect(Collectors.toList());
// 结果:["张伟", "王磊"] 或按音序可能是 "王磊","张伟" (取决于汉字排序)
可以看到,Lambda、方法引用和 Stream 紧密协作,让业务逻辑一目了然。
5. 初学者的常见误区
- Lambda 并不是特殊对象:它必须依附于函数式接口,不能独立存在。
- Stream 不可重用:一个 Stream 只能被消费一次,操作后需重新创建。
- 中间操作的顺序影响性能:比如先
filter可以减少后续map的运算量。 - 避免修改外部变量:Lambda 中引用的局部变量必须是 effectively final。
结语
掌握 Lambda、方法引用和 Stream 是现代 Java 开发的基石。建议多在实际项目中用小任务替换传统循环和条件语句,逐步培养声明式思维。接下来你可以进一步学习 Optional 与函数式错误处理,以及并行流的性能调优。
祝你编程愉快!