一、什么是lambda表達式?
Lambda 是一個匿名函數,我們可以把 Lambda 表達式理解為是一段可以傳遞的代碼(將代碼像數據一樣進行傳遞)。可以寫出更簡潔、更靈活的代碼。作為一種更緊湊的代碼風格,使 Java的語言表達能力得到了提升。
匿名內部類的寫法:
public void demo1(){ Comparator<Integer> comparator = new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return Integer.compare(o1, o2); } }; Runnable runnable = new Runnable() { @Override public void run() {} }; }
這樣寫會發現一個問題,實現的方法是冗余的代碼,實際當中并沒有什么用處。我們看看Lambda的寫法。
Lambda表達式的寫法
public void demo2(){ Comparator<Integer> comparator = (x,y) -> Integer.compare(x, y); Runnable runnable = () -> System.out.println("lambda表達式"); }
我們會發現Lambda表達式的寫法更加的簡潔、靈活。它只關心參數和執行的功能(具體需要干什么,比如->后的Integer.compare(x, y))。
二、lambda表達式語法
lambda表達式的一般語法:
(Type1 param1, Type2 param2, ..., TypeN paramN) -> { statment1; statment2; //............. return statmentM; }
包含三個部分:參數列表,箭頭(->),以及一個表達式或語句塊。
1.一個括號內用逗號分隔的形式參數,參數是函數式接口里面方法的參數
2.一個箭頭符號:->
3.方法體,可以是表達式和代碼塊,方法體是函數式接口里面方法的實現,如果是代碼塊,則必須用{}來包裹起來,且需要一個return 返回值,但有個例外,若函數式接口里面方法返回值是void,則無需{}。
總體看起來像這樣:
(parameters) -> expression 或者 (parameters) -> { statements; }
上面的lambda表達式語法可以認為是最全的版本,寫起來還是稍稍有些繁瑣。別著急,下面陸續介紹一下lambda表達式的各種簡化版:
1. 參數類型省略–絕大多數情況,編譯器都可以從上下文環境中推斷出lambda表達式的參數類型。這樣lambda表達式就變成了:
(param1,param2, ..., paramN) -> { statment1; statment2; //............. return statmentM; }
2. 單參數語法:當lambda表達式的參數個數只有一個,可以省略小括號。lambda表達式簡寫為:
param1 -> { statment1; statment2; //............. return statmentM; }
3. 單語句寫法:當lambda表達式只包含一條語句時,可以省略大括號、return和語句結尾的分號。lambda表達式簡化為:
param1 -> statment
下面看幾個例子:
demo1:無參,無返回值,Lambda 體只需一條語句
Runnable runnable = () -> System.out.println("lamda表達式");
demo2:Lambda 只需要一個參數
Consumer<String> consumer=(x)->System.out.println(x);
demo3:Lambda 只需要一個參數時,參數的小括號可以省略
Consumer<String> consumer=x->System.out.println(x);
demo4:Lambda 需要兩個參數
Comparator<Integer> comparator = (x, y) -> Integer.compare(x, y);
demo5:當 Lambda 體只有一條語句時,return 與大括號可以省略
BinaryOperator<Integer> binaryOperator=(x,y)->(x+y);
demo6:數據類型可以省略,因為可由編譯器推斷得出,稱為“類型推斷”
BinaryOperator<Integer> bo=(x,y)->{ System.out.println("Lambda"); return x+y;};
類型推斷
上述 Lambda 表達式中的參數類型都是由編譯器推斷得出的。Lambda 表達式中無需指定類型,程序依然可以編譯,這是因為 javac 根據程序的上下文,在后臺推斷出了參數的類型。Lambda 表達式的類型依賴于上下文環境,是由編譯器推斷出來的。這就是所謂的 “類型推斷”。
三、lambda表達式的類型
我們都知道,Java是一種強類型語言。所有的方法參數都有類型,那么lambda表達式是一種什么類型呢?
View.OnClickListener listener = new View.OnClickListener() { @Override public void onClick(View v) { //... } }; button.setOnClickListener(listener);
如上所示,以往我們是通過使用單一方法的接口來代表一個方法并且重用它。
在lambda表達式中,仍使用的和之前一樣的形式。我們叫做函數式接口(functional interface)。如我們之前button的點擊響應事件使用的View.OnClickListener
就是一個函數式接口。
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { ... public interface OnClickListener { void onClick(View v); } ... }
那究竟什么樣的接口是函數式接口呢?
函數式接口是只有一個抽象方法的接口,用作表示lambda表達式的類型。 比如Java標準庫中的java.lang.Runnable和java.util.Comparator都是典型的函數式接口。java 8提供 @FunctionalInterface作為注解,這個注解是非必須的,只要接口符合函數式接口的標準(即只包含一個方法的接口),虛擬機會自動判斷,但最好在接口上使用注解@FunctionalInterface進行聲明,以免團隊的其他人員錯誤地往接口中添加新的方法。舉例如下:
@FunctionalInterface public interface Runnable { void run(); } public interface Callable<V> { V call() throws Exception; } public interface ActionListener { void actionPerformed(ActionEvent e); } public interface Comparator<T> { int compare(T o1, T o2); boolean equals(Object obj); }
注意最后這個Comparator接口。它里面聲明了兩個方法,貌似不符合函數接口的定義,但它的確是函數接口。這是因為equals方法是Object的,所有的接口都會聲明Object的public方法——雖然大多是隱式的。所以,Comparator顯式的聲明了equals不影響它依然是個函數接口。
Java中的lambda無法單獨出現,它需要一個函數式接口來盛放,lambda表達式方法體其實就是函數接口的實現。即Lambda表達式不能脫離目標類型存在,這個目標類型就是函數式接口,看下面的例子:
String []datas = new String[] {"peng","zhao","li"}; Comparator<String> comp = (v1,v2) -> Integer.compare(v1.length(), v2.length()); Arrays.sort(datas,comp); Stream.of(datas).forEach(param -> {System.out.println(param);});
Lambda表達式被賦值給了comp函數接口變量。
你可以用一個lambda表達式為一個函數接口賦值:
Runnable r1 = () -> {System.out.println("Hello Lambda!");};
然后再賦值給一個Object:
Object obj = r1;
但卻不能這樣干:
Object obj = () -> {System.out.println("Hello Lambda!");}; // ERROR! Object is not a functional interface!
必須顯式的轉型成一個函數接口才可以:
Object o = (Runnable) () -> { System.out.println("hi"); }; // correct
一個lambda表達式只有在轉型成一個函數接口后才能被當做Object使用。所以下面這句也不能編譯:
System.out.println( () -> {} ); //錯誤! 目標類型不明
必須先轉型:
System.out.println( (Runnable)() -> {} ); // 正確
假設你自己寫了一個函數接口,長的跟Runnable一模一樣:
@FunctionalInterface public interface MyRunnable { public void run(); }
那么
Runnable r1 = () -> {System.out.println("Hello Lambda!");};
MyRunnable2 r2 = () -> {System.out.println("Hello Lambda!");};
都是正確的寫法。這說明一個lambda表達式可以有多個目標類型(函數接口),只要函數匹配成功即可。但需注意一個lambda表達式必須至少有一個目標類型。
JDK預定義了很多函數接口以避免用戶重復定義。最典型的是Function:
@FunctionalInterface public interface Function<T, R> { R apply(T t); }
這個接口代表一個函數,接受一個T類型的參數,并返回一個R類型的返回值。另一個預定義函數接口叫做Consumer,跟Function的唯一不同是它沒有返回值。
@FunctionalInterface public interface Consumer<T> { void accept(T t); }
還有一個Predicate,用來判斷某項條件是否滿足。經常用來進行篩濾操作:
@FunctionalInterface public interface Predicate<T> { boolean test(T t); }
綜上所述,一個lambda表達式其實就是定義了一個匿名方法,只不過這個方法必須符合至少一個函數接口。
四、lambda表達式可使用的變量
先舉例:
@Test public void test1(){ //將為列表中的字符串添加前綴字符串 String waibu = "lambda :"; List<String> proStrs = Arrays.asList(new String[]{"Ni","Hao","Lambda"}); List<String>execStrs = proStrs.stream().map(chuandi -> { Long zidingyi = System.currentTimeMillis(); return waibu + chuandi + " -----:" + zidingyi; }).collect(Collectors.toList()); execStrs.forEach(System.out::println); }
輸出:
lambda :Ni -----:1498722594781 lambda :Hao -----:1498722594781 lambda :Lambda -----:1498722594781
變量waibu :外部變量
變量chuandi :傳遞變量
變量zidingyi :內部自定義變量
lambda表達式可以訪問給它傳遞的變量,訪問自己內部定義的變量,同時也能訪問它外部的變量。不過lambda表達式訪問外部變量有一個非常重要的限制:變量不可變(只是引用不可變,而不是真正的不可變)。
當在表達式內部修改waibu = waibu + " ";時,IDE就會提示你:
Local variable waibu defined in an enclosing scope must be final or effectively final
編譯時會報錯。因為變量waibu被lambda表達式引用,所以編譯器會隱式的把其當成final來處理。
以前Java的匿名內部類在訪問外部變量的時候,外部變量必須用final修飾。現在java8對這個限制做了優化,可以不用顯示使用final修飾,但是編譯器隱式當成final來處理。
五、lambda表達式作用域
總體來說,Lambda表達式的變量作用域與內部類非常相似,只是條件相對來說,放寬了些,以前內部類要想引用外部類的變量,必須像下面這樣
final String[] datas = new String[] { "peng", "Zhao", "li" }; new Thread(new Runnable() { @Override public void run() { System.out.println(datas); } }).start();
將變量聲明為final類型的,現在在Java 8中可以這樣寫代碼
String []datas = new String[] {"peng","Zhao","li"}; new Thread(new Runnable() { @Override public void run() { System.out.println(datas); } }).start();
也可以這樣寫:
new Thread(() -> System.out.println(datas)).start();
看了上面的兩段代碼,能夠發現一個顯著的不同,就是Java 8中內部類或者Lambda表達式對外部類變量的引用條件放松了,不要求強制的加上final關鍵字了,但是Java 8中要求這個變量是effectively final。What is effectively final?
Effectively final就是有效只讀變量,意思是這個變量可以不加final關鍵字,但是這個變量必須是只讀變量,即一旦定義后,在后面就不能再隨意修改,如下代碼會編譯出錯
String []datas = new String[] {"peng","Zhao","li"}; datas = null; new Thread(() -> System.out.println(datas)).start();
Java中內部類以及Lambda表達式中也不允許修改外部類中的變量,這是為了避免多線程情況下的race condition。
六、lambda表達式中的this概念
在lambda中,this不是指向lambda表達式產生的那個對象,而是聲明它的外部對象。
例如:
package com.demo; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class WhatThis { public void whatThis(){ //轉全小寫 List<String> proStrs = Arrays.asList(new String[]{"Ni","Hao","Lambda"}); List<String> execStrs = proStrs.stream().map(str -> { System.out.println(this.getClass().getName()); return str.toLowerCase(); }).collect(Collectors.toList()); execStrs.forEach(System.out::println); } public static void main(String[] args) { WhatThis wt = new WhatThis(); wt.whatThis(); } }
輸出:
com.wzg.test.WhatThis
com.wzg.test.WhatThis
com.wzg.test.WhatThis
ni
hao
lambda
文章列表