轉載請註明出處:https://zhuanlan.zhihu.com/p/20540175java
在介紹Lambda表達式以前,咱們先來看只有單個方法的Interface(一般咱們稱之爲回調接口):git
public interface OnClickListener { void onClick(View v); }
咱們是這樣使用它的:github
button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { v.setText("lalala"); } });
這種回調模式在各類框架中很是流行,可是像上面這樣的匿名內部類並非一個好的選擇,由於:數組
語法冗餘;框架
匿名內部類中的this指針和變量容易產生誤解;ide
沒法捕獲非final局部變量;函數
非靜態內部類默認持有外部類的引用,部分狀況下會致使外部類沒法被GC回收,致使內存泄露。this
使人高興的是Java8爲咱們帶來了Lambda,下面咱們看看利用Lambda如何實現上面的功能:指針
button.setOnClickListener(v -> v.setText("lalala"));
怎麼樣?!五行代碼用一行就搞定了!!!code
在這裏補充個概念
函數式接口
;前面提到的OnClickListener接口只有一個方法,Java中大多數回調接口都有這個特徵:好比Runnable和Comparator;咱們把這些只擁有一個方法的接口稱之爲函數式接口
。
匿名內部類最大的問題在於其冗餘的語法,好比前面的OnClickListener中五行代碼僅有一行是在執行任務。Lambda表達式是匿名方法,前面咱們也看到了它用極其輕量的語法解決了這一問題。
下面給你們看幾個Lambda表達式的例子:
(int x, int y) -> x + y //接收x和y兩個整形參數並返回他們的和 () -> 66 //不接收任何參數直接返回66 (String name) -> {System.out.println(name);} //接收一個字符串而後打印出來 (View view) -> {view.setText("lalala");} //接收一個View對象並調用setText方法
Lambda表達式語法由參數列表
、->
和函數體
組成。函數體既能夠是一個表達式也能夠是一個代碼塊。
__表達式__:表達式會被執行而後返回結果。它簡化掉了return
關鍵字。
__代碼塊__:顧名思義就是一坨代碼,和普通方法中的語句同樣。
<!--lambda常常出如今嵌套環境中,如做爲方法的參數:
Runnable runnable = () -> {doSomething();}; new Thread(runnable); //也能夠這樣寫 new Thread(() -> {doSomething();});-->
經過前面的例子咱們能夠看到,lambda表達式沒有名字,那咱們怎麼知道它的類型呢?答案是經過上下文推導而來的。例如,下面的表達式的類型是OnClickListener
OnClickListener listener = (View v) -> {v.setText("lalala");};
這就意味着一樣的lambda表達式在不一樣的上下文裏有不一樣的類型
Runnable runnable = () -> doSomething(); //這個表達式是Runnable類型的 Callback callback = () -> doSomething(); //這個表達式是Callback類型的
編譯器利用lambda表達式所在的上下文所期待的類型來推導表達式的類型,這個__被期待的類型__被稱爲目標類型
。lambda表達式只能出如今__目標類型__爲函數式接口
的上下文中。
Lambda表達式的類型和目標類型的方法簽名必須一致,編譯器會對此作檢查,一個lambda表達式要想賦值給目標類型T
則必須知足下面全部的條件:
T
是一個函數式接口
lambda表達式的參數必須和T
的方法參數在數量、類型和順序上一致(一一對應)
lambda表達式的返回值必須和T
的方法的返回值一致或者是它的子類
lambda表達式拋出的異常和T
的方法的異常一致或者是它的子類
因爲目標類型是知道lambda表達式的參數類型,因此咱們不必把已知的類型重複一遍。也就是說lambda表達式的參數類型能夠從目標類型獲取:
//編譯器能夠推導出s1和s2是String類型 Comparator<String> c = (s1, s2) -> s1.compareTo(s2); //當表達式的參數只有一個時括號也是能夠省略的 button.setOnClickListener(v -> v.setText("lalala"));
ps: Java7中的泛型方法和<>構造器也是經過目標類型來進行類型推導的,如:
List<Integer> intList = Collections.emptyList>(); List<String> strList = new ArrayList<>();
在內部類中使用變量名和this很是容易出錯。內部類經過繼承獲得的成員變量(包括來講object的)可能會把外部類的成員變量覆蓋掉,未作限制的this引用會指向內部類本身而非外部類。
而lambda表達式的語義就十分簡單:它不會從父類中繼承任何變量,也不用引入新的做用域。lambda表達式的參數及函數體裏面的變量和它外部環境的變量具備相同的語義(this關鍵字也是同樣)。
下面咱們舉個栗子吧!
public class HelloLambda { Runnable r1 = () -> System.out.println(this); Runnable r2 = () -> System.out.println(toString()); @Override public String toString() { return "Hello, lambda!"; } public static void main(String[] args) { new HelloLambda().r1.run(); new HelloLambda().r2.run(); } }
上面的代碼最終會打印兩個Hello, lambda!
,與之相相似的內部類則會打印出相似HelloLambda$1@32a890
和HelloLambda$1@6b32098
這種出乎意料的字符串。
總結:基於詞法做用域的理念,lambda表達式不能夠掩蓋任何其所在上下文的局部變量。
在Java7中,編譯器對內部類中引用的外部變量(即捕獲的變量)要求很是嚴格:若是捕獲的變量沒有被聲明爲final
就會產生一個編譯錯誤。可是在Java8中放寬了這一限制--對於lambda表達式和內部類,容許在其中捕獲那些符合有效只讀的局部變量(若是一個局部變量在初始化後從未被修改過,那麼它就是有效只讀)。
Runnable getRunnable(String name){ String hello = "hello"; return () -> System.out.println(hello+","+name); }
對於this
的引用以及經過this
對未限定字段的引用和未限定方法的調用本質上都屬於使用final
局部變量。包含此類引用的lambda表達式至關於捕獲了this
實例。在其餘狀況下,lambda對象不會保留任何對this
的應用。
這個特性對內存管理是極好的:要知道在java中一個非靜態內部類會默認持有外部類實例的強引用,這每每會形成內存泄露。而在lambda表達式中若是沒有捕獲外部類成員則不會保留對外部類實例的引用。
不過儘管Java8放寬了對捕獲變量的語法限制,但試圖修改捕獲變量的行爲是被禁止的,好比下面這個例子就是非法的:
int sum = 0; list.forEach(i -> {sum += i;});
爲何要禁止這種行爲呢?由於這樣的lambda表達式很容易引發race condition
lambda表達式不支持修改捕獲變量的另一個緣由是咱們可使用更好的方式來實現一樣的效果:使用規約(condition)。java.util.stream包提供了各類規約操做,關於Java8中的Stream API
咱們放到下一章介紹。
lambda表達式容許咱們定義一個匿名方法,並以函數式接口的方式使用它。Java8可以在已有的方法上實現一樣的特性。
方法引用和lambda表達式擁有相同的特性(他們都須要一個目標類型,而且須要被轉化爲函數式接口的實例),不過咱們不須要爲方法引用提供方法體,咱們能夠直接經過方法名引用已有方法。
如下面的代碼爲例,假設咱們要按照userName
排序
class User{ private String userName; public String getUserName() { return userName; } ... } List<User> users = new ArrayList<>(); Comparator<User> comparator = Comparator.comparing(u -> u.getUserName()); Collections.sort(users, comparator);
咱們能夠用方法引用替換上面的lambda表達式
Comparator<User> comparator = Comparator.comparing(User::getUserName);
這裏的User::getUserName
被看作是lambda表達式的簡寫形式。儘管方法引用不必定會把代碼變得更緊湊,但它擁有更明確的語義--若是咱們想要調用的方法擁有一個名字,那麼咱們就能夠經過方法名調用它。
<!--由於函數式接口的方法參數對應於隱式方法調用時的參數,因此被引用方法簽名能夠經過放寬類型,裝箱以及組織到參數數組中的方式對其參數進行操做,就像在調用實際方法同樣:
Consumer<Integer> b1 = System::exit; // void exit(int status) Consumer<String[]> b2 = Arrays:sort; // void sort(Object[] a) Consumer<String> b3 = MyProgram::main; // void main(String... args) Runnable r = Myprogram::mapToInt // void main(String... args)-->
方法引用有不少種,它們的語法以下:
靜態方法引用:ClassName::methodName
實例上的實例方法引用:instanceReference::methodName
超類上的實例方法引用:super::methodName
類型上的實例方法引用:ClassName::methodName
構造方法引用:Class::new
數組構造方法引用:TypeName[]::new
若是你們喜歡這一系列的文章,歡迎關注個人知乎專欄、GitHub、簡書博客。