乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

Lambda 表达式

本章定位:Lambda 表达式是 JDK 8 所有新特性的语法基石。理解 Lambda 不仅是学会 (参数) -> { 方法体 } 的写法,更要理解它背后的函数式接口机制、invokedynamic 指令的实现原理,以及与匿名内部类的本质区别。


1. 场景引入:小崔的烦恼

飞翔科技技术部的小崔正对着屏幕发愁。他正在开发"员工管理分析系统"的筛选功能,需要从员工列表中筛选出薪资高于 15000 的所有员工。

用 JDK 7 的传统写法,他写出了这样的代码:

// 传统写法:匿名内部类
List<Employee> highSalaryEmployees = new ArrayList<>();
for (Employee emp : allEmployees) {
    if (emp.getSalary() > 15000) {
        highSalaryEmployees.add(emp);
    }
}

过了一会儿,产品经理又提了新需求:还要能按部门筛选、按入职日期筛选、按绩效等级筛选……

小崔意识到,如果每个筛选条件都写一套 for 循环,代码会变得极其冗余。这时,架构师白歌走过来说:"试试 Lambda 表达式,把'筛选条件'当作参数传给方法。"

// JDK 8 写法:Lambda 表达式
List<Employee> highSalaryEmployees = allEmployees.stream()
    .filter(emp -> emp.getSalary() > 15000)
    .collect(Collectors.toList());

关键转变:从"如何遍历并筛选"(命令式)变成了"筛选什么"(声明式)。


2. Lambda 表达式语法

2.1 语法定义

Lambda 表达式由三部分组成:

(参数列表) -> { 方法体 }
组成部分说明示例
参数列表可以省略类型(类型推断),单参数可省略括号x、(a, b)、(String s)
箭头符号->,固定语法->
方法体单行可省略 {} 和 return,多行必须用 {}x * 2、{ return x * 2; }

2.2 六种合法写法

// 写法1:无参数,无返回值
Runnable r1 = () -> System.out.println("Hello Lambda");

// 写法2:单参数,省略类型和括号
Consumer<String> c1 = s -> System.out.println(s);

// 写法3:单参数,保留类型和括号
Consumer<String> c2 = (String s) -> System.out.println(s);

// 写法4:多参数,有返回值,单行省略 return
Comparator<Integer> comp = (a, b) -> a - b;

// 写法5:多参数,多行方法体
BinaryOperator<Integer> add = (a, b) -> {
    int result = a + b;
    System.out.println("计算结果: " + result);
    return result;
};

// 写法6:参数为空的 Supplier
Supplier<Employee> empSupplier = () -> new Employee("默认员工", 0, "未知部门");

2.3 语法速查表

场景Lambda 写法等效匿名内部类行数
单参数消费x -> doSomething(x)4 行
无参数返回() -> new Object()5 行
多参数计算(a, b) -> a + b5 行
多行逻辑(a, b) -> { ... }6+ 行
方法引用System.out::println3 行

3. 函数式接口

3.1 概念定义

函数式接口(Functional Interface) 是指有且仅有一个抽象方法的接口。Lambda 表达式的类型就是函数式接口——Lambda 本身没有独立的类型,它的类型由目标类型(Target Type)推断。

@FunctionalInterface  // 编译期校验:确保接口只有一个抽象方法
public interface Predicate<T> {
    boolean test(T t);  // 唯一的抽象方法
    
    // 默认方法不算抽象方法
    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }
    
    // 静态方法也不算
    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
            ? Objects::isNull
            : object -> targetRef.equals(object);
    }
    
    // Object 类的方法也不算(equals, hashCode, toString 等)
    boolean equals(Object obj);
}

关键理解:@FunctionalInterface 注解不是必需的(即使不加,只要接口只有一个抽象方法就是函数式接口),但强烈建议加上——它让编译器帮你把关,防止后续维护者意外添加第二个抽象方法。

