Lambda表达式和方法引用

这里我们只讲lambda表达式的实现原理,以此来理解lambda表达式的一些限制。

lambda表达式的目的

在jdk1.8之前,如果我们想要用到一个自己实现的子类,我们必须自己定义一个匿名内部类最为方便。

/**
* code_1
**/
public class TestLambda {
    
    public static void main(String[] args) throws InterruptedException {
    // 不使用lambda表达式,使用匿名内部类
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                super.run();
                System.out.println("使用匿名内部类的方法,创建线程。");
            }
        };
        thread1.start();
        thread1.join();
        // 使用lambda表达式方法
        Thread thread2 = new Thread(()->{
            System.out.println("使用lambda表达式的方式,创建线程。");
        });
        thread2.start();
        thread2.join();
    }
}

可以看到,匿名内部类的方法,是重新定义一个新的子类实现。但是这个子类实现只能使用这么一次。

所以出现了一个矛盾,往往只使用一次的子类实现,并不需要复杂的重写和实现,更多的时候只是最某个常用的方法进行实现。所以,匿名内部类的写法,有大量的冗余信息。

lambda表达式就是提供了更加简洁的表达式方式来实现这个匿名内部类。

ok,存在逻辑清楚,现在开始如何实现。

Lambda表达式实现

Lambda表达式,实际上不只是个表达式,更涉及到了java要实现的另一个概念:将方法作为参数传递,实现函数式接口。

类似于这样:

/**
* code_2
**/
public class TestLambda {
        // 将方法作为参数传递。
        Thread thread3 = new Thread(TestLambda::run);
        thread3.start();
        thread3.join();
    }

    private static void run() {
        System.out.println("静态方法。");
    }
}

打断点来看,直接走的是Thread类的这个构造方法:

也就是说,这个方法的引用,实际上是直接传递到函数式接口上的。(函数式接口就是只有一个抽象方法的接口)

以前的实现

以前传递给接口上的参数都是什么东西,怎么实现的?

以前传的都是实现了对应接口的类的实例。

Lambda表达式

所以Lambda表达式直接使用以前模式实现就行了。

编译器直接将Lambda表达式做成一个匿名内部类,然后Lambda表达式做一个唯一实例作为参数传递过去不就可以走以前的流程了吗?

看底层文件
/**
* code_3
**/
public class TestLambda {
    
    public static void main(String[] args) throws InterruptedException {
        // 使用lambda表达式方法
        Thread thread2 = new Thread(()->{
            System.out.println("使用lambda表达式的方式,创建线程。");
        });
        thread2.start();
        thread2.join();
    }
}

ok,我们看看实际的实现。如果是这样的实现(code_1),在编译之后应该会有匿名内部类的类文件。

没问题,确实多了一个TestLambda$1.class的文件。咱们使用javap反编译一下。

如果是code_3的实现我们看看编译结果:

只剩下了一个类文件,说明lambda表达式不是通过匿名内部类的方法实现的。

使用 javap -p -c -v TestLambda.class可以看到具体的实现。

可以发现多了一个私有静态方法:

在main方法中,对于原本lambda表达式的位置换成了这样。

所以实际上Java使用私有静态方法来存储lambda方法,这个方法的调用使用invokedynamic指令来计算实际的调用地址

invokedynamic指令后面会跟一个指向常量池的调用点限定符,这个限定符会被解析为一个动态调用点。在运行时,JVM会使用一个引导方法(bootstrap method)来解析这个调用点,并生成一个CallSite对象。这个对象包含了一个方法句柄(MethodHandle),用于直接调用前面生成的私有静态方法

lambda表达式访问和使用外部变量

直接反编译,可以看到,通过编译器,将调用点和私有静态方法的方法参数进行修改即可。

所以实际上lambda表达式内部所操作变量的是另一个作用域的局部变量,我们对其进行赋值,修并不能作用到调用lambda表达式的作用域中的同名变量中。

就像下面这个试图修改局部变量的例子:

public class Main{
    public static void lambdaFunction(int a){
        a*=2;
        System.out.println("a is "+a);
    }
    public static void main(String[] args){
        int a = 10;
        lambdaFunction(a);
        // 在lambdaFunction函数中修改a的值,在外部作用域没有效果
        System.out.println("a is "+ a);
    }
}

所以直接禁止了在lambda表达式内部对外部作用域变量进行赋值的操作,因为这些操作是毫无意义的,还在表面上给人一种重新赋值的迷惑性假象导致逻辑上的bug。

通过限制Lambda表达式只能捕获final或effectively final变量,Java确保了Lambda表达式在并发环境中的行为是可预测的,从而提高了线程安全性。这种设计选择有助于开发者编写更简洁、更少出错的并发代码。

什么是Effectively Final变量

如果一个局部变量满足以下条件,那么它就是effectively final的:

  1. 初始化后不修改:变量在声明和初始化之后,没有再次被赋值。
  2. 可能在循环中修改:在某些情况下,即使在循环中修改了变量的值,只要该循环不会影响到Lambda表达式中变量的值,该变量也可以被视为effectively final。

方法引用

Java 8还引入了方法引用,它是一种特殊的Lambda表达式,用于直接引用现有的方法。方法引用有三种形式:

  1. 静态方法引用ClassName::staticMethodName
  2. 实例方法引用instance::instanceMethodName
  3. 类实例方法引用ClassName::instanceMethodName
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值