Java 函数式编程:Lambda、Stream 与方法引用

FreeGuideOnline 最新 2026-06-17

引言:为什么现代 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 与函数式错误处理,以及并行流的性能调优。

祝你编程愉快!