您好,登錄后才能下訂單哦!
本篇文章給大家分享的是有關Java中Lambda表達式的實現原理是什么,小編覺得挺實用的,因此分享給大家學習,希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。
List<Long> idList = Arrays.asList(1L, 2L, 3L); List<Person> personList = new ArrayList<>(); for (long id : idList) { personList.add(getById(id)); }
代碼重復多了之后,大家就會對這種常見代碼進行抽象,形成一些類庫便于復用。需求可以抽象成:對列表中的每個元素調用一個轉換函數Function轉換并輸出結果列表。
interface Function { <T, R> R fun(T input); } <T, R> List<R> map(List<T> inputList, Function function) { List<R> mappedList = new ArrayList<>(); for (T t : inputList) { mappedList.add(function.fun(t)); } return mappedList; }
有了這個抽象組件方法,最開始的代碼便可以”簡化”成:
List<Long> idList = Arrays.asList(1L, 2L, 3L); List<Person> personList = map(idList, new Function<Long, Person>() { @Override public Person fun(Long input) { return getById(input); } });
因為Java語言中函數并不能作為參數傳遞到方法中,函數只能寄存在一個類中表示。為了能夠把函數作為參數傳遞到方法中,我們被迫使用了匿名內部類實現,需要加相當多的冗余代碼。
在一些支持函數式編程的語言(Functional Programming Language)中(例如Python, Scala, Kotlin等),函數是一等公民,函數可以成為參數傳遞以及作為返回值返回。例如在Kotlin中,上述的代碼可以縮減到很短,代碼只包含關鍵內容,沒有冗余信息。
val personList = idList.map { id -> getById(id) }
這樣的編寫效率差距也導致了一部分Java用戶流失到其他語言,不過最終終于在JDK8也提供了Lambda表達式能力,來支持這種函數傳遞。
List<Person> personList = map(idList, input -> getById(input));
如果要在Java語言中實現lambda表達式,初步觀察,通過javac把這種箭頭語法還原成匿名內部類,就可以輕松實現,因為它們功能基本是等價的(IDEA中經常有提示)。
1.每個匿名內部類都會在編譯時創建一個對應的class,并且是有文件的,因此在運行時不可避免的會有加載、驗證、準備、解析、初始化的類加載過程。
2.每次調用都會創建一個這個匿名內部類class的實例對象,無論是有狀態的(capturing,從上下文中捕獲一些變量)還是無狀態(non-capturing)的內部類。
如果有一種函數引用、指針就好了,但JVM中并沒有函數類型表示。
Java 中有表示函數引用的對象嗎,反射中有個Method對象,但它的問題是性能問題,每次執行都會進行安全檢查,且參數都是Object類型,需要boxing等等。還有其他表示函數引用的方法嗎?MethodHandle,在JDK7中與invokedynamic指令等一起提供的新特性。
直接使用MethodHandle來實現,由于沒有簽名信息,會遇不能重載的問題。并且MethodHandle的invoke方法性能不一定能保證比字節碼調用好。
JVM上動態語言(JRuby, Scala等),實現dynamic typing動態類型,是比較麻煩的。這里簡單解釋一下什么是dynamic typing,與其相對的是static typing靜態類型。
static typing: 所有變量的類型在編譯時都是確定的,并且會進行類型檢查。
dynamic typing: 變量的類型在編譯時不能確定,只能在運行時才能確定、檢查。
例如,如下動態語言的例子,a和b的類型都是未知的,因此a.append(b)這個方法是什么也是未知的。
def add(val a, val b) a.append(b)
而在Java中a和b的類型在編譯時就能確定。
SimpleString add(SimpleString a, SimpleString b) { return a.append(b); }
編譯后的字節碼如下,通過invokevirtual明確調用變量a的函數簽名為 (LSimpleString;)LSimpleString;的方法。
0: aload_1 1: aload_2 2: invokevirtual #2 // Method SimpleString.append:(LSimpleString;)LSimpleString; 5: areturn
關于方法調用的字節碼指令,JVM中提供了四種。
invokestatic - 調用靜態方法
invokeinterface - 調用接口方法
invokevirtual - 調用實例非接口方法的public方法
invokespecial - 其他的方法調用,private,constructor, super
這幾種方法調用指令,在編譯的時候就已經明確指定了要調用什么樣的方法,且均需要接收一個明確的常量池中的方法的符號引用,并進行類型檢查,是不能隨便傳一個不滿足類型要求的對象來調用的,即使傳過來的類型中也恰好有一樣的方法簽名也不行。
這個限制讓JVM上的動態語言實現者感到很艱難,只能暫時通過性能較差的反射等方式實現動態類型。
這說明在字節碼層面無法支持動態分派,該怎么辦呢,又用到了大家熟悉的”All problems in computer science can be solved by another level of indirection”了。
現動態分派,既然不能在編譯時決定,那么我們把這個決策推遲到運行時再決定,由用戶的自定義代碼告訴給JVM要執行什么方法。
在jdk7,Java提供了invokedynamic指令來解決這個問題,同時搭配的還有java.lang.invoke包。這個指令大部分用戶不太熟悉,因為不像invokestatic等指令,它在Java語言中并沒有和它相關的直接概念。
invokedynamic指令: 運行時JVM第一次到這里的時候會進行linkage,會調用用戶指定的bootstrap method來決定要執行什么方法,之后便不需要這個解析步驟。這個invokedynamic指令出現的地方也叫做dynamic call site。
Bootstrap Method: 用戶可以自己編寫的方法,實現自己的邏輯最終返回一個CallSite對象。
CallSite: 負責通過getTarget()方法返回MethodHandle
MethodHandle: MethodHandle表示的是要執行的方法的指針
invokedynamic在最開始時處于未鏈接(unlinked)狀態,這時這個指令并不知道要調用的目標方法是什么。
當JVM要第一次執行某個地方的invokedynamic指令的時候,invokedynamic必須先進行鏈接(linkage)。
鏈接過程通過調用一個bootstrap method,傳入當前的調用相關信息,bootstrap method會返回一個CallSite,這個CallSite中包含了MethodHandle的引用,也就是CallSite的target。
invokedynamic指令便鏈接到這個CallSite上,并把所有的調用delegate到它當前的targetMethodHandle上。根據target是否需要變換,CallSite可以分為MutableCallSite、ConstantCallSite和VolatileCallSite等,可以通過切換target MethodHandle實現動態修改要調用的方法。
下面直接看一下目前java實現lambda的方式 以下面的代碼為例
public class RunnableTest { void run() { Function<Integer, Integer> function = input -> input + 1; function.apply(1); } }
編譯后通過javap查看生成的字節碼
void run(); descriptor: ()V flags: Code: stack=2, locals=2, args_size=1 0: invokedynamic #2, 0 // InvokeDynamic #0:apply:()Ljava/util/function/Function; 5: astore_1 6: aload_1 7: iconst_1 8: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 11: invokeinterface #4, 2 // InterfaceMethod java/util/function/Function.apply:(Ljava/lang/Object;)Ljava/lang/Object; 16: pop 17: return LineNumberTable: line 12: 0 line 13: 6 line 14: 17 LocalVariableTable: Start Length Slot Name Signature 0 18 0 this Lcom/github/liuzhengyang/invokedyanmic/RunnableTest; 6 12 1 function Ljava/util/function/Function; LocalVariableTypeTable: Start Length Slot Name Signature 6 12 1 function Ljava/util/function/Function<Ljava/lang/Integer;Ljava/lang/Integer;>; private static java.lang.Integer lambda$run$0(java.lang.Integer); descriptor: (Ljava/lang/Integer;)Ljava/lang/Integer; flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokevirtual #5 // Method java/lang/Integer.intValue:()I 4: iconst_1 5: iadd 6: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 9: areturn LineNumberTable: line 12: 0 LocalVariableTable: Start Length Slot Name Signature 0 10 0 input Ljava/lang/Integer;
對應Function<Integer, Integer> function = input -> input + 1;這一行的字節碼為
0: invokedynamic #2, 0 // InvokeDynamic #0:apply:()Ljava/util/function/Function; 5: astore_1
JVM第一次解析時,調用用戶定義的bootstrap method
bootstrap method會返回一個CallSite
CallSite中能夠得到MethodHandle,表示方法指針
JVM之后調用這里就不再需要重新解析,直接綁定到這個CallSite上,調用對應的target MethodHandle,并能夠進行inline等調用優化
第一行invokedynamic后面有兩個參數,第二個0沒有意義固定為0 第一個參數是#2,指向的是常量池中類型為CONSTANT_InvokeDynamic_info的常量。
#2 = InvokeDynamic #0:#32 // #0:apply:()Ljava/util/function/Function;
這個常量對應的#0:#32中第二個#32表示的是這個invokedynamic指令對應的動態方法的名字和方法簽名(方法類型)
#32 = NameAndType #43:#44 // apply:()Ljava/util/function/Function;
第一個#0表示的是bootstrap method在BootstrapMethods表中的索引。在javap結果的最后看到是。
BootstrapMethods: 0: #28 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; Method arguments: #29 (Ljava/lang/Object;)Ljava/lang/Object; #30 invokestatic com/github/liuzhengyang/invokedyanmic/RunnableTest.lambda$run$0:(Ljava/lang/Integer;)Ljava/lang/Integer; #31 (Ljava/lang/Integer;)Ljava/lang/Integer;
再看下BootstrapMethods屬性對應JVM虛擬機規范里的說明。
BootstrapMethods_attribute { u2 attribute_name_index; u4 attribute_length; u2 num_bootstrap_methods; { u2 bootstrap_method_ref; u2 num_bootstrap_arguments; u2 bootstrap_arguments[num_bootstrap_arguments]; } bootstrap_methods[num_bootstrap_methods]; } bootstrap_method_ref The value of the bootstrap_method_ref item must be a valid index into the constant_pool table. The constant_pool entry at that index must be a CONSTANT_MethodHandle_info structure bootstrap_arguments[] Each entry in the bootstrap_arguments array must be a valid index into the constant_pool table. The constant_pool entry at that index must be a CONSTANT_String_info, CONSTANT_Class_info, CONSTANT_Integer_info, CONSTANT_Long_info, CONSTANT_Float_info, CONSTANT_Double_info, CONSTANT_MethodHandle_info, or CONSTANT_MethodType_info structure CONSTANT_MethodHandle_info The CONSTANT_MethodHandle_info structure is used to represent a method handle 這個BootstrapMethod屬性可以告訴invokedynamic指令需要的boostrap method的引用以及參數的數量和類型。 #28對應的是bootstrap_method_ref,為 #28 = MethodHandle #6:#40 // invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
按照JVM規范,BootstrapMethod接收3個標準參數和一些自定義參數,標準參數如下MethodHandles.$Lookup類型的caller參數,這個對象能夠通過類似反射的方式拿到在執行invokedynamic指令這個環境下能夠調動到的方法,比如其他類的private方法是調用不到的。
這個參數由JVM來入棧String類型的invokedName參數,表示invokedynamic要實現的方法的名字,在這里是apply,是lambda表達式實現的方法名,這個參數由JVM來入棧MethodType類型的invokedType參數,表示invokedynamic要實現的方法的類型,在這里是()Function,這個參數由JVM來入棧
#29,#30,#31是可選的自定義參數類型 #29 = MethodType #41 // (Ljava/lang/Object;)Ljava/lang/Object; #30 = MethodHandle #6:#42 // invokestatic com/github/liuzhengyang/invokedyanmic/RunnableTest.lambda$run$0:(Ljava/lang/Integer;)Ljava/lang/Integer; #31 = MethodType #21 // (Ljava/lang/Integer;)Ljava/lang/Integer;
通過java.lang.invoke.LambdaMetafactory#metafactory的代碼說明下
public static CallSite metafactory(MethodHandles.Lookup caller, String invokedName, MethodType invokedType, MethodType samMethodType, MethodHandle implMethod, MethodType instantiatedMethodType)
前面三個介紹過了,剩下幾個為
MethodType samMethodType: sam(SingleAbstractMethod)就是#29 = MethodType #41 // (Ljava/lang/Object;)Ljava/lang/Object;,表示要實現的方法對象的類型,不過它沒有泛型信息,(Ljava/lang/Object;)Ljava/lang/Object;
MethodHandle implMethod: 真正要執行的方法的位置,這里是com.github.liuzhengyang.invokedyanmic.Runnable.lambda$run$0(Integer)Integer/invokeStatic,這里是javac生成的一個對lambda解語法糖之后的方法,后面進行介紹
MethodType instantiatedMethodType: 和samMethod基本一樣,不過會包含泛型信息,(Ljava/lang/Integer;)Ljava/lang/Integer;
private static java.lang.Integer lambda$run$0(java.lang.Integer);這個方法是有javac把lambda表達式desugar解語法糖生成的方法,如果lambda表達式用到了上下文變量,則為有狀態的,這個表達式也叫做capturing-lambda,會把變量作為這個生成方法的參數傳進來,沒有狀態則為non-capturing。
另外如果使用的是java8的MethodReference,例如Main::run這種語法則說明有可以直接調用的方法,就不需要再生成一個中間方法。
繼續看5: astore_1這條指令,表示把當前操作數棧的對象引用保存到index為1的局部變量表中,即賦值給了function變量。
說明前面執行完invokedynamic #2, 0后,在操作數棧中插入了一個類型為Function的對象。
這里的過程需要繼續看一下LambdaMetafactory#metafactory的實現。
mf = new InnerClassLambdaMetafactory(caller, invokedType, invokedName, samMethodType, implMethod, instantiatedMethodType, false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY); mf.validateMetafactoryArgs(); return mf.buildCallSite();
創建了一個InnerClassLambdaMetafactory,然后調用buildCallSite返回CallSite
看一下InnerClassLambdaMetafactory是做什么的: Lambda metafactory implementation which dynamically creates an inner-class-like class per lambda callsite.
怎么回事!饒了一大圈還是創建了一個inner class!先不要慌,先看完,最后分析下和普通inner class的區別。
創建InnerClassLambdaMetafactory的過程大概是參數的一些賦值和初始化等,再看buildCallSite,這個復雜一些,方法描述說明為Build the CallSite. Generate a class file which implements the functional interface, define the class, if there are no parameters create an instance of the class which the CallSite will return, otherwise, generate handles which will call the class' constructor.
創建一個實現functional interface的的class文件,define這個class,如果是沒有參數non-capturing類型的創建一個類實例,CallSite可以固定返回這個實例,否則有狀態,CallSite每次都要通過構造函數來生成新對象。 這里相比普通的InnerClass,有一個內存優化,無狀態就使用一個對象。
方法實現的第一步是調用spinInnerClass(),通過ASM生成一個function interface的實現類字節碼并且進行類加載返回。
只保留關鍵代碼
cw.visit(CLASSFILE_VERSION, ACC_SUPER + ACC_FINAL + ACC_SYNTHETIC, lambdaClassName, null, JAVA_LANG_OBJECT, interfaces); for (int i = 0; i < argDescs.length; i++) { FieldVisitor fv = cw.visitField(ACC_PRIVATE + ACC_FINAL, argNames[i], argDescs[i], null, null); fv.visitEnd(); } generateConstructor(); if (invokedType.parameterCount() != 0) { generateFactory(); } // Forward the SAM method MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, samMethodName, samMethodType.toMethodDescriptorString(), null, null); mv.visitAnnotation("Ljava/lang/invoke/LambdaForm$Hidden;", true); new ForwardingMethodGenerator(mv).generate(samMethodType); byte[] classBytes = cw.toByteArray(); return UNSAFE.defineAnonymousClass(targetClass, classBytes, null);
生成方法為
聲明要實現的接口
創建保存參數用的各個字段
生成構造函數,如果有參數,則生成一個static Factory方法
實現function interface里的要實現的方法,forward到implMethodName上,也就是javac生成的方法或者MethodReference指向的方法
生成完畢,通過ClassWrite.toByteArray拿到class字節碼數組
通過UNSAFE.defineAnonymousClass(targetClass, classBytes, null) define這個內部類class。這里的defineAnonymousClass比較特殊,它創建出來的匿名類會掛載到targetClass這個宿主類上,然后可以用宿主類的類加載器加載這個類。但是不會但是并不會放到SystemDirectory里,SystemDirectory是類加載器對象+類名字到kclass地址的映射,沒有放到這個Directory里,就可以重復加載了,來方便實現一些動態語言的功能,并且能夠防止一些內存泄露情況。
這些比較抽象,直觀的看一下生成的結果 // $FF: synthetic class final class RunnableTest$$Lambda$1 implements Function { private RunnableTest$$Lambda$1() { } @Hidden public Object apply(Object var1) { return RunnableTest.lambda$run$0((Integer)var1); } } 如果有參數的情況呢,例如從外部類中使用了一個非靜態字段,并使用了一個外部局部變量 private int a; void run() { int b = 0; Function<Integer, Integer> function = input -> input + 1 + a + b; function.apply(1); } 對應的結果為 final class RunnableTest$$Lambda$1 implements Function { private final RunnableTest arg$1; private final int arg$2; private RunnableTest$$Lambda$1(RunnableTest var1, int var2) { this.arg$1 = var1; this.arg$2 = var2; } private static Function get$Lambda(RunnableTest var0, int var1) { return new RunnableTest$$Lambda$1(var0, var1); } @Hidden public Object apply(Object var1) { return this.arg$1.lambda$run$0(this.arg$2, (Integer)var1); } }
創建完inner class之后,就是生成需要的CallSite了。
如果沒有參數,則生成這個inner class的一個function interface對象示例,創建一個固定返回這個對象的MethodHandle,再包裝成ConstantCallSite返回。
如果有參數,則返回一個需要每次調用Factory方法產生function interface的對象實例的MethodHandle,包裝成ConstantCallSite返回。
這樣就完成了bootstrap的過程。invokedynamic鏈接完之后,后面的調用就直接調用到對應的MethodHandle了,具體是實現就是返回固定的內部類對象,或每次創建新內部類對象。
既然lambda表達式又不需要什么動態分派(調動哪個方法是明確的), 為什么要用invokedynamic呢?
JVM虛擬機的一個基本保證就是低版本的class文件也是能夠在高版本的JVM上運行的,并且JVM虛擬機通過版本升級,是在不斷優化和提升性能的。
直接轉換成內部類實現,固然簡單,但編譯后的二進制字節碼(包括第三方jar包等)內容就固定了,實現固定為創建內部類對象+invoke{virtual, static, special, interface}調用。
以上就是Java中Lambda表達式的實現原理是什么,小編相信有部分知識點可能是我們日常工作會見到或用到的。希望你能通過這篇文章學到更多知識。更多詳情敬請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。