背景

本文是《Java 后端从小白到大神》修仙系列第十三篇,正式进入Java后端世界,本篇文章主要聊Java基础中的Lambda表达式。若想详细学习请点击首篇博文,我们开始吧。

文章概览

  1. Lambda表达式概述
  2. Lambda表达式语法
  3. 函数式接口
  4. Lambda表达式的使用技巧
  5. Lambda表达式的实际应用
  6. Lambda表达式的原理
  7. Lambda表达式最佳实践
  8. 常见问题与解决方案

1. Lambda表达式概述

Lambda 表达式,就是 Java 8 用来简写代码的语法。专门用来快速实现只有一个抽象方法的接口,省去写类、写匿名内部类的麻烦,让代码更短、更清晰。一句话总结:Lambda = 简化版的匿名方法,只针对函数式接口。

1.1 核心特性

  • 简洁性:替代冗余的匿名内部类,使代码更简洁易读
  • 函数式编程:支持将函数作为参数传递,实现函数式编程风格
  • 类型推断:编译器自动推断参数类型,减少代码冗余
  • 闭包:可以访问外部的final或等效final的局部变量

1.2 Lambda表达式总结

  1. 函数式接口 = 只有1个抽象方法的接口
  2. Lambda = 快速实现这个方法
  3. Lambda 的格式必须和接口里的方法匹配
  4. Runnable / Consumer / Function 都是内置的函数式接口
  5. 它们代表不同的函数类型(有无参数、有无返回)
  6. 函数式接口类型格式:函数式接口<参数类型(1 个或多个),返回值类型>

Lambda 是实现,函数式接口是它的类型!

生活场景

函数式接口 = 插座,Lambda = 插头

  • Runnable 是两孔插座
  • Consumer 是三孔插座
  • Function 是带接地的插座

Lambda 必须形状对应,才能插进去!

2. Lambda表达式语法

2.1 基本语法

1
(参数列表) -> { 代码体 }
  • 参数列表:可以是空参数、单参数或多参数,类型可省略(编译器自动推断)
  • 箭头符号-> 分隔参数和代码体
  • 代码体:单行代码可省略 {}return,多行需明确写 {}return

2.2 语法变体

场景 Lambda 写法 说明
无参数,无返回值 () -> System.out.println("Hello") 适用于 Runnable 等接口
单参数,无类型 s -> System.out.println(s) 编译器自动推断参数类型
单参数,带类型 (String s) -> System.out.println(s) 显式指定参数类型
多参数,带类型 (int a, int b) -> a + b 多个参数需要使用括号
多行代码体 (a, b) -> { int c = a + b; return c; } 多行代码需要使用大括号和 return 语句
方法引用 System.out::println 当 Lambda 只是调用一个已有的方法时使用

2.3 语法示例

Java自带的函数式接口

Runnable / Consumer / Function 全部都是 函数式接口

  • Runnable:规定无参、无返回
  • Consumer:规定1个参、无返回
  • Function:规定1个参、1个返回

Runnable 接口长这样(内部)

1
2
3
4
@FunctionalInterface
public interface Runnable {
    void run();  // 只有 1 个抽象方法:无参、无返回
}

所以 Lambda 必须匹配它

1
() -> System.out.println("...")

完全对应:无参、无返回

Consumer 接口长这样

1
2
3
4
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);  // 1个参、无返回
}

Lambda 必须匹配

1
s -> System.out.println(s)

完全对应:1个参、无返回

Function 接口长这样

1
2
3
4
@FunctionalInterface
public interface Function<T,R> {
    R apply(T t);  // 1个参、1个返回
}

Lambda 匹配

1
x -> x * x

完全对应:1个参、1个返回

匿名内部类格式

Java 就把这一整块 → 当成一个「临时的、没有名字的类」

1
2
3
new 接口/父类() {
   只要这里写方法 = 这就是类
}

代码示例

无参无返回 → Runnable

1
Runnable runnable = () -> System.out.println("Hello");

1参无返回 → Consumer

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

1参有返回 → Function

1
Function<Integer, Integer> square = x -> x * x;

2参有返回 → BiFunction

1
2
3
4
// BiFunction<Integer, Integer, String>
//              ↑        ↑        ↑
//            参数1    参数2    返回值类型
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;

多行代码体