3.2 函数式接口的判断规则

方法类型计入抽象方法计数?说明
接口声明的抽象方法计入有且仅有 1 个
default 方法不计入有默认实现
static 方法不计入接口静态方法
重写 Object 类的方法不计入如 equals、hashCode、toString

3.3 Java 内置函数式接口

JDK 8 在 java.util.function 包中提供了 43 个函数式接口。以下是日常开发中最核心的 6 个:

接口抽象方法说明使用场景
Predicate<T>boolean test(T t)断言/判断筛选条件、校验规则
Consumer<T>void accept(T t)消费者遍历、打印、写入
Function<T,R>R apply(T t)转换函数类型转换、数据提取
Supplier<T>T get()供应者工厂方法、懒加载
BiFunction<T,U,R>R apply(T t, U u)双参数函数合并、聚合计算
UnaryOperator<T>T apply(T t)一元运算(Function 子接口)单类型转换
BinaryOperator<T>T apply(T t1, T t2)二元运算(BiFunction 子接口)累加、比较

3.3.1 Predicate<T>:断言型

// 场景:筛选薪资 > 15000 的员工
Predicate<Employee> highSalary = emp -> emp.getSalary() > 15000;

// 组合使用:薪资高 AND 技术部门
Predicate<Employee> isTech = emp -> "技术部".equals(emp.getDepartment());
Predicate<Employee> highSalaryInTech = highSalary.and(isTech);

boolean result = highSalaryInTech.test(new Employee("小崔", 18000, "技术部"));
// result = true

Predicate 的默认方法组合:

方法逻辑示例
and(Predicate)逻辑与p1.and(p2)
or(Predicate)逻辑或p1.or(p2)
negate()逻辑非p1.negate()
isEqual(Object)静态方法,判断相等Predicate.isEqual(target)

3.3.2 Consumer<T>:消费型

// 场景:打印员工信息
Consumer<Employee> printEmployee = emp -> 
    System.out.printf("姓名: %s, 薪资: %.2f, 部门: %s%n", 
        emp.getName(), emp.getSalary(), emp.getDepartment());

// 链式消费:先打印,再记录日志
Consumer<Employee> logEmployee = emp -> 
    Logger.log("处理员工: " + emp.getName());
Consumer<Employee> printAndLog = printEmployee.andThen(logEmployee);

3.3.3 Function<T,R>:函数型

// 场景:从员工对象中提取姓名
Function<Employee, String> extractName = Employee::getName;

// 场景:计算员工的年终奖(薪资 * 绩效系数)
Function<Employee, Double> calcBonus = emp -> emp.getSalary() * emp.getPerformanceRate();

// 链式组合:先提取姓名,再转大写
Function<Employee, String> upperName = extractName.andThen(String::toUpperCase);

3.3.4 Supplier<T>:供给型

// 场景:懒加载创建默认员工
Supplier<Employee> defaultEmployee = () -> new Employee("待分配", 0, "未知部门");

// 场景:生成唯一 ID
Supplier<String> idGenerator = () -> "EMP-" + UUID.randomUUID().toString().substring(0, 8);

3.4 函数式接口继承关系


4. 深度原理:Lambda 不是内部类的语法糖

这是本章最重要的知识点,也是面试中最容易被问倒的问题。

4.1 常见误解

很多初学者(包括小崔)认为 Lambda 表达式就是匿名内部类的"语法糖"——编译器把 Lambda 翻译成一个匿名内部类,然后生成 .class 文件。

这个理解是错误的。

4.2 证据:字节码层面

让我们用实验说话。创建以下两个类:

// 匿名内部类版本
public class AnonymousInnerDemo {
    public static void main(String[] args) {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello from Anonymous Inner Class");
            }
        };
        r.run();
    }
}
// Lambda 版本
public class LambdaDemo {
    public static void main(String[] args) {
        Runnable r = () -> System.out.println("Hello from Lambda");
        r.run();
    }
}

