引言:为什么我们需要Lambda表达式?

想象一下,你是一个餐厅经理,每次需要安排员工做特定任务时,都需要写一份详细的“员工职位说明书”。比如让小王去洗盘子,你需要写:

员工 小王 = new 员工() {
    @Override
    public void 执行任务() {
        洗盘子();
    }
};
小王.执行任务();

这太麻烦了!你真正想说的只是:“小王,去洗盘子”。而Lambda表达式让你可以这样做:



员工 小王 = () -> 洗盘子();
小王.执行任务();
或者更简单:

java

安排任务(() -> 洗盘子());

这就是Lambda表达式的魅力——让代码只说重点,去掉繁琐的仪式感。现在,让我们一起探索这个让Java代码变得更简洁、更优雅的神奇特性。

第一章:Lambda表达式初体验

1.1 Lambda表达式的基本形态

Lambda表达式就像一个“迷你函数”,它不需要名字,直接定义行为。我们来看几个最简单的例子:

// 场景1:没有参数,没有返回值的任务
Runnable 简单的问候 = () -> System.out.println("你好,世界!");

// 场景2:有参数,没有返回值的操作
Consumer<String> 打印消息 = (消息) -> System.out.println("收到:" + 消息);

// 场景3:有参数,也有返回值的计算
Function<Integer, Integer> 计算平方 = (数字) -> 数字 * 数字;

1.2 Lambda表达式的语法糖

Lambda表达式有很多简化写法,就像给你提供了一套“快捷键”:

// 完整写法:就像穿正装
(参数类型 参数名) -> { 语句; return 值; }

// 简化写法1:当只有一条语句时,脱掉外套
(参数) -> 语句

// 简化写法2:当只有一个参数时,连括号都省了
参数 -> 语句

// 简化写法3:编译器能猜出类型时,类型也省了
(参数1, 参数2) -> 表达式

实际应用中的例子:

// 示例1:打印所有员工姓名
员工列表.forEach(员工 -> System.out.println(员工.姓名));

// 示例2:筛选出成年的员工
List<员工> 成年员工 = 员工列表.stream()
    .filter(员工 -> 员工.年龄 >= 18)
    .collect(Collectors.toList());

// 示例3:计算员工平均工资
double 平均工资 = 员工列表.stream()
    .mapToDouble(员工 -> 员工.工资)
    .average()
    .orElse(0);

第二章:理解函数式接口——Lambda的“插座”

2.1 什么是函数式接口?

如果把Lambda表达式比作“电器”,那么函数式接口就是“插座”。这个插座有个特点:它只有一个插孔(抽象方法)

// 这是一个函数式接口——只有一个抽象方法
@FunctionalInterface
interface 任务 {
    void 执行();  // 唯一的抽象方法
    
    // 可以有默认方法(这是Java 8的新特性)
    default void 介绍() {
        System.out.println("我是一个任务");
    }
}

// 使用Lambda表达式“插入”到这个接口
任务 扫地任务 = () -> System.out.println("正在扫地...");
扫地任务.执行();  // 输出:正在扫地...

2.2 Java自带的“标准插座”

Java已经为我们准备了一些常用的函数式接口,就像家里已经装好了各种标准插座:

2.2.1 Supplier<T>:生产者(只产出,不消费)
// 想象成一个自动售货机,按一下按钮就出货
Supplier<String> 自动问候机 = () -> "你好!";
String 问候语 = 自动问候机.get();  // 得到"你好!"

// 实际用途:延迟计算
Supplier<Double> 随机数生成器 = () -> Math.random();
System.out.println("随机数:" + 随机数生成器.get());
2.2.2 Consumer<T>:消费者(只消费,不产出)
// 想象成一个垃圾桶,你给它东西,它处理掉
Consumer<String> 消息处理器 = (消息) -> System.out.println("处理消息:" + 消息);
消息处理器.accept("重要通知");  // 输出:处理消息:重要通知

