同样是简化代码,Lambda 和匿名内部类的核心原理是什么?

使用案例

匿名内部类使用

java 复制代码
public class AnonymousInnerClassTest {  
    public static void main(String[] args) {  
        Consumer<String> consumer = new Consumer<String>() {  
            @Override  
            public void accept(String s) {  
                System.out.println(s);  
            }  
        };  
        consumer.accept("Hello");  
    }  
}

可以看到编译器帮助生成了一个 class 文件,这个 class 文件的内容和我们自己定义一个类没有任何区别,如下图所示:

Lambda表达式使用

java 复制代码
public class LambdaTest {  
    public static void main(String[] args) {  
        Consumer<String> consumer = s -> System.out.println(s);  
        consumer.accept("Hello");  
    }  
}

可以看到编译器并未帮助生成 class 文件:

原理解读

匿名内部类原理

匿名内部类本质上是一个语法糖。当代码中的某个类只在一个地方使用,不需要复用的时候,这个时候就可以通过匿名内部类语法来简化使用。它本质上和通过 Java 类文件定义没有区别,只是编译器在编译的时候自动帮你生成了 class 文件。

上面案例中的代码等价如下:

java 复制代码
public static void main(String[] args) {  
        Consumer<String> consumer = new AnonymousInnerClassTest$1();
        consumer.accept("Hello");  
    }  

可以通过查看字节码文件来证实

Lambda表达式原理

通过查看字节码文件可以看到,编译器会生成一个 lambda$main$0() 的私有静态方法,它的方法执行体就是 lambda 表达式的内容。如下图所示:

等价的代码如下:

java 复制代码
public class LambdaTest {  
    public static void main(String[] args) {  
        // Consumer<String> consumer = s -> System.out.println(s);  
        // 这里被替换为了invokedynamic字节码的调用
        invokedynamic #7, 0
        consumer.accept("Hello");  
    }  
    
    // 这个方法是编译器生成的
    private static void lambda$main$0(String s) {
	    System.out.println(s);
    }
}

main() 方法中第一次执行到 invokedynamic 指令时,JVM 会找到 invokedynamic 指令关联的引导方法,即 LambdaMetafactory.metafactory()。如下图所示:

然后把 invokedynamic 指令后面的信息,连同对编译器生成的静态方法 lambda$main$0 的引用(通过一个叫MethodHandle的对象),一起传递给 metafactory()方法。LambdaMetafactory 接收到这些信息后,会像一个代码生成器一样,在内存中即时生成一个新类的字节码,等价的代码逻辑如下:

java 复制代码
final class LambdaTest$$Lambda$1 implements java.util.function.Consumer {
    // 对于无状态Lambda,可以是一个单例
    private static final LambdaTest$$Lambda$1 INSTANCE = new LambdaTest$$Lambda$1();

    private LambdaTest$$Lambda$1() {}

    @Override
    public void accept(Object s) {
        LambdaTest.lambda$main$0((String) s);
    }
}

metafactory() 方法并不直接返回这个类的实例,而是返回一个 CallSite 对象。这个 CallSite 对象内部持有一个「工厂」,这个工厂知道如何创建上面那个动态生成的 LambdaTest$$Lambda$1 类的实例。JVM通过 CallSite 返回的工厂,创建 LambdaTest$$Lambda$1 的一个实例。源码如下:

java 复制代码
public static CallSite metafactory(MethodHandles.Lookup caller, String interfaceMethodName, MethodType factoryType,  
   MethodType interfaceMethodType, MethodHandle implementation, MethodType dynamicMethodType) throws LambdaConversionException {  
    AbstractValidatingLambdaMetafactory mf;  
    mf = new InnerClassLambdaMetafactory(Objects.requireNonNull(caller),  
			 Objects.requireNonNull(factoryType),  
			 Objects.requireNonNull(interfaceMethodName),  
			 Objects.requireNonNull(interfaceMethodType),  
			 Objects.requireNonNull(implementation),  
			 Objects.requireNonNull(dynamicMethodType),  
			 false,  
			 EMPTY_CLASS_ARRAY,  
			 EMPTY_MT_ARRAY);  
    mf.validateMetafactoryArgs();  
    return mf.buildCallSite();  
}
java 复制代码
abstract public class CallSite {
	public abstract MethodHandle getTarget();
}