1
2
3
4
5
6
7
8
9
Function<Integer, String> converter = (num) -> {
    if (num < 0) {
        return "负数";
    } else if (num == 0) {
        return "零";
    } else {
        return "正数";
    }
};

完整代码(不用 Lambda)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class Test {
    public static void main(String[] args) {
        
        // 1. 正常写法:创建 Runnable 对象(匿名内部类)
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                // 这里是要执行的代码
                System.out.println("我是 run() 方法里的代码");
            }
        };

        // 2. 正常调用:直接调用 run() 方法
        runnable.run(); 
    }
}

换成 Lambda 后

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Test {
    public static void main(String[] args) {
        
        // Lambda 简化版
        Runnable runnable = () -> System.out.println("我是 run() 方法里的代码");

        // 调用方式 完全一样!
        runnable.run(); 
    }
}

3. 函数式接口

3.1 什么是函数式接口

函数式接口(Functional Interface)是 Java 中只包含一个抽象方法的接口。从 Java 8 开始,函数式接口可以使用 Lambda 表达式进行实例化,从而简化代码编写。

特点

  • 只有一个抽象方法(可以有多个默认方法和静态方法)
  • 可以使用 @FunctionalInterface 注解标记(推荐使用,确保编译器检查)

3.2 内置函数式接口

Java 8 提供了一些常用的内置函数式接口,位于 java.util.function 包中:

接口 方法签名 用途
Runnable void run() 无参数,无返回值的任务
Callable<V> V call() 无参数,带返回值的任务,支持抛出异常
Consumer<T> void accept(T) 接受单个输入参数,无返回值
Supplier<T> T get() 无参数,返回结果
Function<T, R> R apply(T) 接受单个输入参数,返回结果
Predicate<T> boolean test(T) 接受单个输入参数,返回布尔值,用于条件判断
BiFunction<T, U, R> R apply(T, U) 接受两个输入参数,返回结果
UnaryOperator<T> T apply(T) 接受一个参数,返回相同类型的结果
BinaryOperator<T> T apply(T, T) 接受两个相同类型的参数,返回相同类型的结果

3.3 自定义函数式接口

自定义一个函数式接口,只需创建一个接口并在其中声明一个抽象方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@FunctionalInterface
interface MathOperation {
    int operate(int a, int b);
    
    // 可以有默认方法
    default String getOperationName() {
        return "数学运算";
    }
    
    // 可以有静态方法
    static MathOperation add() {
        return (a, b) -> a + b;
    }
}

// 使用示例
public class CustomFunctionalInterfaceExample {
    public static void main(String[] args) {
        // 使用 Lambda 表达式实现自定义函数式接口
        MathOperation addition = (a, b) -> a + b;
        MathOperation subtraction = (a, b) -> a - b;
        
        System.out.println("加法: " + operate(10, 5, addition));
        System.out.println("减法: " + operate(10, 5, subtraction));
        System.out.println("操作名称: " + addition.getOperationName());
        
        // 使用静态方法创建实例
        MathOperation staticAdd = MathOperation.add();
        System.out.println("静态方法加法: " + operate(20, 10, staticAdd));
    }
    
    public static int operate(int a, int b, MathOperation operation) {
        return operation.operate(a, b);
    }
}

4. Lambda表达式的使用技巧

4.1 方法引用(Method Reference)

当 Lambda 体里,只做了一件事:调用一个已经存在的方法,别的啥也没干 → 就可以用方法引用 :: 简化。

看这个 Lambda:

1
s -> System.out.println(s)

它干了什么?
只干了一件事:调用了已经存在的 println 方法
没有计算、没有判断、没有新逻辑,就只是转发调用。

这种就叫:
Lambda 只是调用一个已有的方法

于是可以简化成方法引用

1
System.out::println

效果完全一样,但更短。

静态方法引用

1
2
3
4
5
// Lambda:只调用 Math.abs
a -> Math.abs(a)

// 方法引用
Math::abs

实例方法引用

1
2
3
4
5
6
7
String str = "hello";

// Lambda:只调用 str.length()
() -> str.length()

// 方法引用
str::length

构造方法引用

1
2
3
4
5
// Lambda:只 new 对象
() -> new String()

// 方法引用
String::new

什么时候不能用方法引用?

只要 Lambda 里多干了一点事,就不能用:

