一.前言

JAVA8引入的stream流在日常开发中使用非常频繁,配合着Lambda表达式一起使用让原本冗长的代码瞬间变得清爽干练了许多,不过在使用之中也出现了些许问题,例如下情况,在lambda表达式中使用了外部变量,并且想要修改它

可以看到编译器直接编译报错,并且提示lambda表达中的使用的变量需要是final 或者effective final, 正常情况下我们直接使用这个变量,不做任何修改都是没有问题的,为什么一想要修改就出现问题了呢,下面来展开探讨下。

二.Lambda表达式与匿名内部类

Lambda表达式的使用通常要对应的接口能够支持函数式编程,通常表现为在接口上有标注@FunctionalInterface注解的接口,这类接口通常只有一个抽象方法(与Object类下同名的抽象方法不算,因为所有类都间接或直接继承于Object,所以同名的抽象方法肯定会被实现),所以只要实现了这个抽象方法就相当于构造了一个实现该接口的类。

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

//这样就创建了一个实现了Runnable接口的类
Runnable runnable =()->{
};

lambda表达式其实非常类似于匿名内部类,在Java8之前在内部匿名类中使用外部的局部变量的时会直接编译报错 “Cannot refer to a non-final variable arg inside an inner class defined in a different method”,而在JAVA8中报错就消失了,和Lambda表达式中一样,可以正常使用,但同样的不能对其进行修改。那为什么从之前的非final变量不能使用到可以直接使用,其实是Java在匿名内部类中帮我们隐式的创建了变量的对应final副本,具体如何下面来解析看下

三.实践

为了证明Lambda表达式中的变量与外部的局部变量并不是同一个变量,我们先看这段代码

public class TestInnerClass2 {
    void testInnerClass() {
        int a = 1;
        Runnable runnable = () -> {
            System.out.println(a);
        };
    }
}

通过javac将该文件编译成.class文件,后再用javap 进行反编译

可以看到,除了本身的方法,根据lambda表达式有编译生成了一个私有的静态方法,并且方法参数列表为(int),这个其实就是根据lambda生成的匿名内部类的需要实现的方法。
通过jclasslib插件我们再来看下testInnerClass的字节码


可以看到,调用lambda表达式创建Runbale对象时,实际是通过invovkdynamic调用BoootstrapMehtod来完成创建匿名内部类以及生成上面展示的私有静态方法。
那么我们lambda表达式中使用的a变量到底是哪里来的,其实上面反编译的时候已经看到,生成的私有静态方法的参数列表中是有int这个参数的

通过字节码看到打印的时候也确实是直接iload_0从该方法的本地变量表中取出变量a,那么这个变量a难道就只是简单的通过基础类型变量的值传递,复制进来的变量吗,那为什么会不能修改呢,其实这个变量是lambda生成的匿名内部类自己定义了一份private final 的变量,所以才会让我们在修改的时候提示报错,为了验证这个,我们测试如下代码

public class TestInnerClass2 {
   public void testInnerClass() throws NoSuchFieldException {
        int a = 1;
       Runnable runnable = () -> {
            System.out.println(a);
        };
       Class<? extends Runnable> aClass = runnable.getClass();
       Field[] declaredFields = aClass.getDeclaredFields();
       for(Field field:declaredFields){
           System.out.println(field);
       }
   }
    public static void main(String[] args) throws NoSuchFieldException {
        new TestInnerClass2().testInnerClass();
    }
}

输出结果如下:

可以看到,我们创建的这个匿名内部类下是存在一个成员属性,并且用private final int所修饰,我们将这个属性进行打印来看一下

    public void testInnerClass() throws NoSuchFieldException, IllegalAccessException {
        int a = 1;
        Runnable runnable = () -> {
            System.out.println(a);
        };
        Class<? extends Runnable> aClass = runnable.getClass();
        Field[] declaredFields = aClass.getDeclaredFields();
        for (Field field : declaredFields) {
            field.setAccessible(true);
            System.out.println(field + " ,name=" + field.getName() + ", value=" + field.get(runnable));
        }
    }

可以看到,这个属性名为arg$1,并且值为1,所以这就验证了在Lambda表达式使用到的的外部的局部变量都是Lambda自己创建的一份private final的副本,所以才会不可修改。

所以说在lambda表达式中引用到的外部的基础类型变量,都会创建对应的private final副本,对象类型的变量,也都会创建对应的private final的副本,只不过lambda创建的对象变量保存的对象地址也是和外部的一样的。所以我们在lambda调用对象的方法同样也会改变外部的对象,因为二者的都是指向堆区中同一个对象,但要是想将lambda内的对象变量指向别的地址,那同样的编译器也会直接报错。

我们不借助lambda表达式,直接用匿名内部类来引用外部变量再来看看是怎么样

public class TestInnerClass2 {
    public void testInnerClass() throws NoSuchFieldException, IllegalAccessException {
        int a = 1;
        Runnable runnable = () -> {
            System.out.println(a);
        };
        Class<? extends Runnable> aClass = runnable.getClass();
        Field[] declaredFields = aClass.getDeclaredFields();
        for (Field field : declaredFields) {
            field.setAccessible(true);
            System.out.println(field + " ,name=" + field.getName() + ", value=" + field.get(runnable));
        }
        Comparator<Integer> comparator =new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                System.out.println(a);
                return 0;
            }
            @Override
            public boolean equals(Object obj) {
                return false;
            }
        };
        Class<? extends Comparator> bClass = comparator.getClass();
        Field[] bdeclaredFields = bClass.getDeclaredFields();
        for (Field field : bdeclaredFields) {
            System.out.println(field + " ,name=" + field.getName() + ", value=" + field.get(comparator));
        }
        System.out.println(this);
    }

通过字节码可以看到,原生的匿名内部类并没有像lambda表达式一样创建一个private static 的方法,也没有借助BootsratpMethod构建匿名内部类,而是直接new 了一个内部类,名字就叫做TestInnerClass2$1,将程序运行打印结果来看一下

通过打印结果看到,原生匿名内部类和lambda表达式还是略有不同,原生匿名内部类是将引用到的外部变量定义了一份final 的副本,并没有用private来修饰,除此之外匿名内部类还持有另外一个成员变量this$0,指向了外部对象的地址。

四.总结

通过上面的实践,我们发现了Lambda表达式中引用的外部局部变量的处理,是在内部定义了一份private final的变量,而原生匿名内部类则是定义了一份final的变量,所以我们在开发中想要对二者内的外部变量进行修改时都会出现报错Variable used in lambda expression should be final or effectively final 或者 Variable 'a' is accessed from within inner class, needs to be final or effectively final,提示我们内部类使用的变量都是final或者有效final的。
最后,对于Lambda表达式的实现原理和细节其实还有很多暂时还不清楚,例如BootstrapMethods这些的使用,还需继续学习,由于作者水平有限,文中可能有错误之处,请大家指出。

posted on 2022-06-15 14:40  Yuqi与其  阅读(1510)  评论(1编辑  收藏  举报