当执行 consumer.accept("Hello") 时,字节码是 invokeinterface,这就像一个普通的多态方法调用,会调用到我们动态生成的 LambdaTest$$Lambda$1 类的accept方法。如下图所示:

上述过程的代码等价于下面的代码:

java 复制代码
import java.lang.invoke.*;
import java.util.function.Consumer;

public class ManualLambdaFactory {
	// 定义一个私有静态方法,模拟JVM自动生成的静态方法
    private static void printString(String s) {
        System.out.println(s);
    }

    public static void main(String[] args) throws Throwable {
        // LambdaMetafactory.metafactory()方法有6个参数
        
        // 这是 metafactory 的第1个参数
        MethodHandles.Lookup caller = MethodHandles.lookup();

        // 参数2: invokedName - 要实现的接口方法名
        String invokedName = "accept";

        // 参数3: invokedType - 工厂本身的类型
        MethodType invokedType = MethodType.methodType(Consumer.class);

        // 参数4: samMethodType - 函数式接口中抽象方法的签名
        MethodType samMethodType = MethodType.methodType(void.class, Object.class);

        // 参数5: implMethod - 指向我们实现体(printString)的方法句柄(MethodHandle,有点类似于C++中的方法指针)
        MethodHandle implMethod = caller.findStatic(
                ManualLambdaFactory.class, // 在哪个类里找
                "printString",             // 方法名叫什么
                MethodType.methodType(void.class, String.class)
        );

        // 参数6: instantiatedMethodType - 实现方法的实际签名
        MethodType instantiatedMethodType = MethodType.methodType(void.class, String.class);

        // 核心步骤:手动调用 metafactory,模拟 invokedynamic 指令的行为
        CallSite callSite = LambdaMetafactory.metafactory(
                caller,
                invokedName,
                invokedType,
                samMethodType,
                implMethod,
                instantiatedMethodType
        );
        
        // 调用这个工厂,可以得到Consumer实例
        MethodHandle factory = callSite.getTarget();

        // 调用工厂,创建 Consumer 实例
        // 这里需要一个强制类型转换,因为 MethodHandle 返回的是 Object
        Consumer<String> consumer = (Consumer<String>) factory.invokeExact();
        
        consumer.accept("Hello, World!");
    }
}

effectively final 变量

假设把上面的匿名内部类的代码修改为如下,这个时候 Idea 就会有提示说变量 a 必须是 final 或者 effectively final 的。也就是说在匿名内部类后面的代码不能再对变量 a 的值进行修改了。如下图所示:

java 复制代码
public class AnonymousInnerClassTest {  
    public static void main(String[] args) {  
        String a = "hello";  
        Consumer<String> consumer = new Consumer<String>() {  
            @Override  
            public void accept(String s) {  
                System.out.println(a);  
            }  
        };  
        a = "nihao";  
        consumer.accept("Hello");  
    }  
}

通过查看生成的匿名内部类的 class 文件可以看到,匿名内部类中引用的外部变量实际上是通过匿名内部类的构造函数中传递进去的,而 Java 中方法的参数传递都是值传递。假设在匿名内部类后面的代码修改了变量 a 的值,那么匿名内部类中是无法感知到这个值的变化的,那就可能造成两边的值是不一样的。

相关推荐
Yeats_Liao2 小时前
时序数据库系列(六):物联网监控系统实战
数据库·后端·物联网·时序数据库
金銀銅鐵2 小时前
[Java] 用 Swing 生成一个最大公约数计算器
java·后端
brzhang2 小时前
我觉得可以试试 TOON —— 一个为 LLM 而生的极致压缩数据格式
前端·后端·架构
苏三的开发日记2 小时前
库存预扣减之后,用户订单超时之后补偿库存的方案
后端
知其然亦知其所以然3 小时前
这波AI太原生了!SpringAI让PostgreSQL秒变智能数据库!
后端·spring·postgresql
观望过往4 小时前
Spring Boot 集成 EMQ X 4.0 完整技术指南
java·spring boot·后端·emqx
心之语歌4 小时前
对于 时间复杂度和空间复杂度分析
后端
青旬4 小时前
AI编程祛魅-最近几个失败的ai编程经历
后端·程序员