Lambda 表达式原理

面试官问:"Lambda 表达式是怎么实现的?"

候选人小徐答:"Lambda 会被编译成匿名内部类。"

面试官追问:"是所有的 Lambda 都会生成新的类吗?"

小徐说:"应该是的..."

面试官又问:"那 Lambda 和匿名内部类有什么区别?"

小徐答不上来。

【面试官心理】 这道题考查的是候选人对 Java Lambda 底层实现的理解。能说出 invokedynamic 和 LambdaMetafactory 的候选人,说明对 JVM 字节码有研究。

一、Lambda 不是匿名内部类 🔴

1.1 字节码对比

// Lambda 表达式
Runnable r1 = () -> System.out.println("Lambda");

// 匿名内部类
Runnable r2 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Anonymous");
    }
};

// 反编译后的字节码差异:
// 匿名内部类:生成 OuterClass$1.class
// Lambda:使用 invokedynamic 指令,不生成类文件

1.2 invokedynamic 指令

// Lambda 编译后使用 invokedynamic 指令
// javap 反编译:
/*
invokedynamic #34,  0  // InvokeDynamic #0:run:()Ljava/lang/Runnable;
*/

// JVM 在运行时通过 LambdaMetafactory 调用 LambdaMetafactory.metafactory()
// 动态生成 Lambda 对象的实例化代码

二、Lambda 的实现机制 🔴

2.1 LambdaMetafactory

// 运行时,JVM 调用 LambdaMetafactory.metafactory()
// 生成一个 CallSite,指向实际的 Lambda 表达式

// 生成的代码类似于:
class Lambda implements Runnable {
    private final OuterClass outer;

    Lambda(OuterClass outer) {
        this.outer = outer;
    }

    @Override
    public void run() {
        outer.print(); // 实际执行的代码
    }
}

2.2 两种 Lambda 类型

// 类型一:无捕获 Lambda(不访问外部变量)
Runnable r1 = () -> System.out.println("no capture");
// JVM 缓存:所有 () -> println 的地方共享同一个 Lambda 实例

// 类型二:捕获 Lambda(访问外部变量)
int x = 10;
Runnable r2 = () -> System.out.println(x);
// 每个捕获都需要新的 Lambda 实例(因为 x 的值可能不同)

三、Lambda 捕获机制 🔴

3.1 有效 final(Effectively Final)

// ✅ 可以捕获:外部变量是 effectively final
int x = 10;
Runnable r = () -> System.out.println(x);

// ❌ 不能捕获:外部变量被修改
int x = 10;
x = 20; // 修改后不再是 effectively final
Runnable r = () -> System.out.println(x); // 编译错误

3.2 捕获的内存模型

class Outer {
    void method() {
        int x = 10;
        String s = "hello";

        Runnable r = () -> {
            System.out.println(s); // 捕获 s 的引用
            // x = 20; // 编译错误
        };
    }
}

// 编译后生成的内部类(伪代码):
class Outer$1 implements Runnable {
    private final String val$s; // 捕获的值(副本)

    Outer$1(String val$s) {
        this.val$s = val$s;
    }

    public void run() {
        System.out.println(this.val$s);
    }
}

四、Lambda vs 匿名内部类 🟡

维度Lambda匿名内部类
编译产物invokedynamic(运行时生成)生成新的 .class 文件
this 引用指向包围类的 this指向匿名内部类自身
作用域词法作用域有自己的作用域
性能更快(无需加载类)较慢(需要类加载)
内存更节省(可共享)每个实例一个类

4.1 this 引用的区别

class Outer {
    Runnable r = new Runnable() {
        @Override
        public void run() {
            System.out.println(this); // this 是匿名内部类自身
        }
    };

    Runnable lambda = () -> {
        System.out.println(this); // this 是 Outer 自身
    };
}

五、方法引用 🟡

5.1 四种方法引用

// 类型一:静态方法引用
Function<String, Integer> parser = Integer::parseInt;
parser.apply("123"); // 123

// 类型二:实例方法引用(特定对象)
String str = "hello";
Supplier<Integer> len = str::length;
len.get(); // 5

// 类型三:实例方法引用(任意对象)
Function<String, Integer> len2 = String::length;
len2.apply("world"); // 5

// 类型四:构造器引用
Supplier<ArrayList<String>> factory = ArrayList::new;
factory.get(); // new ArrayList<>()

六、性能特点 🟡

6.1 Lambda 的性能开销

// Lambda 的性能开销来源:
// 1. invokedynamic 首次调用时的 CallSite 建立
// 2. LambdaMetafactory 生成 CallSite
// 3. 如果每次都创建 Lambda,有分配开销

// 优化:
// 1. 缓存 Lambda(如果重复使用)
Function<String, Integer> parser = Integer::parseInt; // 只创建一次
list.stream().map(parser).collect(toList());

// 2. 方法引用通常比 Lambda 快
list.stream().map(String::toUpperCase).collect(toList()); // 方法引用
list.stream().map(s -> s.toUpperCase()).collect(toList()); // Lambda

七、追问升级

面试官:"JDK 8 的 Lambda 为什么要用 invokedynamic?"

// 原因一:性能
// invokedynamic 允许 JVM 延迟绑定,在运行时选择最优实现
// 避免过早优化

// 原因二:灵活性
// JDK 8 可以通过 -Djava.lang.invoke.MethodHandle.DEFINE_MODE
// 切换 Lambda 的实现策略

// 原因三:兼容性
// invokedynamic 是稳定的字节码指令
// 不影响已编译的 class 文件

【面试官心理】 能说出 invokedynamic 原因的候选人,说明对 JVM 字节码有研究。这是 P6+ 的加分点。