Java中的lambda每次执行都会创建一个新对象吗
- 2020 年 1 月 13 日
- 笔记
之前写过一篇文章 Java中的Lambda是如何实现的,该篇文章中讲到,在lambda表达式执行时,jvm会先为该lambda生成一个java类,然后再创建一个该类对应的对象,最后执行该对象对应的方法,以此来执行我们写的lambda方法体。
那该lambda表达式每次执行时都会创建一个新对象吗?
我们先卖个关子,不说答案,先看几个例子:
$ cat Test.java import java.util.function.Consumer; public class Test { public static void main(String[] args) { for (int i = 0; i < 3; i++) { test(i); } } static void test(int a) { forEach(b -> check(a + b)); } static void forEach(Consumer<Integer> c) { System.out.println(c); c.accept(1); } static void check(int i) { if (i < 0) { throw new RuntimeException(); } } } $ java Test.java Test$$Lambda$216/0x0000000800c93c40@f0f2775 Test$$Lambda$216/0x0000000800c93c40@5a4aa2f2 Test$$Lambda$216/0x0000000800c93c40@6591f517
由上可见,我们在调用forEach方法时,传入的参数是一个lambda表达式,forEach方法在执行前,会输出一下这个lambda表达式对应的对象。
通过上面的输出结果我们发现,三次输出的lambda表达式对应的对象的值均不同,由此可知,每次调用forEach方法时,都新建了一个该lambda表达式对应的对象。
我们再来看另外一个例子:
$ cat Test.java import java.util.function.Consumer; public class Test { public static void main(String[] args) { for (int i = 0; i < 3; i++) { test(i); } } static void test(int a) { forEach(Test::check); // 等同于forEach(b -> check(b)) } static void forEach(Consumer<Integer> c) { System.out.println(c); c.accept(1); } static void check(int i) { if (i < 0) { throw new RuntimeException(); } } } $ java Test.java Test$$Lambda$221/0x0000000800c94040@709ba3fb Test$$Lambda$221/0x0000000800c94040@709ba3fb Test$$Lambda$221/0x0000000800c94040@709ba3fb
这次这个例子和上个例子的区别是,传入forEach方法的lambda表达式里,没有再使用test方法的参数a,执行该示例后我们发现,三次输出的lambda表达式的对象结果都是一样的,这说明三次forEach执行使用都是同一个lambda对象。
也就是说,如果lambda表达式里使用了上下文中的其他变量,则每次lambda表达式的执行,都会创建一个新对象,而如果lambda表达式里没有使用上下文中的其他变量,则每次lambda的执行,都共用同一个对象,对吗?
在初次执行上面的两个示例后,看到执行结果,我就是这么猜测的,而在又一遍看过jvm中lambda相关实现代码后,也验证了我这个猜测是对的。
关键代码是下面这个方法:
// java.lang.invoke.InnerClassLambdaMetafactory CallSite buildCallSite() throws LambdaConversionException { final Class<?> innerClass = spinInnerClass(); if (invokedType.parameterCount() == 0 && !disableEagerInitialization) { // In the case of a non-capturing lambda, we optimize linkage by pre-computing a single instance, // unless we've suppressed eager initialization final Constructor<?>[] ctrs = innerClass.getDeclaredConstructors(); Object inst = ctrs[0].newInstance(); return new ConstantCallSite(MethodHandles.constant(samBase, inst)); } else { return new ConstantCallSite( MethodHandles.Lookup.IMPL_LOOKUP .findStatic(innerClass, NAME_FACTORY, invokedType)); } }
为了方便理解,我对该方法做了精简。
在该方法中,先调用spinInnerClass方法,为该lambda表达式生成一个java类,然后判断该lambda表达式有没有使用上下文中的其他变量,如果没有(invokedType.parameterCount() == 0),则直接创建一个该类的实例,并在以后每次执行该lambda表达式时,都使用这个实例。
如果使用了上下文中的其他变量,则每次执行lambda表达式时,都会调用innerClass里的一个名为NAME_FACTORY(get$Lambda)的静态方法,该方法会新建一个新的lambda实例。该过程在上一篇文章中有讲,这里就不再赘述了。
综上可知:
当lambda表达式里没有使用上下文中的其他变量时,则每次执行lambda表达式都使用同一个对象。
当lambda表达式里使用了上下文中的其他变量时,则每次执行lambda表达式都会新建一个对象。