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表達式都會新建一個對象。