编译后查看字节码:

# 匿名内部类编译产物
AnonymousInnerDemo.class          ← 主类
AnonymousInnerDemo$1.class        ← 匿名内部类(独立的 .class 文件!)

# Lambda 编译产物
LambdaDemo.class                  ← 主类(没有额外的 .class 文件!)

关键发现:Lambda 表达式不会生成独立的 .class 文件,而匿名内部类会。这说明编译器的处理方式完全不同。

4.3 invokedynamic 指令

编译器将 Lambda 表达式翻译成三条指令:

  1. invokedynamic:动态调用指令,JVM 首次执行时调用 LambdaMetafactory.metafactory() 方法
  2. 一个私有的静态方法(或实例方法):包含 Lambda 方法体的实际代码
  3. 方法句柄(MethodHandle):指向该静态方法的引用

invokedynamic 的完整流程:

  1. 编译期:javac 将 Lambda 体提取为私有静态方法,生成 invokedynamic 指令但不决定具体实现方式
  2. 链接期(首次执行 invokedynamic):JVM 调用 LambdaMetafactory.metafactory() 引导方法
  3. 运行时:LambdaMetafactory 通过 Unsafe.defineAnonymousClass 或 MethodHandle 动态生成一个实现了函数式接口的类
  4. 缓存:生成的实例被缓存,后续调用直接复用

4.4 Lambda 与匿名内部类的本质区别

对比维度Lambda 表达式匿名内部类
编译产物无独立 .class 文件生成 Outer$N.class
实现机制invokedynamic + MethodHandle编译期生成内部类
this 引用指向外层类实例指向匿名内部类自身
作用域不引入新的作用域引入新的嵌套作用域
变量捕获只能捕获 effectively final 变量只能捕获 effectively final 变量(JDK 8+)
性能首次调用略慢(生成),后续更快每次创建都需要加载新的类
占用内存不产生 Class 元数据每个类都占用 PermGen/Metaspace

4.5 this 引用的陷阱

这是 Lambda 和匿名内部类最容易混淆的地方:

public class ThisReferenceDemo {
    private String name = "外部类";

    public void testAnonymousInner() {
        // 匿名内部类中的 this 指向内部类自身
        Runnable r = new Runnable() {
            private String name = "匿名内部类";
            @Override
            public void run() {
                System.out.println(this.name);  // 输出: "匿名内部类"
            }
        };
        r.run();
    }

    public void testLambda() {
        // Lambda 中的 this 指向外层类
        Runnable r = () -> {
            System.out.println(this.name);  // 输出: "外部类"
            // this 在这里就是 ThisReferenceDemo 的实例
        };
        r.run();
    }
}

5. 完整代码示例

示例一:员工筛选系统

场景:小崔需要实现飞翔科技的员工筛选功能,支持按薪资、部门、入职日期等多个条件灵活组合。

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

/**
 * 员工实体类
 */
class Employee {
    private String name;
    private double salary;
    private String department;
    private LocalDate hireDate;

    public Employee(String name, double salary, String department, LocalDate hireDate) {
        this.name = name;
        this.salary = salary;
        this.department = department;
        this.hireDate = hireDate;
    }

    public String getName() { return name; }
    public double getSalary() { return salary; }
    public String getDepartment() { return department; }
    public LocalDate getHireDate() { return hireDate; }

    @Override
    public String toString() {
        return String.format("Employee{name='%s', salary=%.0f, dept='%s', hired=%s}",
                name, salary, department, hireDate);
    }
}

/**
 * 员工筛选器 —— 使用内置函数式接口实现行为参数化
 */
class EmployeeFilter {
    /**
     * 通用筛选方法:将筛选条件作为参数传入
     */
    public static List<Employee> filter(List<Employee> employees, 
                                         Predicate<Employee> condition) {
        List<Employee> result = new ArrayList<>();
        for (Employee emp : employees) {
            if (condition.test(emp)) {
                result.add(emp);
            }
        }
        return result;
    }

