深入理解 Java Lambda
Java Lambda
Lambda
lambda名稱的由來 許多年前,在計算機出來之前,有位名叫 Alonzo Church 的邏輯學家,他想證明 什麼樣的數學函式是可以有效計算的(奇怪的是,當時已經存在了許多已知的函式, 但是沒有人知道怎樣去計算他們的值。)它使用希臘字母lambda (λ)來標記引數。 至於為什麼非要用λ 是因為《數學原理》中使用‘^’來表示自由變數,所以Church就用 大寫的 lambda Λ 來表示引數,但是最終還是換回了小寫。 於是從那時起,帶引數變數的表示式都被叫做lambda表示式。
Lambda
Java不是一種函式式語言,函式無法獨立的存在。可這帶來的問題就是Java過於的“重”。Lambda出現之前通常情況你的引數只能傳遞值而不能很方便的傳遞行為。
Java提供了匿名類來達到傳遞一組行為的效果,但是匿名內部類語法過於冗餘;匿名內部類中的this
和變數名容易讓人產生誤解;無法獲取非final
的區域性變數。
例:建立一個比較器
匿名類:
Comparator<Integer> comparator = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
};
Lambda:
Comparator<Integer> comparator = (i1, i2) -> i1 - i2;
++Lambda 表示式的結構++:
- 一個 Lambda 表示式可以有零個或多個引數
- 引數的型別既可以明確宣告,也可以根據上下文來推斷。例如:int a
與a
效果相同
- 所有引數需包含在圓括號內,引數之間用逗號相隔。例如:a, b
或 int a, int b
或 String a, int b, float c
- 空圓括號代表引數集為空。例如:() -> 42
- 當只有一個引數,且其型別可推導時,圓括號()
可省略。例如:a -> return a * a
{}
可省略。匿名函式的返回型別與該主體表達式一致
- 如果 Lambda 表示式的主體包含一條以上語句,則表示式必須包含在花括號{}
中(形成程式碼塊)。匿名函式的返回型別與程式碼- 塊的返回型別一致,若沒有返回則為空
函式式介面
匿名內部類有一個特點,每一個匿名類都對應一個介面。介面作為Java的一個特性,可以通過介面來作為和函式特性的連線。通過
@FunctionalInterface
來建立函式式介面可以達到一定的約束規範。再根據常用函式的特徵進行抽象可以避免不必要的介面建立。
Java8通過@FunctionalInterface
來建立函式式介面,當然如果符合潛在的標準(函式式介面只能有一個抽象方法),也可以不適用註解。在自己定義函式式介面的時候,可以加上這個註解來檢測是否和jdk已有的函式式介面重複,避免自己建立冗餘的介面。
Java8中對函式式介面的抽象都放在了 function
包中大致分為幾類:
- consumer
一個或多個入參,無返回型別
- supplier
無入參,返回物件或基礎型別
- function
一個或兩個入參,返回不同型別或不同的基礎型別
- operator
一個或兩個相同型別的入參,返回相同的型別
- predicate
一個或兩個入參,返回布林型
- run
無入參,無返回
這些函式式介面基本涵蓋了日常工作中大多數對函式式介面的需求。
Lambda為什麼可以做到型別推導?可以做到什麼程度?
Lambda 本身就是型別推導
當我們定義了類似(int a) -> "abc"
的lambda表示式後,系統自動為它推匯出型別。
編譯器負責推導lambda的型別,它利用上下文被期待的型別當做推導的目標型別,當滿足下面條件時,就會被賦予目標型別: - 被期待的目標型別是一個函式式介面 - lambda的入參型別和數量與該介面一致 - 返回型別一致 - 丟擲異常型別一致
(int a) -> "abc"
通常會被推導為 function
型別
剛才我們提到了目標物件的上下文,上下文環境一般為:++變數宣告++,++返回語句++,++賦值++,++方法的引數++,++強制轉換型別等++。
Lambda表示式具有顯式型別和隱式型別,顯式型別是由我們指定的型別,比如 Function f = (int a) -> "abc"
,隱式型別是由編譯器推導而來,對於返回型別不明確的情況,即存在二義性,則我們需要手動給出目標型別。
// Object o = () -> System.out.println("123"); 編譯器無法識別,提示語法錯誤
Object o = (Runnable) () -> System.out.println("123");
方法應用
方法應用使用
::
雙冒號,是lambda的簡化
- 靜態方法引用
Class::method
- 例項方法引用
instance::method
this::method
supper::method
- 構造方法引用
Class::new
String[]::new
int[]::new
方法引用會生成一個lambda表示式,構造方法引用必然生成的是 supplier
型別,其他情況則需要根據具體型別推導。
Lambda和匿名類是什麼關係?僅僅是語法糖? Java編譯器如何處理Lambda?
java虛擬機器裡呼叫方法的位元組碼指令有5種: - invokestatic //呼叫靜態方法 - invokespecial //呼叫私有方法、例項構造器方法、父類方法 - invokevirtual //呼叫例項方法 - invokeinterface //呼叫介面方法,會在執行時再確定一個實現此介面的物件 - ==invokedynamic== //先在執行時動態解析出呼叫點限定符所引用的方法,然後再執行該方法,在此之前的4條呼叫指令,分派邏輯是固化在java虛擬機器內部的,而invokedynamic指令的分派邏輯是由使用者所設定的引導方法決定的。
graph LR
a(indy)---b(call sit specifier);
b---c(bootstrap method);
c---d(callsit);
e(Method Handle)---d;
f(method)---e
==表面==
public class Illusory {
public void say(){
System.out.println("Hi");
}
}
==真實==
import java.lang.invoke.*;
public class Real {
private static void hello() {
System.out.println("Hello!");
}
public static CallSite bootstrap(MethodHandles.Lookup caller, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException {
MethodHandles.Lookup lookup = MethodHandles.lookup();
Class thisClass = lookup.lookupClass();
MethodHandle mh = lookup.findStatic(thisClass, "hello", MethodType.methodType(void.class));
return new ConstantCallSite(mh.asType(type));
}
}
==變化==
import org.objectweb.asm.*;
import java.lang.invoke.CallSite;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
//Opcodes接口裡定義了幾乎全部的java位元組碼命令
public class Change implements Opcodes {
public static void main(String[] args) throws Exception {
byte[] codes = dump();
Class<?> clazz = new MyClassLoader().defineClass("Illusory", codes);
clazz.getMethod("say").invoke(clazz.newInstance());
}
public static byte[] dump() throws Exception {
ClassWriter classWriter = new ClassWriter(0);
MethodVisitor visitor;
classWriter.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "Illusory", null, "java/lang/Object", null);
// 預設構造方法
visitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
visitor.visitCode();
visitor.visitVarInsn(ALOAD, 0);
visitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
visitor.visitInsn(RETURN);
visitor.visitMaxs(1, 1);
visitor.visitEnd();
// say 方法的改變
visitor = classWriter.visitMethod(ACC_PUBLIC, "say", "()V", null, null);
visitor.visitCode();
//---------Unknown world!--------------
// visitor.visitCode();
// visitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
// visitor.visitLdcInsn("Unknown world!");
// visitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
// visitor.visitInsn(RETURN);
// visitor.visitMaxs(2, 1);
//-----------------------
//----------Real world!-------------
MethodType methodType = MethodType.methodType(CallSite.class, MethodHandles.Lookup.class, String.class,
MethodType.class);
Handle bootstrap = new Handle(Opcodes.H_INVOKESTATIC, "Real", "bootstrap", methodType.toMethodDescriptorString(), false);
visitor.visitInvokeDynamicInsn("dynamicInvoke", "()V", bootstrap);
visitor.visitInsn(RETURN);
visitor.visitMaxs(0, 1);
//-----------------------
visitor.visitEnd();
classWriter.visitEnd();
return classWriter.toByteArray();
}
private static class MyClassLoader extends ClassLoader implements Opcodes {
public Class<?> defineClass(String name, byte[] b) {
return super.defineClass(name, b, 0, b.length);
}
}
}
現在我們結合我們得到的程式碼,再重新理解一下invokedynamic的定義:
先在執行時動態解析出呼叫點限定符所引用的方法, (即通過bootstrap方法動態解析出say方法)
然後再執行該方法,(即執行say方法)
而invokedynamic指令的分派邏輯是由使用者所設定的引導方法決定的。(這裡的引導方法,即我們定義的bootstrap方法,這裡我們的邏輯是直接分派了Change方法,但是我們也可以寫一些邏輯,比如根據呼叫時候的引數型別來動態決定呼叫哪個方法)
現在我們已經自己實踐了invokedynamic命令的使用,但是我相信很多人還是不明白這個命令的意義所在,這要從語言的靜態型別和動態型別說起:
靜態型別就是每個變數在初始化的時候就要宣告唯一的型別並且不能改變。
動態型別就是說變數沒有固定型別,變數的型別取決於它裡面元素的型別。
java語言是靜態型別的。有人可能會提到泛型,java的泛型是擦除式的,也就是說雖然在編寫java原始碼時看起來好像不能確定變數型別,但是在java編譯為位元組碼的過程中,每一個變數都是有確定的型別的。
所以從java語言的角度,之前的4條方法呼叫指令是完全夠用的,但是要知道,jvm不只是跨平臺的,還是跨語言的,當有人在jvm上試圖開發動態型別語言的時候,問題就來了:
jvm大多數指令都是型別無關的,但是在方法呼叫的時候,卻不是這樣,每個方法呼叫在編譯階段就必須指明方法引數和返回值型別,但是動態型別語言的方法引數,直到執行時刻才能知道型別啊,因此jdk就做了這樣一個“補丁”:用invokedynamic呼叫方法的時候,會轉到bootstrap方法,在這個方法裡可以動態獲取引數型別,然後根據引數型別分派合適的方法作為CallSite(動態呼叫點),最後真實呼叫的就是CallSize裡的方法。如此便能在jvm上實現動態型別語言的方法呼叫了。
lambda最後會由編譯器生成static 方法在當前類中,利用了invokedynamic命令脫離了內部類實現的優化。
例:
public class LambdaTest {
public static void main(String[] args) {
List<String> list = Arrays.asList("a","b");
list.forEach(System.out::println);
}
}
public class LambdaTest {
public LambdaTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_2
1: anewarray #2 // class java/lang/String
4: dup
5: iconst_0
6: ldc #3 // String a
8: aastore
9: dup
10: iconst_1
11: ldc #4 // String b
13: aastore
14: invokestatic #5 // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
17: astore_1
18: aload_1
19: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
22: dup
23: invokevirtual #7 // Method java/lang/Object.getClass:()Ljava/lang/Class;
26: pop
27: invokedynamic #8, 0 // InvokeDynamic #0:accept:(Ljava/io/PrintStream;)Ljava/util/function/Consumer;
32: invokeinterface #9, 2 // InterfaceMethod java/util/List.forEach:(Ljava/util/function/Consumer;)V
37: return
}