由lambda中的变量想到的那些事
主要内容是Java中lambda的实现原理,为什么在lambda中使用的外部变量需要是“final”语义的,以及invokedynamic
指令的简单介绍
Java中lambda的引入简化了编程,减少了代码量,更是流式编程必不可少的语法
但是lambda也有一些限制,例如有时候当我们试图在lambda表达式中修改一个外部变量的时,将得到variable used in lambda expression should be final or effectively final
这个错误,这是为什么呢?IDEA会智能提示我们使用数组或者原子类可以避免这个错误,这又是为什么呢?以前我都是直接按照IDEA的智能提示修改了,没有细究,也不是不能用。我之前也在网上简单地搜索了一下相关的资料,但是没太看懂,所以今天再来探索一下。
如下面的代码所示,在代码中统计了一个List中”apple”的个数,这样的代码自然是没法通过编译的。
1 2 3 4 5 6 7 8 9 10 public void test () { List<String> fruits = Arrays.asList("apple" , "banana" , "apple" , "watermelon" , "grape" ); int count = 0 ; fruits.forEach(f -> { if ("apple" .equals(f)) { count++; } }); System.out.println("the total number of apple is " + count); }
当然,上面的代码可以由以下代码代替(集合的操作大都可以思考一下是否可以以流的形式进行操作)
1 2 3 4 5 public void test () { List<String> fruits = Arrays.asList("apple" , "banana" , "apple" , "watermelon" , "grape" ); int count = (int ) fruits.stream().filter("apple" ::equals).count(); System.out.println("the total number of apple is " + count); }
lambda的实现原理
首先我们定义一个函数式接口,函数式接口中有且仅有一个抽象方法
1 2 3 4 @FunctionalInterface public interface MyFunctionInterface { void sayHello (String name) ; }
在FunctionInterfaceDemo
中使用lambda表达式。我们先设想一下lambda是怎么实现的呢?是新生成了一个类吗,不然为什么要搞一个函数式接口呢?我们加上JVM参数-Djdk.internal.lambda.dumpProxyClasses
,这样代码运行时产生的中间类将会被保存下来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class FunctionInterfaceDemo { public void welcome (String name, MyFunctionInterface fi) { fi.sayHello(name); } public static void main (String[] args) { long now = System.currentTimeMillis(); if (now % 2 == 0 ) { new FunctionInterfaceDemo ().welcome("equator" , (name) -> System.out.println("welcome, " + name)); } else { new FunctionInterfaceDemo ().welcome("leo" , (name) -> System.out.println("hi, " + name)); } } }
运行一次 上面的代码之后,我们可以发现产生了一个 中间类FunctionInterfaceDemo$$Lambda$1
,它实现了我们的函数式接口MyFunctionInterface
,并在实现的方法中调用了FunctionInterfaceDemo
类的lambda$main$0
方法,但是我们并没有写过这样的方法呀,应该是编译器自动生成的方法。
1 2 3 4 5 6 7 8 9 final class FunctionInterfaceDemo$$Lambda$1 implements MyFunctionInterface { private FunctionInterfaceDemo$$Lambda$1 () { } @Hidden public void sayHello (String var1) { FunctionInterfaceDemo.lambda$main$0 (var1); } }
使用命令javap -p
反编译FunctionInterfaceDemo.class
文件,可以看到的确自动生成了两个私有的静态方法(方法是私有的,但不一定是静态的。如果在静态方法中使用lambda表达式,会生成静态的私有方法;反之则是非静态的私有方法,this指向使用lambda表达式那个类的对象实例 )
1 2 3 4 5 6 7 8 Compiled from "FunctionInterfaceDemo.java" public class com.equator.lambda.FunctionInterfaceDemo { public com.equator.lambda.FunctionInterfaceDemo(); public void welcome(java.lang.String, com.equator.lambda.MyFunctionInterface); public static void main(java.lang.String[]); private static void lambda$main$1(java.lang.String); private static void lambda$main$0(java.lang.String); }
使用javap -c -v
指令反编译FunctionInterfaceDemo.class
文件,可以看到自动生成的方法的逻辑正是我们的lambda表达式的逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 private static void lambda$main$0(java.lang.String); Code: 0: getstatic #13 // Field java/lang/System.out:Ljava/io/PrintStream; 3: new #14 // class java/lang/StringBuilder 6: dup 7: invokespecial #15 // Method java/lang/StringBuilder."<init>":()V 10: ldc #20 // String welcome, 12: invokevirtual #17 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 15: aload_0 16: invokevirtual #17 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 19: invokevirtual #18 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 22: invokevirtual #19 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 25: return
现在我们知道了lambda的实现原理是编译时编译器会在使用到lambda的类中自动生成对应的私有方法,然后在运行期间JVM会生成实现了函数式接口的内部类,在实现的方法中调用了前面生成的私有方法(内容是lambda表达式的逻辑)。具体生成内部类的方法在java.lang.invoke.LambdaMetafactory
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static CallSite metafactory (MethodHandles.Lookup caller, String invokedName, MethodType invokedType, MethodType samMethodType, MethodHandle implMethod, MethodType instantiatedMethodType) throws LambdaConversionException { AbstractValidatingLambdaMetafactory mf; mf = new InnerClassLambdaMetafactory (caller, invokedType, invokedName, samMethodType, implMethod, instantiatedMethodType, false , EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY); mf.validateMetafactoryArgs(); return mf.buildCallSite(); }
使用了外部变量的lambda表达式
我们得知了lambda的实现原理,那么为什么在lambda中的使用到的外部变量需要是final或者是effectively final的呢?
注:final指的是显式地声明final,effectively final指的是没有显式声明final,但是不对这个变量进行修改
我们再定义一个类VariableUseIntTest
,加上-Djdk.internal.lambda.dumpProxyClasses
参数,然后运行
1 2 3 4 5 6 7 8 9 10 11 12 public class VariableUseIntTest { public static void intro (String name, Consumer consumer) { consumer.accept(name); } public static void main (String[] args) { int age = 22 ; intro("leo" , (name) -> { System.out.println(String.format("My name is %s. I am %s years old." , name, age)); }); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 final class VariableUseIntTest$$Lambda$1 implements Consumer { private final int arg$1 ; private VariableUseIntTest$$Lambda$1 (int var1) { this .arg$1 = var1; } private static Consumer get$Lambda(int var0) { return new VariableUseIntTest$$Lambda$1 (var0); } @Hidden public void accept (Object var1) { VariableUseIntTest.lambda$main$0 (this .arg$1 , var1); } }
也就是说,lambda在使用外部变量的时候,走的是Java中方法传参的途径来捕获外部变量的。在方法内部修改一个基本类型这个操作对方法外部不可见,所以final的语义算是对Java程序员的一种提醒与要求~
Java中方法传递参数都是值传递,会拷贝一个副本到当前栈帧的局部变量表中。无论是基本类型还是引用类型,变量都是两个不同的变量,但是引用类型的副本和原变量都引用堆上同一个对象实例,所以在一个方法内部可以通过这个副本去修改对象的内容。
解决方法
下面的2、3、4几个方法其实都是同一个原理:对象实例在堆上分配,我们通过变量去修改实例的内容而不是修改变量本身。
1 2 3 4 5 6 7 8 9 10 public void test () { List<String> fruits = Arrays.asList("apple" , "banana" , "apple" , "watermelon" , "grape" ); Box box = new Box (0 ); fruits.forEach(f -> { if ("apple" .equals(f)) { box.setNum(box.getNum() + 1 ); } }); System.out.println("the total number of apple is " + box.getNum()); }
1 2 3 4 5 6 7 8 9 10 public void test () { List<String> fruits = Arrays.asList("apple" , "banana" , "apple" , "watermelon" , "grape" ); final int [] count = {0 }; fruits.forEach(f -> { if ("apple" .equals(f)) { count[0 ]++; } }); System.out.println("the total number of apple is " + count[0 ]); }
使用原子类(和线程安全没有关系哟,只是将AtomicInteger作为基本类型int变量的容器,这种方法可能是4种方法中最好的一个吧,数组不太美观,也不用自己创建一个容器类)
1 2 3 4 5 6 7 8 9 10 public void test () { List<String> fruits = Arrays.asList("apple" , "banana" , "apple" , "watermelon" , "grape" ); AtomicInteger count = new AtomicInteger (); fruits.forEach(f -> { if ("apple" .equals(f)) { count.getAndIncrement(); } }); System.out.println("the total number of apple is " + count.get()); }
invokedynamic指令
我们再来看看这段代码:这段代码只运行一次的话,由于now要么是奇数要么是偶数,所以lambda实际上只会执行一次。中间类只有一个,因为lambda的内部类是运行时生成的;但是私有方法生成了两个,所以这个是编译时就生成的方法。(我们也可以只编译不运行来验证这个说法)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class FunctionInterfaceDemo { public void welcome (String name, MyFunctionInterface fi) { fi.sayHello(name); } public static void main (String[] args) { long now = System.currentTimeMillis(); if (now % 2 == 0 ) { new FunctionInterfaceDemo ().welcome("equator" , (name) -> System.out.println("welcome, " + name)); } else { new FunctionInterfaceDemo ().welcome("leo" , (name) -> System.out.println("hi, " + name)); } } } public class com .equator.lambda.FunctionInterfaceDemo { public com.equator.lambda.FunctionInterfaceDemo(); public void welcome (java.lang.String, com.equator.lambda.MyFunctionInterface) ; public static void main (java.lang.String[]) ; private static void lambda$main$1 (java.lang.String); private static void lambda$main$0 (java.lang.String); }
可以看到在main方法中lambda表达式的调用使用到了invokedynamic
指令,为什么lambda的实现用到了invokedynamic
指令呢,可以看看RednaxelaFX的一个回答 。简单地说就是更加灵活了,以前的四种方法调用指令(invokestatic、invokespecial、invokevirtual、invokeinterface
)其分派逻辑都固定在JVM内,而invokedynamic
指令的分派逻辑由用户设定的引导方法来决定(功能类似于invokevirtual
但是更加灵活)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static void main (java.lang.String[]) ; Code: 0 : invokestatic #3 3 : lstore_1 4 : lload_1 5 : ldc2_w #4 8 : lrem 9 : lconst_0 10 : lcmp 11 : ifne 34 14 : new #6 17 : dup 18 : invokespecial #7 21 : ldc #8 23 : invokedynamic #9 , 0 28 : invokevirtual #10 31 : goto 51 34 : new #6 37 : dup 38 : invokespecial #7 41 : ldc #11 43 : invokedynamic #12 , 0 48 : invokevirtual #10 51 : return
对应常量池(#0,#1表示BootstrapMethods
属性表里面的第0和第1项)
1 2 #9 = InvokeDynamic #0:#58 // #0:sayHello:()Lcom/equator/lambda/MyFunctionInterface; #12 = InvokeDynamic #1:#58 // #1:sayHello:()Lcom/equator/lambda/MyFunctionInterface;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 BootstrapMethods: 0: #55 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljav a/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invok e/CallSite; Method arguments: #56 (Ljava/lang/String;)V #57 invokestatic com/equator/lambda/FunctionInterfaceDemo.lambda$main$0:(Ljava/lang/String;)V #56 (Ljava/lang/String;)V 1: #55 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljav a/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invok e/CallSite; Method arguments: #56 (Ljava/lang/String;)V #61 invokestatic com/equator/lambda/FunctionInterfaceDemo.lambda$main$1:(Ljava/lang/String;)V #56 (Ljava/lang/String;)V
lambda使用到的引导方法都是同一个java/lang/invoke/LambdaMetafactory.metafactory
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public static CallSite metafactory (MethodHandles.Lookup caller, String invokedName, MethodType invokedType, MethodType samMethodType, MethodHandle implMethod, MethodType instantiatedMethodType) throws LambdaConversionException { AbstractValidatingLambdaMetafactory mf; mf = new InnerClassLambdaMetafactory (caller, invokedType, invokedName, samMethodType, implMethod, instantiatedMethodType, false , EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY); mf.validateMetafactoryArgs(); return mf.buildCallSite(); }
参考资料