?
Java為什么需要lambda運算式?
能夠提升代碼簡潔性、提高代碼可讀性,
例如,在平時的開發程序中,把一個串列轉換成另一個串列或map等等這樣的轉換操作是一種常見需求,
在沒有lambda之前通常都是這樣實作的,
List<Long> idList = Arrays.asList(1L, 2L, 3L);
List<Person> personList = new ArrayList<>();
for (long id : idList) {
personList.add(getById(id));
}
代碼重復多了之后,大家就會對這種常見代碼進行抽象,形成一些類別庫便于復用,
上面的需求可以抽象成:對一個串列中的每個元素呼叫一個轉換函式轉換并輸出結果串列,
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));
Lambda運算式只是匿名內部類的語法糖嗎?
如果要在Java語言中實作lambda運算式,初步觀察,通過javac把這種箭頭語法還原成匿名內部類,就可以輕松實作,因為它們功能基本是等價的(IDEA中經常有提示),
但是匿名內部類有一些缺點,
- 每個匿名內部類都會在編譯時創建一個對應的class,并且是有檔案的,因此在運行時不可避免的會有加載、驗證、準備、決議、初始化的類加載程序,
- 每次呼叫都會創建一個這個匿名內部類class的實體物件,無論是有狀態的(capturing,從背景關系中捕獲一些變數)還是無狀態(non-capturing)的內部類,
invokedynamic介紹
如果有一種函式參考、指標就好了,但JVM中并沒有函式型別表示,
Java中有表示函式參考的物件嗎,反射中有個Method物件,但它的問題是性能問題,每次執行都會進行安全檢查,且引數都是Object型別,需要boxing等等,
還有其他表示函式參考的方法嗎?MethodHandle,它是在JDK7中與invokedynamic指令等一起提供的新特性,
但直接使用MethodHandle來實作,由于沒有簽名資訊,會遇不能多載的問題,并且MethodHandle的invoke方法性能不一定能保證比位元組碼呼叫好,
invokedynamic出現的背景
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
這幾種方法呼叫指令,在編譯的時候就已經明確指定了要呼叫什么樣的方法,且均需要接收一個明確的常量池中的方法的符號參考,并進行型別檢查,是不能隨便傳一個不滿足型別要求的物件來呼叫的,即使傳過來的型別中也恰好有一樣的方法簽名也不行,
invokedynamic功能
這個限制讓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),
鏈接程序通過呼叫一個boostrap method,傳入當前的呼叫相關資訊,bootstrap method會回傳一個CallSite,這個CallSite中包含了MethodHandle的參考,也就是CallSite的target,
invokedynamic指令便鏈接到這個CallSite上,并把所有的呼叫delegate到它當前的targetMethodHandle上,根據target是否需要變換,CallSite可以分為MutableCallSite、ConstantCallSite和VolatileCallSite等,可以通過切換target MethodHandle實作動態修改要呼叫的方法,
?
lambda運算式真正是如何實作的
下面直接看一下目前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
這里再復習一下invokedynamic的步驟,
- JVM第一次決議時,呼叫用戶定義的
bootstrap method bootstrap method會回傳一個CallSiteCallSite中能夠得到MethodHandle,表示方法指標- JVM之后呼叫這里就不再需要重新決議,直接系結到這個
CallSite上,呼叫對應的targetMethodHandle,并能夠進行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了,具體是實作就是回傳固定的內部類物件,或每次創建新內部類物件,
再次對比通過invokedynamic相對于直接匿名內部類語法糖的優勢
我們再想一下,Java8實作這一套騷操作的原因是什么, 既然lambda運算式又不需要什么動態分派(調動哪個方法是明確的), 為什么要用invokedynamic呢?
JVM虛擬機的一個基本保證就是低版本的class檔案也是能夠在高版本的JVM上運行的,并且JVM虛擬機通過版本升級,是在不斷優化和提升性能的,
直接轉換成內部類實作,固然簡單,但編譯后的二進制位元組碼(包括第三方jar包等)內容就固定了,實作固定為創建內部類物件+invoke{virtual, static, special, interface}呼叫,
未來提升性能只能靠提升創建類物件、invoke指令呼叫這幾個地方的優化,換個熟悉點的說法就是這里寫死了,
如果通過invokedynamic呢,javac編譯后把足夠的資訊保留了下來,在JVM執行時能夠動態決定如何實作lambda,也就能不斷優化lambda運算式的實作,并保持兼容性,給未來留下了更多可能,
總結
本文是我學習lambda的一些總結,介紹了lambda運算式出現的原因、實作方法以及不同實作思路上的對比, 對lambda知識也只是略看了一些代碼、資料,如有錯誤或不明確的地方還請大家無情指出,
?
微信公眾號【程式員黃小斜】作者是前螞蟻金服Java工程師,專注分享Java技術干貨和求職成長心得,不限于BAT面試,演算法、計算機基礎、資料庫、分布式、spring全家桶、微服務、高并發、JVM、Docker容器,ELK、大資料等,關注后回復【book】領取精選20本Java面試必備精品電子書,
?
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/380814.html
標籤:Java