// 实际用途:批量处理
List<String> 消息列表 = Arrays.asList("消息1", "消息2", "消息3");
消息列表.forEach(消息 -> System.out.println("正在处理:" + 消息));
2.2.3 Function<T, R>:转换器(输入一个,输出另一个)
// 想象成一个魔法转换箱,放进去苹果,变成苹果汁
Function<String, Integer> 长度计算器 = (字符串) -> 字符串.length();
int 长度 = 长度计算器.apply("Hello");  // 得到5

// 实际用途:数据转换
List<String> 名字列表 = Arrays.asList("张三", "李四", "王五");
List<Integer> 名字长度列表 = 名字列表.stream()
    .map(名字 -> 名字.length())  // 将每个名字转换成长度
    .collect(Collectors.toList());
2.2.4 Predicate<T>:判断器(返回true或false)
// 想象成一个安检门,通过就亮绿灯,不通过亮红灯
Predicate<Integer> 成年检查器 = (年龄) -> 年龄 >= 18;
boolean 是否成年 = 成年检查器.test(20);  // 得到true

// Predicate的强大之处:可以组合
Predicate<Integer> 是正数 = (数字) -> 数字 > 0;
Predicate<Integer> 是偶数 = (数字) -> 数字 % 2 == 0;

// 组合1:既是正数又是偶数
Predicate<Integer> 正偶数 = 是正数.and(是偶数);

// 组合2:是正数或是偶数
Predicate<Integer> 正数或偶数 = 是正数.or(是偶数);

// 组合3:不是正数
Predicate<Integer> 非正数 = 是正数.negate();
2.2.5 Comparator<T>:比较器(比较两个对象)
// 想象成天平,比较两个东西哪个更重
List<String> 水果列表 = Arrays.asList("香蕉", "苹果", "橙子");

// 按字母顺序排序(传统写法)
水果列表.sort(new Comparator<String>() {
    @Override
    public int compare(String 水果1, String 水果2) {
        return 水果1.compareTo(水果2);
    }
});

// 使用Lambda表达式(简洁多了!)
水果列表.sort((水果1, 水果2) -> 水果1.compareTo(水果2));

第三章:方法引用——Lambda的"快捷方式"

3.1 为什么需要方法引用?

有时候,Lambda表达式只是简单调用一个已有的方法:

// Lambda写法
员工列表.forEach(员工 -> System.out.println(员工));

// 实际上就是调用System.out.println
// 这时候可以用方法引用这个"快捷方式"
员工列表.forEach(System.out::println);

方法引用就像给你的常用操作设置了一个"快捷键",让代码更加简洁。

3.2 方法引用的四种形式

3.2.1 静态方法引用:类名::静态方法名
// 场景:数学计算
// Lambda写法
Function<Double, Double> 计算平方根 = (数字) -> Math.sqrt(数字);

// 方法引用写法(更简洁!)
Function<Double, Double> 计算平方根 = Math::sqrt;

// 使用
double 结果 = 计算平方根.apply(16.0);  // 得到4.0
3.2.2 实例方法引用:对象::实例方法名
// 场景:字符串处理
String 前缀 = "Hello, ";

// Lambda写法
Function<String, String> 添加前缀 = (名字) -> 前缀.concat(名字);

// 方法引用写法
Function<String, String> 添加前缀 = 前缀::concat;

// 使用
String 完整问候 = 添加前缀.apply("World");  // 得到"Hello, World"
3.2.3 类的任意对象的实例方法引用:类名::实例方法名
// 场景:排序字符串数组
String[] 名字数组 = {"张三", "李四", "王五"};

// Lambda写法
Arrays.sort(名字数组, (名字1, 名字2) -> 名字1.compareTo(名字2));

// 方法引用写法
Arrays.sort(名字数组, String::compareTo);

这种形式比较特殊,它适用于Lambda表达式的第一个参数是方法的调用者,第二个参数是方法的参数的情况。

3.2.4 构造器引用:类名::new
// 场景:创建对象
// Lambda写法
Supplier<List<String>> 创建列表 = () -> new ArrayList<>();

