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表达式都会新建一个对象。