1
2
3
4
s -> {
    System.out.println(s);
    System.out.println("额外一句");  // 多干了 → 不能简化
}
1
a -> Math.abs(a) * 2;  // 计算了 → 不能简化

Lambda 里只做一件事:调用现成方法 → 就能写成 类名::方法对象::方法

4.2 Lambda 的变量捕获

什么是 Lambda 的变量捕获?Lambda 表达式 可以使用方法里定义的普通局部变量,就像把外面的变量“抓进来”用,这就叫 变量捕获

规则只有一句话
被 Lambda 抓到里面用的局部变量,必须是 不会变的(有效 final)。

也就是说:

  • 变量定义后就再也没改过值
  • 虽然你没写 final 关键字
  • 但编译器看它行为就是“不变的” → 这就叫 有效 final(effectively final)

举个最直观的例子

可以用(有效 final)

1
2
3
4
5
6
7
public void test() {
    int num = 10;  // 只赋值一次,后面没改过

    Runnable r = () -> {
        System.out.println(num);  // 可以捕获
    };
}

不可以用(不是有效 final)

1
2
3
4
5
6
7
8
public void test() {
    int num = 10;
    num = 20;  // 变量被修改了!

    Runnable r = () -> {
        System.out.println(num);  // 报错!
    };
}

为什么必须是“不变的”?

  1. Lambda 不是立刻执行,可能延后执行 它会把变量复制一份自己用。 如果原变量后来变了,两边不一致就会混乱。

  2. 避免多线程下出 bug 多个线程同时跑 Lambda,变量值一变,结果就不可预测。

  3. 编译器好处理 不变的东西,编译器能直接锁死值,不用时刻跟踪变化。

总结

  • 变量捕获 = Lambda 用了外面方法里的局部变量
  • 有效 final = 这个变量只赋值一次,后面再也不改
  • 规则:Lambda 只能抓不变的局部变量,改过就报错

在 Lambda 中使用可变变量的解决方案

  1. Lambda 不能改外部局部变量,但可以改对象里的字段
1
2
3
4
5
// 自定义 Counter 类
Counter counter = new Counter(0);
nums.forEach(n -> {
    counter.value++; // 允许!
});
  1. 使用数组
1
2
int[] count = {0};
nums.forEach(n -> count[0]++);
  1. 使用原子类
1
2
AtomicInteger count = new AtomicInteger(0);
nums.forEach(n -> count.incrementAndGet());

4.3 结合 Stream API

Lambda 表达式与 Stream API 配合使用,提供了强大的集合操作能力:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// 过滤偶数并计算平方和
int sumOfEvenSquares = numbers.stream()
    .filter(n -> n % 2 == 0)      // 过滤偶数
    .map(n -> n * n)              // 计算平方
    .mapToInt(Integer::intValue)  // 转换为 int 流
    .sum();                       // 求和

System.out.println("偶数平方和: " + sumOfEvenSquares); // 输出 220

// 分组统计
Map<String, List<Integer>> groupedByParity = numbers.stream()
    .collect(Collectors.groupingBy(n -> n % 2 == 0 ? "偶数" : "奇数"));

System.out.println("分组统计: " + groupedByParity);
// 输出: {奇数=[1, 3, 5, 7, 9], 偶数=[2, 4, 6, 8, 10]}

5. Lambda表达式的实际应用

5.1 替代匿名内部类

线程创建

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 传统写法
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("线程运行中");
    }
}).start();

// Lambda 写法
new Thread(() -> System.out.println("线程运行中")).start();

集合排序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
List<String> names = Arrays.asList("Bob", "Alice", "Tom", "David");

// 传统写法
Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.compareTo(s2);
    }
});

// Lambda 写法
names.sort((s1, s2) -> s1.compareTo(s2));

// 方法引用写法
names.sort(String::compareTo);

5.2 事件驱动编程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// GUI 事件监听(示例)
button.addActionListener(e -> System.out.println("按钮被点击!"));

// 鼠标事件监听(示例)
panel.addMouseListener(new MouseAdapter() {
    @Override
    public void mouseClicked(MouseEvent e) {
        System.out.println("鼠标点击: " + e.getPoint());
    }
});

// 注意:MouseAdapter 不是函数式接口,因为它有多个方法
// 但可以使用 Lambda 表达式配合函数式接口使用