// 构造器引用写法
Supplier<List<String>> 创建列表 = ArrayList::new;

// 使用
List<String> 我的列表 = 创建列表.get();

第四章:实际应用场景

4.1 场景一:简化事件处理

// 传统写法(Java 8之前)
按钮.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("按钮被点击了");
    }
});

// Lambda写法(简洁多了!)
按钮.addActionListener(事件 -> System.out.println("按钮被点击了"));

// 如果只是调用一个方法,还可以使用方法引用
按钮.addActionListener(System.out::println);

4.2 场景二:简化线程创建

// 传统写法
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("线程运行中");
    }
}).start();

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

4.3 场景三:集合处理(配合Stream API)

// 假设有一个员工列表
List<Employee> 员工列表 = ...;

// 需求1:找出所有工资超过5000的员工
List<Employee> 高薪员工 = 员工列表.stream()
    .filter(员工 -> 员工.getSalary() > 5000)
    .collect(Collectors.toList());

// 需求2:提取所有员工的名字
List<String> 员工名字 = 员工列表.stream()
    .map(员工 -> 员工.getName())  // 或者 Employee::getName
    .collect(Collectors.toList());

// 需求3:按工资排序
List<Employee> 按工资排序 = 员工列表.stream()
    .sorted((员工1, 员工2) -> 员工1.getSalary() - 员工2.getSalary())
    .collect(Collectors.toList());

4.4 场景四:策略模式简化

// 定义策略接口
interface 折扣策略 {
    double 计算折扣(double 原价);
}

// 不同的折扣策略
折扣策略 普通会员折扣 = 原价 -> 原价 * 0.95;      // 95折
折扣策略 黄金会员折扣 = 原价 -> 原价 * 0.85;      // 85折
折扣策略 钻石会员折扣 = 原价 -> 原价 * 0.7;       // 7折

// 使用策略
class 收银台 {
    public double 结账(double 原价, 折扣策略 策略) {
        return 策略.计算折扣(原价);
    }
}

// 调用
收银台 收银台 = new 收银台();
double 应付金额 = 收银台.结账(1000, 黄金会员折扣);  // 得到850

第五章:常见问题与最佳实践

5.1 什么时候该用Lambda,什么时候不该用?

该用Lambda的情况:

  1. 需要传递简单行为时

  2. 实现只有一个方法的接口时

  3. 配合Stream API处理集合时

  4. 简化事件监听器、线程创建等

不该用Lambda的情况:

  1. 行为很复杂,需要多行代码时

  2. 需要重用该行为时(考虑使用方法引用或单独的方法)

  3. 行为需要多个地方调用,且有明确的业务含义时

5.2 性能考虑

一般来说,Lambda表达式的性能与传统写法相当。但要注意:

  1. 避免在热循环中创建Lambda:每次循环都创建新的Lambda对象会有开销

  2. 使用方法引用:通常比等效的Lambda表达式性能稍好

  3. 注意自动装箱:使用原始类型特化的函数式接口(如IntConsumer)避免装箱

总结

Lambda表达式和方法引用是Java现代化的重要一步,它们让Java代码:

  1. 更简洁:减少了大量样板代码

  2. 更易读:代码更接近自然语言描述

  3. 更灵活:可以轻松传递行为作为参数

  4. 更强大:与Stream API结合,处理数据更加方便

学习Lambda表达式就像学习一门新语言,开始时可能会觉得有点陌生,但一旦掌握,你就会发现它让你的编程体验更加愉快。记住:

  • 从简单的例子开始练习

  • 理解函数式接口是Lambda的基础

  • 方法引用是Lambda的"语法糖",让代码更简洁

  • 多在实际项目中实践,才能真正掌握

编程就像烹饪,Lambda表达式就是你的新厨具——开始时可能需要适应,但用熟之后,你会发现它能帮你做出更美味的"代码大餐"!

祝你在Lambda的世界里编程愉快!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