【版權申明】非商業目的註明出處可自由轉載
博文地址:
出自:shusheng007
文章目錄
概述
今天在Pluralsight看了一個講Java Lambda 表達式的視頻教程,覺得很好,自己研究並記錄分享一下以饗讀者。
因爲Java8已經出來好久了,Lambda已經被大量使用了,所以這裏只是分享一下對其的思考和總結,不準備過多講解其用法,目的是使我們對其有更加深刻的理解。
匿名類到Lambda表達式
我們知道,只有函數接口才可以使用Lambda表達式。
函數接口:只有一個abstract的方法的接口
那我們怎麼將實現了函數接口的匿名類轉換成Lambda表達式呢?我們以code來說話:
示例1,抽象方法無入參,無返回值
我們以Runnable函數接口爲例
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
匿名類:
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("hello thread");
}
};
new Thread(runable).start();
Lambda 表達式
- 將方抽象方法括號及其中參數拷貝出來並加上
->
()->
- 將抽象方法方法體拷貝出來,如果其中只有一句代碼則
{}
可以省略
()->{ System.out.println("hello thread");}
省略括號後
()->System.out.println("hello thread")
new Thread(() -> System.out.println("hello thread")).start();
示例2,抽象方法帶入參,帶返回值的情形
例如用於比較的Comparator函數接口
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
...
}
匿名類
Comparator<Integer> comparator = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o2, o1);
}
};
轉換爲Lambda表達式
- 將方抽象方法括號及其中參數拷貝出來並加上
->
(Integer o1, Integer o2) ->
- 將抽象方法方法體拷貝出來放在
->
後面(Integer o1, Integer o2) ->`{ return Integer.compare(o2, o1); }
- 簡化
參數類型可以省略,如果方法體只有一句代碼則{}
和return
關鍵字可以省略(o1, o2)-> Integer.compare(o2, o1)
不是說越簡單越好,如果你發現帶上類型可讀性更好,那就帶上,完全沒有問題。
不知道你們平時在開發中是否遇到過特別複雜函數接口,手寫lambda表達式變得很困難,不好理解。此時反而利用IDE的提示功能直接new匿名類對象反而更方便,然後寫完了再使用IDE一鍵將其轉換爲Lambda表達式。
Method reference(方法引用)
Lambda的一種簡寫方式,無他。在可以使用Lambda的地方都可以使用Method Reference, 反之亦然。唯一決定你使用哪種方式的是可讀性,哪個可讀性高就使用哪個。
其語法爲
類名::方法名
其中方法既可以是類方法,也可以是實例方法。
//方法引用
stream.forEach(System.out::println);
//Lambda表達式
stream.forEach(x->System.out.println(x));
靈魂拷問
Lambda 有類型嗎?有的話是什麼類型?
Lambda可以賦值給變量嗎?可以當方法的入參和返回值嗎?
Lambda是對象嗎?有的話我們可以在代碼中引用它嗎?
第一問:
Java是強類型語言,在Java中任何事物都有類型,Lambda也不例外,它的類型就是其對應的函數接口。
第二問:
Lambda可以賦值給變量,而且可以作爲方法的參數及返回值
第三問:
Lambda是個對象嗎?這塊情況比較就比較複雜了,我只能將自己的調查和理解呈上,至於更深刻的機制需要你去研究。不過我這裏說的,對於普通程序員已經足夠了,我個人覺得這部分很有意思,如果你也想知道Java編譯器到底如何處理Lambda表達式這種語法糖的話,應該接着往下看。
首先我們來看一段代碼
package top.ss007;
...
public class Student {
Runnable runnable = () -> {
};
Comparator<Integer> comparator = (o1, o2) -> 1;
Runnable runnableAnon = new Runnable() {
@Override
public void run() {
}
};
}
定義了一個Student類,裏面聲明瞭3個field,兩個賦值爲Lambda表達式,一個賦值爲匿名內部類對象。
我們使用Java編譯器javac
將其編譯爲class文件
javac -g Student.java
-g 保留所有調試信息
執行上述命令後生成了Student.class
和Student$1.class
兩個文件
然後我們使用字節碼查看器ByteCodeViewer查看一下這兩個文件,從命名上就可以看出Student$1.class
是Student類的內部類.
我們先看使用匿名內部類實現函數接口的部分:
Runnable runnableAnon = new Runnable() {
@Override
public void run() {
}
};
我們知道Javac 會爲其生成一個實現了Runnable接口的Student的內部類,這塊我們就不看字節碼了直接看生成的代碼類
class Student$1 implements Runnable {
// $FF: synthetic field
final Student this$0;
Student$1(Student this$0) {
this.this$0 = this$0;
}
public void run() {
}
}
可見,非靜態的內部類是會持有其包含類的一個引用的。
接下來看一下其具體的賦值字節碼
public Student() { // <init> //()V
<localVar:index=0 , name=this , desc=Ltop/ss007/Student;, sig=null, start=L1, end=L2>
L1 {
aload0 // reference to self
invokespecial java/lang/Object.<init>()V
}
...
L5 {
aload0 // reference to self
new top/ss007/Student$1
dup
aload0 // reference to self
invokespecial top/ss007/Student$1.<init>(Ltop/ss007/Student;)V
putfield top/ss007/Student.runnableAnon:java.lang.Runnable
return
}
}
上面是Student類的構造函數的字節碼
L1{} 裏面是調用Object的構造函數的代碼,又一次印證了Object是所有類型的基類。
L5{} 的代碼是我們要關注的,通過new top/ss007/Student$1
new 一個Student$1的對象,初始化後賦值給field runableAnon.
通過上面的調查我們已經清楚了javac是如何處理使用匿名內部類實現函數接口的方式,接下來讓我看一下Lambda表達式是如何處理的。
查看Student的字節碼文件可以發現如下兩段代碼
private static synthetic lambda$new$1(java.lang.Integer arg0, java.lang.Integer arg1) { //(Ljava/lang/Integer;Ljava/lang/Integer;)I
<localVar:index=0 , name=o1 , desc=Ljava/lang/Integer;, sig=null, start=L1, end=L2>
<localVar:index=1 , name=o2 , desc=Ljava/lang/Integer;, sig=null, start=L1, end=L2>
L1 {
iconst_1
ireturn
}
L2 {
}
}
private static synthetic lambda$new$0() { //()V
L1 {
return
}
}
其中有兩個使用了synthetic
的方法,說明這兩個方法是編譯器幫我們生成的。
lambda$new$0
對應的是
()->{}
lambda$new$1
對應的是
(o1, o2) -> 1;
賦值語句位於Student的構造函數中,如下所示:
public Student() { // <init> //()V
<localVar:index=0 , name=this , desc=Ltop/ss007/Student;, sig=null, start=L1, end=L2>
...
L3 {
aload0 // reference to self
invokedynamic 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; : run()Ljava/lang/Runnable; ()V top/ss007/Student.lambda$new$0()V (6) ()V
putfield top/ss007/Student.runnable:java.lang.Runnable
}
L4 {
aload0 // reference to self
invokedynamic 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; : compare()Ljava/util/Comparator; (Ljava/lang/Object;Ljava/lang/Object;)I top/ss007/Student.lambda$new$1(Ljava/lang/Integer;Ljava/lang/Integer;)I (6) (Ljava/lang/Integer;Ljava/lang/Integer;)I
putfield top/ss007/Student.comparator:java.util.Comparator
}
...
}
L3{} 塊內是
Runnable runnable = () -> { };
的處理代碼
L4{} 塊內是
Comparator<Integer> comparator = (o1, o2) -> 1;
的處理代碼
其中最爲關鍵的就是
invokedynamic 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; : run()Ljava/lang/Runnable; ()V top/ss007/Student.lambda$new$0()V (6) ()V
上面的代碼的作用是什麼呢?
總的來說,其是JVM在Runtime時創建() -> { }
對應的類,即在運行時創建一個實現了Runable
接口的類(字節碼)。
我們看到此處使用了invokedynamic
,只是爲了動態的生成其對應類的字節碼,但是Lambda方法的執行仍然使用的是 invokevirtual
或者invokeinterface
, 在首次調用時,生成對應類的字節碼,然後就緩存起來,下次使用緩存。
那麼是時候回答一下開頭的問題了:每一個Lambda表達式是否對應一個對象?
答案是:是的!但是,注意但是,每個Lambda是對應一個對象,但是有可能出現多對一的情況。
例如Lambda1,Lambda2 結構相同,那麼他們在JVM中就有可能對應同一個對象
總結
寫一篇不誤人子弟的文章實在是太費時間了,因爲你要不斷的確認自己寫的是不是真的可以實現,而不是自己的想當然。
技術真的是沒有止境,應儘早確定自己的方向,少年加油。