1. 程式人生 > 其它 >Java8特性詳解 lambda表示式(三):原理篇

Java8特性詳解 lambda表示式(三):原理篇

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中經常有提示)。

但是匿名內部類有一些缺點。

  1. 每個匿名內部類都會在編譯時建立一個對應的class,並且是有檔案的,因此在執行時不可避免的會有載入、驗證、準備、解析、初始化的類載入過程。
  2. 每次呼叫都會建立一個這個匿名內部類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語言中並沒有和它相關的直接概念。

關鍵的概念有如下幾個

  1. invokedynamic指令: 執行時JVM第一次到這裡的時候會進行linkage,會呼叫使用者指定的bootstrap method來決定要執行什麼方法,之後便不需要這個解析步驟。這個invokedynamic指令出現的地方也叫做dynamic call site
  2. Bootstrap Method: 使用者可以自己編寫的方法,實現自己的邏輯最終返回一個CallSite物件。
  3. CallSite: 負責通過getTarget()方法返回MethodHandle
  4. MethodHandle: MethodHandle表示的是要執行的方法的指標

再串聯起來梳理下

invokedynamic在最開始時處於未連結(unlinked)狀態,這時這個指令並不知道要呼叫的目標方法是什麼。
當JVM要第一次執行某個地方的invokedynamic指令的時候,invokedynamic必須先進行連結(linkage)。
連結過程通過呼叫一個boostrap method,傳入當前的呼叫相關資訊,bootstrap method會返回一個CallSite,這個CallSite中包含了MethodHandle的引用,也就是CallSite的target。
invokedynamic指令便連結到這個CallSite上,並把所有的呼叫delegate到它當前的targetMethodHandle上。根據target是否需要變換,CallSite可以分為MutableCallSiteConstantCallSiteVolatileCallSite等,可以通過切換targetMethodHandle實現動態修改要呼叫的方法。

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的步驟。

  1. JVM第一次解析時,呼叫使用者定義的bootstrap method
  2. bootstrap method會返回一個CallSite
  3. CallSite中能夠得到MethodHandle,表示方法指標
  4. 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個標準引數和一些自定義引數,標準引數如下

  1. MethodHandles.$Lookup型別的caller引數,這個物件能夠通過類似反射的方式拿到在執行invokedynamic指令這個環境下能夠調動到的方法,比如其他類的private方法是呼叫不到的。這個引數由JVM來入棧
  2. String型別的invokedName引數,表示invokedynamic要實現的方法的名字,在這裡是apply,是lambda表示式實現的方法名,這個引數由JVM來入棧
  3. 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);

生成方法為

  1. 宣告要實現的介面
  2. 建立儲存引數用的各個欄位
  3. 生成建構函式,如果有引數,則生成一個static Factory方法
  4. 實現function interface裡的要實現的方法,forward到implMethodName上,也就是javac生成的方法或者MethodReference指向的方法
  5. 生成完畢,通過ClassWrite.toByteArray拿到class位元組碼陣列
  6. 通過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面試必備精品電子書。