    /**
     * 通用处理:对筛选结果执行操作
     */
    public static void process(List<Employee> employees,
                                Predicate<Employee> filter,
                                Consumer<Employee> action) {
        for (Employee emp : employees) {
            if (filter.test(emp)) {
                action.accept(emp);
            }
        }
    }
}

public class LambdaEmployeeDemo {
    public static void main(String[] args) {
        // 准备数据
        List<Employee> employees = new ArrayList<>();
        employees.add(new Employee("大翔", 50000, "管理层", LocalDate.of(2015, 3, 1)));
        employees.add(new Employee("白歌", 35000, "技术部", LocalDate.of(2017, 6, 15)));
        employees.add(new Employee("小崔", 18000, "技术部", LocalDate.of(2021, 7, 1)));
        employees.add(new Employee("孔蓝", 12000, "技术部", LocalDate.of(2023, 9, 1)));
        employees.add(new Employee("老刘", 9000, "后勤部", LocalDate.of(2018, 1, 10)));

        // === 场景1:筛选薪资 > 15000 的员工 ===
        Predicate<Employee> highSalary = emp -> emp.getSalary() > 15000;
        List<Employee> highSalaryEmps = EmployeeFilter.filter(employees, highSalary);
        System.out.println("=== 薪资 > 15000 的员工 ===");
        highSalaryEmps.forEach(System.out::println);

        // === 场景2:组合条件:技术部 AND 入职超过2年 ===
        Predicate<Employee> inTech = emp -> "技术部".equals(emp.getDepartment());
        Predicate<Employee> hiredBefore2Years = emp -> 
            emp.getHireDate().isBefore(LocalDate.now().minusYears(2));
        Predicate<Employee> techVeteran = inTech.and(hiredBefore2Years);

        System.out.println("\n=== 技术部老员工(入职 > 2年) ===");
        List<Employee> techVeterans = EmployeeFilter.filter(employees, techVeteran);
        techVeterans.forEach(System.out::println);

        // === 场景3:使用 Consumer 链式处理 ===
        System.out.println("\n=== 打印 + 计算年终奖 ===");
        Consumer<Employee> printer = emp -> System.out.printf("姓名: %s, 薪资: %.0f%n", 
            emp.getName(), emp.getSalary());
        Consumer<Employee> bonusCalc = emp -> {
            double bonus = emp.getSalary() * 12 * 0.15;  // 15% 年终奖
            System.out.printf("  → 预估年终奖: %.0f%n", bonus);
        };

        EmployeeFilter.process(employees, 
            emp -> emp.getSalary() > 10000, 
            printer.andThen(bonusCalc));

        // === 场景4:Function 提取 + Supplier 默认值 ===
        Function<Employee, String> nameExtractor = Employee::getName;
        Supplier<Employee> defaultEmp = () -> new Employee("默认员工", 0, "无", LocalDate.now());

        System.out.println("\n=== 员工姓名列表 ===");
        employees.stream()
            .map(nameExtractor)
            .forEach(System.out::println);
    }
}
运行输出:
=== 薪资 > 15000 的员工 ===
Employee{name='大翔', salary=50000, dept='管理层', hired=2015-03-01}
Employee{name='白歌', salary=35000, dept='技术部', hired=2017-06-15}
Employee{name='小崔', salary=18000, dept='技术部', hired=2021-07-01}

=== 技术部老员工(入职 > 2年) ===
Employee{name='白歌', salary=35000, dept='技术部', hired=2017-06-15}
Employee{name='小崔', salary=18000, dept='技术部', hired=2021-07-01}

=== 打印 + 计算年终奖 ===
姓名: 大翔, 薪资: 50000
  → 预估年终奖: 90000
姓名: 白歌, 薪资: 35000
  → 预估年终奖: 63000
