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