5.3 并行处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class StreamTest {
    public static void main(String[] args) {

        // 生成 1 ~ 1000000 的数字列表
        List<Integer> numbers = IntStream.rangeClosed(1, 1000000)
                                         .boxed()
                                         .collect(Collectors.toList());

        // ===================== 串行流:一个人按顺序干活 =====================
        // 记录开始时间
        long startTime = System.currentTimeMillis();

        // 串行计算:单线程逐个处理,筛选偶数并求和
        int sumSerial = numbers.stream()              // 串行流:单线程处理
                                .filter(n -> n % 2 == 0)  // 只保留偶数
                                .mapToInt(Integer::intValue)
                                .sum();                 // 求和

        // 串行耗时
        long serialTime = System.currentTimeMillis() - startTime;

        // ===================== 并行流:多个人同时一起干活 =====================
        // 重新记录开始时间
        startTime = System.currentTimeMillis();

        // 并行计算:多线程同时处理,速度更快
        int sumParallel = numbers.parallelStream()    // 并行流:多线程同时处理
                                   .filter(n -> n % 2 == 0) // 只保留偶数
                                   .mapToInt(Integer::intValue)
                                   .sum();                // 求和

        // 并行耗时
        long parallelTime = System.currentTimeMillis() - startTime;

        // 输出结果:结果一定一样,时间并行通常更短
        System.out.println("串行处理结果: " + sumSerial + ", 时间: " + serialTime + "ms");
        System.out.println("并行处理结果: " + sumParallel + ", 时间: " + parallelTime + "ms");
    }
}

5.4 函数式接口参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Calculator {
    public static void main(String[] args) {
        // 使用函数式接口作为参数
        int result1 = calculate(10, 5, (a, b) -> a + b);
        int result2 = calculate(10, 5, (a, b) -> a - b);
        int result3 = calculate(10, 5, (a, b) -> a * b);
        int result4 = calculate(10, 5, (a, b) -> a / b);
        
        System.out.println("加法: " + result1);
        System.out.println("减法: " + result2);
        System.out.println("乘法: " + result3);
        System.out.println("除法: " + result4);
    }
    
    public static int calculate(int a, int b, BiFunction<Integer, Integer, Integer> operation) {
        return operation.apply(a, b);
    }
}

6. Lambda表达式的原理

6.1 工作机制

Lambda 表达式在运行时会被编译为以下形式之一:

  1. 静态方法:如果 Lambda 表达式没有捕获外部变量
  2. 实例方法:如果 Lambda 表达式捕获了外部变量
  3. 匿名内部类:在某些复杂情况下

6.2 类型推断

编译器通过上下文推断 Lambda 表达式的类型:

  • 从函数式接口的抽象方法签名推断参数类型
  • 从代码体推断返回类型

6.3 闭包实现

什么是闭包? Lambda 能“记住并使用”它外面的变量,这个能力就叫闭包。 就这么简单。

用最直白的例子讲闭包

1
2
3
4
5
6
7
public void test() {
    int age = 20;       // 外部变量

    Runnable r = () -> {
        System.out.println(age);  // Lambda 用到了外面的 age
    };
}

这里:

  • Lambda 用到了外部变量 age
  • Lambda 把 age “抓进来、记住它”
  • 这个行为 → 闭包

7. Lambda表达式最佳实践

7.1 代码风格

  • 保持简洁:Lambda 表达式应该简短明了,避免复杂逻辑
  • 使用方法引用:当 Lambda 只是调用一个已有的方法时,使用方法引用
  • 合理命名参数:使用有意义的参数名,提高代码可读性
  • 避免过长的 Lambda:如果 Lambda 表达式超过 3-4 行,考虑将其提取为方法

7.2 性能考虑

  • 避免在循环中创建 Lambda:重复创建 Lambda 表达式会影响性能
  • 合理使用并行流:对于大型集合,并行流可以提高性能,但对于小型集合可能会有 overhead
  • 注意自动装箱:在处理基本类型时,注意避免频繁的自动装箱和拆箱

7.3 常见模式

1. 策略模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 传统策略模式
interface PaymentStrategy {
    void pay(int amount);
}

class CreditCardPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("信用卡支付: " + amount);
    }
}