姓名: 小崔, 薪资: 18000
  → 预估年终奖: 32400
姓名: 孔蓝, 薪资: 12000
  → 预估年终奖: 21600

=== 员工姓名列表 ===
大翔
白歌
小崔
孔蓝
老刘

示例二:自定义函数式接口与策略模式

场景:白歌要求小崔将薪资计算策略抽象为函数式接口,支持不同的计算规则(标准薪资、绩效加权、年终折算)。

/**
 * 自定义函数式接口 —— 薪资计算策略
 */
@FunctionalInterface
interface SalaryStrategy {
    /**
     * 根据基础薪资计算实际发放金额
     * @param baseSalary 基础月薪
     * @return 实际发放金额
     */
    double calculate(double baseSalary);

    // 默认方法:策略组合
    default SalaryStrategy withBonus(double bonusRate) {
        return baseSalary -> calculate(baseSalary) * (1 + bonusRate);
    }
}

public class SalaryStrategyDemo {
    public static void main(String[] args) {
        double baseSalary = 20000;  // 基础月薪

        // === 策略1:标准月薪(不扣除) ===
        SalaryStrategy standard = salary -> salary;
        System.out.printf("标准月薪: %.2f%n", standard.calculate(baseSalary));

        // === 策略2:扣除五险一金(假设扣 22%) ===
        SalaryStrategy afterInsurance = salary -> salary * 0.78;
        System.out.printf("扣除五险一金后: %.2f%n", afterInsurance.calculate(baseSalary));

        // === 策略3:含年终奖(15%)的折算月薪 ===
        SalaryStrategy withBonus = standard.withBonus(0.15);
        System.out.printf("含15%%年终奖: %.2f%n", withBonus.calculate(baseSalary));

        // === 策略4:组合:先扣保险,再加奖金 ===
        SalaryStrategy netWithBonus = afterInsurance.withBonus(0.15);
        System.out.printf("扣除保险+含奖金: %.2f%n", netWithBonus.calculate(baseSalary));

        // === 使用策略列表进行批量计算 ===
        System.out.println("\n=== 批量薪资计算 ===");
        double[] salaries = {12000, 18000, 35000, 50000};
        for (double s : salaries) {
            double actual = netWithBonus.calculate(s);
            System.out.printf("月薪 %8.0f → 实发 %8.2f (税率: %.0f%%)%n",
                s, actual, (1 - actual / s) * 100);
        }
    }
}
运行输出:
标准月薪: 20000.00
扣除五险一金后: 15600.00
含15%年终奖: 23000.00
扣除保险+含奖金: 17940.00

=== 批量薪资计算 ===
月薪    12000 → 实发 10764.00 (税率: 10%)
月薪    18000 → 实发 16146.00 (税率: 10%)
月薪    35000 → 实发 31395.00 (税率: 10%)
月薪    50000 → 实发 44850.00 (税率: 10%)

6. 方法引用

方法引用是 Lambda 的简写形式,当 Lambda 体仅调用一个已存在的方法时,可以使用方法引用让代码更加简洁。

类型语法等效 Lambda说明
静态方法引用ClassName::staticMethod(args) -> ClassName.staticMethod(args)引用静态方法
实例方法引用(特定对象)instance::method(args) -> instance.method(args)引用某实例的方法
实例方法引用(类名)ClassName::instanceMethod(obj, args) -> obj.instanceMethod(args)第一个参数作为调用者
构造方法引用ClassName::new(args) -> new ClassName(args)引用构造器
// 静态方法引用
Function<String, Integer> parser = Integer::parseInt;          // 等效: s -> Integer.parseInt(s)

// 实例方法引用(特定对象)
String prefix = "EMP-";
Function<String, String> addPrefix = prefix::concat;           // 等效: s -> prefix.concat(s)

// 实例方法引用(类名)
Function<Employee, String> getName = Employee::getName;        // 等效: emp -> emp.getName()

