Java8 Lambda总结
什么是Lambda?
Lambda是一個匿名函數,我們可以把Lambda表達式理解為是一段可以傳遞的代碼(將代碼像參數一樣進行傳遞,稱為行為參數化)。Lambda允許把函數作為一個方法的參數(函數作為參數傳遞進方法中),要做到這一點就需要了解,什么是函數式接口,這里先不做介紹,等下一篇在講解。
首先先看一下lambda長什么樣?
正常寫法:
lambda寫法:
new Thread(() -> System.out.println("hello lambda") ).start();怎么樣?是不是感覺很簡潔,沒錯,這就是lambda的魅力,他可以讓你寫出來的代碼更簡單、更靈活。
Lambda怎么寫?
大家看一些上面的這個圖,這就是lambda的語法,一個lambda分為三部分:參數列表、操作符、lambda體。以下是lambda表達式的重要特征:
- 可選類型聲明:
不需要聲明參數類型,編譯器可以統一識別參數值。也就說(s) -> System.out.println(s)和 (String s) -> System.out.println(s)是一樣的編譯器會進行類型推斷所以不需要添加參數類型。 - 可選的參數圓括號:
一個參數無需定義圓括號,但多個參數需要定義圓括號。例如:s -> System.out.println(s) 一個參數不需要添加圓括號。
(x, y) -> Integer.compare(y, x) 兩個參數添加了圓括號,否則編譯器報錯。 - 可選的大括號:
如果主體包含了一個語句,就不需要使用大括號。
不需要大括號.
s -> System.out.println(s)需要大括號
(s) -> {if (s.equals("s")) {System.out.println(s);}};- 可選的返回關鍵字:
如果主體只有一個表達式返回值則編譯器會自動返回值,大括號需要指定明表達式返回了一個數值。
Lambda體不加{ }就不用寫return:
Comparator com = (x, y) -> Integer.compare(y, x);
復制代碼Lambda體加上{ }就需要添加return:
類型推斷
上面我們看到了一個lambda表達式應該怎么寫,但lambda中有一個重要特征是可選參數類型聲明,就是說不用寫參數的類型,那么為什么不用寫呢?它是怎么知道的參數類型呢?這就涉及到類型推斷了。
java8的泛型類型推斷改進:
支持通過方法上下文推斷泛型目標類型
支持在方法調用鏈路中,泛型類型推斷傳遞到最后一個方法
在上面的代碼中,ps的類型是List,所以ps.stream()的返回類型是Stream。map()方法接收一個類型為Function<T, R>的函數式接口,這里T的類型即是Stream元素的類型,也就是Person,而R的類型未知。由于在重載解析之后lambda表達式的目標類型仍然未知,我們就需要推導R的類型:通過對lambda表達式lambda進行類型檢查,我們發現lambda體返回String,因此R的類型是String,因而map()返回Stream。絕大多數情況下編譯器都能解析出正確的類型,但如果碰到無法解析的情況,我們則需要:
使用顯式lambda表達式(為參數p提供顯式類型)以提供額外的類型信息
把lambda表達式轉型為Function<Person, String>
為泛型參數R提供一個實際類型。( map(p -> p.getName()))
方法引用
方法引用是用來直接訪問類或者實例已經存在的方法或構造方法,提供了一種引用而不執行方法的方式。是一種更簡潔更易懂的Lambda表達式,當Lambda表達式中只是執行一個方法調用時,直接使用方法引用的形式可讀性更高一些。
方法引用使用 “ :: ” 操作符來表示,左邊是類名或實例名,右邊是方法名。
(注意:方法引用::右邊的方法名是不需要加()的,例:User::getName)
方法引用的幾種形式:
類 :: 靜態方法
類 :: 實例方法
對象 :: 實例方法
例如:
Consumer<String> consumer = (s) -> System.out.println(s);等同于:
Consumer<String> consumer = System.out::println;例如:
Function<String, Integer> stringToInteger = (String s) -> Integer.parseInt(s);等同于:
Function<String, Integer> stringToInteger = Integer::parseInt;例如:
BiPredicate<List<String>, String> contains = (list, element) -> list.contains(element);等同于:
BiPredicate<List<String>, String> contains = List::contains;注意:
Lambda體中調用方法的參數列表與返回值類型,要與函數式接口中抽象方法的函數列表和返回值類型保存一致
若Lambda參數列表中的第一個參數是實例方法的調用者,而第二個參數是實例方法的參數時,可以使用ClassName::method
構造器引用
語法格式:類名::new
例如:
Supplier supplier = ()->new User();
等同于:
Supplier supplier = User::new;
復制代碼注意:
需要調用的構造器方法與函數式接口中抽象方法的參數列表保持一致。
Lambda是怎么實現的?
研究了半天Lambda怎么寫,可是它的原理是什么?我們簡單看個例子,看看真相到底是什么:
上面的代碼自定義了一個函數式接口,定義一個靜態方法然后用這個函數式接口來接收參數。編寫完這個類以后,我們到終端界面javac進行編譯,然后用javap(javap是jdk自帶的反解析工具。它的作用就是根據class字節碼文件,反解析出當前類對應的code區(匯編指令)、本地變量表、異常表和代碼行偏移量映射表、常量池等等信息。)進行解析,如下圖:
執行javap -p 命令 ( -p -private 顯示所有類和成員)
看上圖發現在編譯Lambda表達式生成了一個lambda$main$0靜態方法,這個靜態方法實現了Lambda表達式的邏輯,現在我們知道原來Lambda表達式被編譯成了一個靜態方法,那么這個靜態方式是怎么調用的呢?我們繼續進行
執行javap -v -p 命令 ( -v -verbose 輸出附加信息)
public com.lxs.stream.StreamTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object.""😦)V
4: return
LineNumberTable:
line 7: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc #2 // String hello lambda
2: invokedynamic #3, 0 // InvokeDynamic #0:print:()Lcom/lxs/stream/Print;
7: invokestatic #4 // Method printString:(Ljava/lang/String;Lcom/lxs/stream/Print;)V
10: return
LineNumberTable:
line 10: 0
line 12: 10
public static void printString(java.lang.String, com.lxs.stream.Print<java.lang.String>);
descriptor: (Ljava/lang/String;Lcom/lxs/stream/Print;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: aload_1
1: aload_0
2: invokeinterface #5, 2 // InterfaceMethod com/lxs/stream/Print.print:(Ljava/lang/Object;)V
7: return
LineNumberTable:
line 15: 0
line 16: 7
Signature: #19 // (Ljava/lang/String;Lcom/lxs/stream/Print<Ljava/lang/String;>;)V
private static void lambda$mainKaTeX parse error: Expected 'EOF', got '#' at position 183: … getstatic #?6 …Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #27 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandlesKaTeX parse error: Expected 'EOF', got '#' at position 201: …guments: #?28 (Ljava/lang/…mainKaTeX parse error: Expected 'EOF', got '#' at position 31: …tring;)V #?30 (Ljava/lang/…Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #27 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandlesKaTeX parse error: Expected 'EOF', got '#' at position 201: …guments: #?28 (Ljava/lang/…mainKaTeX parse error: Expected 'EOF', got '#' at position 31: …tring;)V #?30 (Ljava/lang/…main$0作為參數傳了進去,我們來看metafactory 的方法里的實現代碼:
public static CallSite metafactory(MethodHandles.Lookup caller,
String invokedName,
MethodType invokedType,
MethodType samMethodType,
MethodHandle implMethod,
MethodType instantiatedMethodType)
throws LambdaConversionException {
AbstractValidatingLambdaMetafactory mf;
mf = new InnerClassLambdaMetafactory(caller, invokedType,
invokedName, samMethodType,
implMethod, instantiatedMethodType,
false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
mf.validateMetafactoryArgs();
return mf.buildCallSite();
}
復制代碼在buildCallSite的函數中,是函數spinInnerClass 構建了這個內部類。也就是生成了一個StreamTest?Lambda$1.class這樣的內部類,這個類是在運行的時候構建的,并不會保存在磁盤中。
@Override
CallSite buildCallSite() throws LambdaConversionException {
final Class<?> innerClass = spinInnerClass();
以下省略。。。
}
復制代碼如果想看到這個構建的類,可以通過設置環境參數
System.setProperty(“jdk.internal.lambda.dumpProxyClasses”, " . ");
會在你指定的路徑 . 當前運行路徑上生成這個內部類。我們看下一下生成的類長什么樣
從圖中可以看出動態生成的內部類實現了我自定義的函數式接口,并且重寫了函數式接口中的方法。
我們在javap -v -p StreamTest?LambdaKaTeX parse error: Expected '}', got 'EOF' at end of input: …ream.StreamTest$Lambda$1();
descriptor: ()V
flags: ACC_PRIVATE
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #10 // Method java/lang/Object.""😦)V
4: return
public void print(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=2
0: aload_1
1: checkcast #15 // class java/lang/String
4: invokestatic #21 // Method com/lxs/stream/StreamTest.lambda$mainKaTeX parse error: Expected 'EOF', got '#' at position 84: …ions: 0: #?13() } 復制代碼發現在重…main0方法。總結:這樣實現了Lambda表達式,使用invokedynamic指令,運行時調用LambdaMetafactory.metafactory動態的生成內部類,實現了函數式接口,并在重寫函數式接口中的方法,在方法內調用lambda0方法。 總結: 這樣實現了Lambda表達式,使用invokedynamic指令,運行時調用LambdaMetafactory.metafactory動態的生成內部類,實現了函數式接口,并在重寫函數式接口中的方法,在方法內調用lambda0方法。總結:這樣實現了Lambda表達式,使用invokedynamic指令,運行時調用LambdaMetafactory.metafactory動態的生成內部類,實現了函數式接口,并在重寫函數式接口中的方法,在方法內調用lambdamain$0,內部類里的調用方法塊并不是動態生成的,只是在原class里已經編譯生成了一個靜態的方法,內部類只需要調用該靜態方法。
作者:倆右
鏈接:https://juejin.im/post/6844903890274484231
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
總結
以上是生活随笔為你收集整理的Java8 Lambda总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java IO(BIO, NIO, AI
- 下一篇: Java 流式编程stream