// 使用 Lambda 表达式
PaymentStrategy creditCardPayment = amount -> System.out.println("信用卡支付: " + amount);
PaymentStrategy wechatPayment = amount -> System.out.println("微信支付: " + amount);

// 使用
creditCardPayment.pay(100);
wechatPayment.pay(200);

2. 模板方法模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 模板方法
public void processTemplate(List<Integer> numbers, Function<Integer, Integer> processor) {
    for (Integer number : numbers) {
        System.out.println(processor.apply(number));
    }
}

// 使用
processTemplate(Arrays.asList(1, 2, 3), n -> n * n); // 平方
processTemplate(Arrays.asList(1, 2, 3), n -> n * 2);  // 翻倍

8. 常见问题与解决方案

8.1 变量捕获问题

问题:Lambda 表达式中不能修改外部变量

解决方案:使用类、原子类或数组来存储可变值

8.2 类型推断问题

问题:编译器无法推断 Lambda 表达式的类型

解决方案:显式指定参数类型或使用类型转换

1
2
3
4
5
6
7
8
// 类型推断失败
Function<Object, String> converter = obj -> obj.toString(); // 编译错误

// 解决方案 1:显式指定参数类型
Function<Object, String> converter1 = (Object obj) -> obj.toString();

// 解决方案 2:使用类型转换
Function<Object, String> converter2 = obj -> ((String) obj).toUpperCase();

8.3 异常处理问题

问题:Lambda 表达式中抛出的检查型异常需要处理

解决方案:在 Lambda 表达式内部处理异常或使用包装方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

public class LambdaExceptionDemo {

    public static void main(String[] args) {
        List<String> files = Arrays.asList("file1.txt", "file2.txt");

        // ==============================================
        // 【错误写法】
        // Lambda 里直接抛检查型异常(IOException),编译报错!
        // 因为 forEach 接收的 Consumer 接口没声明 throws Exception
        // ==============================================
        /*
        files.forEach(file -> {
            // 编译报错:未处理的异常 java.io.IOException
            Files.readAllLines(Paths.get(file));
        });
        */

        // ==============================================
        // 【方案1:最常用】在 Lambda 内部 try-catch 处理
        // ==============================================
        files.forEach(file -> {
            try {
                Files.readAllLines(Paths.get(file));
            } catch (IOException e) {
                System.err.println("读取文件失败:" + file);
                e.printStackTrace();
            }
        });

        // ==============================================
        // 【方案2:优雅写法】包装成运行时异常
        // 定义一个允许抛异常的函数式接口,再统一包装
        // ==============================================
        files.forEach(fileWrapper(file -> {
            // 这里可以 throws IOException,外部无感
            Files.readAllLines(Paths.get(file));
        }));
    }

    // ==============================================
    // 包装工具方法:把“会抛受检异常”的Lambda,转成普通Consumer
    // ==============================================
    public static Consumer<String> fileWrapper(ThrowingConsumer<String> throwingConsumer) {
        return file -> {
            try {
                throwingConsumer.accept(file);
            } catch (Exception e) {
                // 捕获受检异常,包装成运行时异常抛出
                throw new RuntimeException("文件处理异常", e);
            }
        };
    }

    // ==============================================
    // 自定义函数式接口:允许抛出 Exception
    // ==============================================
    @FunctionalInterface
    public interface ThrowingConsumer<T> {
        void accept(T t) throws Exception;
    }
}

总结

Lambda 表达式是 Java 8 引入的重要特性,它不仅简化了代码,还为 Java 带来了函数式编程的能力。应该掌握以下内容:

  1. Lambda 表达式的语法:基本语法和各种变体
  2. 函数式接口:内置函数式接口和自定义函数式接口
  3. 使用技巧:方法引用、变量捕获、结合 Stream API
  4. 实际应用:替代匿名内部类、事件驱动编程、并行处理
  5. 原理:工作机制、类型推断、闭包实现
  6. 最佳实践:代码风格、性能考虑、常见模式
  7. 问题解决:变量捕获、类型推断、异常处理

Lambda 表达式特别适合集合操作、异步任务和事件处理等场景,熟练掌握 Lambda 表达式可以使你的代码更加简洁、优雅和高效。在实际开发中,应该根据具体场景合理使用 Lambda 表达式,充分发挥其优势。