// 构造方法引用
Supplier<Employee> empFactory = Employee::new;                 // 等效: () -> new Employee()
Function<String, Employee> namedEmp = Employee::new;           // 等效: name -> new Employee(name)

7. 易错场景

反例一:Lambda 中修改外部变量

小崔写了一段代码,试图在 Lambda 中修改变量:

// ❌ 错误:Lambda 中不能修改外部局部变量
int count = 0;
List<Employee> employees = getEmployees();
employees.forEach(emp -> {
    if (emp.getSalary() > 15000) {
        count++;  // 编译错误:Variable used in lambda should be final or effectively final
    }
});
System.out.println("高薪员工数: " + count);

原理分析:Lambda 表达式捕获的是变量的值,而不是变量本身。如果允许在 Lambda 中修改外部局部变量,会导致多线程不安全以及语义混乱。Lambda 要求外部局部变量是 effectively final(声明后未被重新赋值)的。

// ✅ 正确:使用原子类或 Stream 的 count() 方法
long count = employees.stream()
    .filter(emp -> emp.getSalary() > 15000)
    .count();
System.out.println("高薪员工数: " + count);

// ✅ 或者使用数组装一层(但不推荐,仅是变通方案)
int[] counter = {0};
employees.forEach(emp -> {
    if (emp.getSalary() > 15000) {
        counter[0]++;  // 数组元素可以修改
    }
});

反例二:Lambda 抛出受检异常

// ❌ 错误:Lambda 中抛出的受检异常无法传播
employees.forEach(emp -> {
    // 编译错误:Unhandled exception: IOException
    Files.write(Paths.get("employee.txt"), emp.toString().getBytes());
});
// ✅ 正确:在 Lambda 内部 try-catch
employees.forEach(emp -> {
    try {
        Files.write(Paths.get("employee.txt"), 
            (emp.toString() + "\n").getBytes(), StandardOpenOption.APPEND);
    } catch (IOException e) {
        throw new UncheckedIOException(e);  // 包装为非受检异常
    }
});

// ✅ 更好:抽取方法并处理异常
private static Consumer<Employee> safeWriter(Path path) {
    return emp -> {
        try {
            Files.write(path, (emp.toString() + "\n").getBytes(), StandardOpenOption.APPEND);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    };
}

反例三:将 Lambda 赋值给 Object 类型变量

// ❌ 错误:不能直接将 Lambda 赋值给 Object
Object obj = () -> System.out.println("Hello");  // 编译错误:Target type of lambda must be an interface

Lambda 表达式的类型必须是一个函数式接口类型,不能是 Object。需要先赋值给函数式接口:

// ✅ 正确:先赋值给函数式接口,再赋值给 Object
Runnable r = () -> System.out.println("Hello");
Object obj = (Object) r;  // 需要强制转型

反例四:滥用 Lambda 导致可读性下降

// ❌ 错误:Lambda 嵌套过深,可读性极差
employees.stream()
    .filter(e -> e.getDepartment().equals("技术部"))
    .map(e -> {
        return new Object() {
            String name = e.getName();
            double bonus = e.getSalary() * 0.15;
        };
    })
    .collect(Collectors.groupingBy(
        o -> o.bonus > 5000 ? "高奖金" : "低奖金",
        Collectors.mapping(o -> o.name, Collectors.toList())
    ));
// ✅ 正确:抽取中间变量和方法,保持可读性
Predicate<Employee> isTech = e -> "技术部".equals(e.getDepartment());
Function<Employee, BonusInfo> toBonusInfo = e -> 
    new BonusInfo(e.getName(), e.getSalary() * 0.15);
Function<BonusInfo, String> bonusLevel = b -> b.bonus > 5000 ? "高奖金" : "低奖金";

Map<String, List<String>> result = employees.stream()
    .filter(isTech)
    .map(toBonusInfo)
    .collect(Collectors.groupingBy(bonusLevel,
        Collectors.mapping(BonusInfo::getName, Collectors.toList())));

8. 面试考点

Q1:Lambda 表达式和匿名内部类有什么区别?

Lambda 和匿名内部类有本质区别,Lambda 不是语法糖。区别体现在四个方面:

  1. 实现机制:匿名内部类编译期生成独立的 Outer$N.class 文件;Lambda 通过 invokedynamic 指令在运行时动态生成实现类,不产生 .class 文件。
  2. this 引用:匿名内部类的 this 指向内部类自身;Lambda 的 this 指向外层类实例。
  3. 作用域:匿名内部类引入新的作用域,可以定义与外部同名的变量形成遮蔽;Lambda 不引入新作用域。
  4. 性能:匿名内部类每次创建都需要加载新类;Lambda 首次调用通过 LambdaMetafactory 生成实例后缓存复用。

Q2:什么是函数式接口?@FunctionalInterface 注解有什么用?

函数式接口是指有且仅有一个抽象方法的接口(Object 类的 public 方法和 default 方法不计入抽象方法计数)。@FunctionalInterface 注解让编译器在校验时确保该接口只有一个抽象方法,如果开发者误加了第二个抽象方法,编译会直接报错。建议所有函数式接口都加上此注解,作为一种文档标记和防御性编程手段。

Q3:Lambda 表达式为什么要求外部变量是 effectively final 的?

这是由 Lambda 的捕获语义决定的。Lambda 表达式捕获的是变量的值副本,而非变量本身。如果允许在 Lambda 中修改外部局部变量:

  1. 会破坏值捕获语义——修改的只是副本,不会影响原始变量,造成语义混乱
  2. 在多线程环境下会导致竞态条件
  3. 与函数式编程的"无副作用"哲学相悖

实例字段和静态字段没有此限制,因为 Lambda 通过 this 引用访问它们,修改是可见的。

Q4:invokedynamic 指令在 Lambda 实现中具体如何工作?

invokedynamic 是 JVM 在 JDK 7 引入的动态调用指令,JDK 8 用它来实现 Lambda。流程如下:

  1. 编译期:javac 将 Lambda 体提取为私有方法(如 private static boolean lambda$main$0(Employee e)),生成 invokedynamic 指令
  2. 首次执行 invokedynamic 时,JVM 调用引导方法 LambdaMetafactory.metafactory()
  3. 引导方法通过 Unsafe.defineAnonymousClass 生成一个实现了函数式接口的类,通过 MethodHandle 直接绑定到编译期生成的私有方法
  4. 生成的实例被 JVM 缓存,后续调用直接返回缓存实例

这个设计使得 Lambda 无需在编译期确定实现策略,未来的 JDK 版本可以在不修改字节码的情况下优化 Lambda 实现。

Q5:java.util.function 包中有哪些核心接口?各自的使用场景是什么?

  • Predicate<T>:boolean test(T t) —— 用于筛选/判断/校验,如 Stream 的 filter()
  • Consumer<T>:void accept(T t) —— 用于遍历/消费/输出,如 forEach()
  • Function<T,R>:R apply(T t) —— 用于类型转换/数据提取,如 Stream 的 map()
  • Supplier<T>:T get() —— 用于懒加载/工厂方法/默认值生成
  • BiFunction<T,U,R>:双参数版本,用于合并计算
  • UnaryOperator<T>:Function 的特化,输入输出同类型
  • BinaryOperator<T>:BiFunction 的特化,输入输出同类型,如 reduce()

白歌总结:"Lambda 表达式改变了 Java 程序员思考问题的方式。当你写 emp -> emp.getSalary() > 15000 时,传递的不只是一段代码,而是一种行为。行为参数化是 JDK 8 带给我们的最核心的编程范式转变。"

上一页
章节导读
下一页
Stream API 基础