Lambda 表示式
簡介
相傳,在遠古時代,有一位邏輯學家某某,想要形式化的表示能有效計算的數學函式,由於別的書中使用重音符^來表示自由變數,某某受此啟發,使用大寫的lambda(∧)表示引數,後來又改成了小寫的lambda(λ),從此以後,帶引數變數的表示式就被稱為lambda表示式,讀音:lan b(m)da (蘭畝達)。
到了2015年,lambda表示式加入了JDK8,它顯著的增強了Java,在最近幾年中,lambda表示式已經成為了計算機語言設計的重點關注物件,幾年前的泛型重塑了Java,如今lambda表示式也正在重塑Java的程式設計風格。
簡而言之:lambda表示式,無論如何,就算天崩地裂,得了絕症,也得學!
言歸正傳
lambda表示式本質上就是一個匿名方法,但是這個方法不是獨立執行的,而是用於實現由函式式介面定義的另一個方法,因此lambda表示式會導致產生一個匿名類,也可以稱之為閉包。
語法結構
lambda表示式在Java語言中引入了一個新的語法元素和操作符,這個操作符是->,有時候被稱為lambda操作符或者箭頭操作符,它將lambda表示式分成兩個部分,左側指定了lambda表示式需要的所有引數(不需要引數則使用空括號),右側是表示式的主體。
下面看一個最簡單的lambda表示式:
()-> 12;
這個lambda表示式沒有引數,但是它有返回值,返回的是Int型別。
如果程式碼要完成的功能無法放在一個表示式中,就可以像寫方法一樣,把程式碼放在程式碼塊中,用大括號包起來。
()->{ for(int i=0;i<10;i++){ System.out.println(i); } }
這個lambda沒有返回值,也可以說它的返回值是void,如果有返回值的話,需要在表示式主體的最後使用return關鍵字返回指定型別的資料。
下面來看一個有引數的lambda表示式
(int n,int m)->n+m;
引數為int,返回值也是int型別。
不過一般來說我們這麼寫:
(n,m)->n+m;
如果只有一個入參,你甚至連括號都可以省:
n -> n+1;
省略了引數型別,因為引數型別是可以被自動推斷出來的。
函式式介面
函式式介面是僅包含一個抽象方法的介面,可以反過來這麼說,凡是隻包含一個抽象方法的介面,都可以叫做函式式介面。
為什麼要說函式式介面呢?因為lambda表示式的執行需要依賴函式式介面。
從JDK8開始,可以為介面的宣告的方法指定預設行為,就是所謂的預設方法(在介面中用default關鍵字宣告的方法,並且可以在介面中直接實現該方法,使得實現該介面的類不需要實現該方法,就如繼承一樣直接呼叫),因為該預設方法沒有指定預設實現,所以它就是隱式的是抽象方法,沒有必要使用abstract修飾符,當然,如果願意的話,也可以加上abstract修飾符。
示例:
public interface Admin { String getName(); }
隨便寫的一個介面,只要這個介面中只有一個抽象方法,那麼這就是一個函式式介面。(注意措辭:只有一個抽象方法,並不是只有一個方法,因為還可以存在預設方法)
lambda表示式允許你直接以內聯的形式為函式式介面的抽象方法提供實現,也就是說,lambda表示式構成了一個函式式介面定義的抽象方法的實現,該函式式介面定義了它的目標型別。
下面示例lambda表示式的使用方法(使用到上面的Admin介面):
Admin admin = ()->"張三"; System.out.println(admin.getName());
列印結果為:張三
當目標型別上下文出現lambda表示式時,會自動建立實現了該函式式介面的一個類的例項,函式式介面宣告的抽象方法的行為由lambda表示式定義,當通過目標呼叫該方法時,就會執行lambda表示式,因此,lambda表示式提供了一種將程式碼片段轉換為物件的方法。
也就是說,也就是說,也就是說,重要的事情說三遍:如果我寫一個方法,引數是Admin型別,那麼我可以呼叫這個方法直接傳入lambda表示式即可,為什麼連說三遍呢?因為這是最流行的用法,也是lambda的風騷之處:傳遞行為。
比如:
private static void myName(Admin admin){ String name = admin.getName(); System.out.println(name); }
然後我可以這麼呼叫:myName(()->"張三");
但是本篇部落格剩下的例子幾乎不會這樣子寫,是因為程式碼多了不易讓人理解,因此儘量寫的直接一些。
當然,老土的辦法是用內部類來實現:
Admin admin = new Admin() { @Override public String getName() { return "張三"; } }; System.out.println(admin.getName());
型別檢查與型別推斷
為什麼下面這個程式碼不能編譯呢?
Object o = () -> System.out.println("張三");
因為lambda表示式上下文的目標型別必須是一個函式式介面,而Object並不是函式式介面
我下面這個介面是函式式介面
public interface User { String userInfo(String name,int age); }
那麼我這樣子可以嗎?
User user = () -> System.out.println("張三");
也是不行的,因為User介面的抽象方法是有入參也有返回值的,但是() -> System.out.println("張三")卻是一個沒有入參也沒有返回值的表示式。
所以說lambda表示式的入參和返回值必須要和函式式介面相容。
User函式式介面的入參是String和int,返回值是String,正確的用法應該是這樣的:
String name = "小紅"; int age = 19; User user = (String username,int userage) -> { return username+"今年"+userage+"歲了"; }; System.out.println(user.userInfo(name,age));
可以更簡潔一點嗎? 可以的。
Java編譯器會從上下文來推斷出用什麼函式式介面來配合lambda表示式,它也可以推斷出適合lambda表示式的簽名。
就像我們經常使用的菱形運算子一樣:
HashMap<String,Integer> map1 = new HashMap<String,Integer>(); HashMap<String,Integer> map2 = new HashMap<>();
因此省略引數型別也是可以的:
User user = (username,userage) -> { return username+"今年"+userage+"歲了"; };
有時候寫明引數型別更易讀,有時候省略引數型別更易讀,這個就是仁者見仁智者見智了。
還可以再簡潔一點嗎?當然可以。
注意這個lambda表示式的主體,並不是什麼複雜的計算流程,它僅僅只是一個普通的表示式。
所以可以不用塊表示式:
User<String> user2 = (username,userage) -> username+"今年"+userage+"歲了";
就這樣一行程式碼就完事,自帶隱式的return。
泛型函式式介面
lambda表示式的型別推斷是相當的智慧,不過,如果我寫這麼個函式式介面,泛型,試一試它還能不能智慧的起來?
public interface MyUser<T> { String userInfo(String name,T age); }
顯然不可能,編譯都過不了,因為它已經懵逼了,不知道你的引數到底是個什麼型別。
這時候,就需要在lambda表示式的目標型別上指定引數型別:
String name = "小芳"; int age1 = 17; double age2 = 17.5; MyUser<Integer> myUser = (n,a) -> n+a; String str1 = myUser.userInfo(name,age1); System.out.println(str1); MyUser<Double> myUser1 = (n,a) -> n+a; String str2 = myUser1.userInfo(name,age2); System.out.println(str2);
引用值,而不是變數
我們目前為止在lambda表示式主體中使用的變數都是傳進來的引數,在lambda表示式中,可以訪問外層作用域定義的變數,將其引用為當前表示式內的區域性變數,這叫做變數捕獲。
在這種情況下,lambda表示式只能使用final的區域性變數,也就是說,被lambda表示式捕獲的外層變數,都會自動變成實質上的final型別,final變數是指在第一次賦值以後,值不能再發生變化的變數。
示例:
String name = "小紅"; int age = 22; int status = 1; User user = (n,a) -> { //status ++; //不允許 return n+a+"狀態是"+status; }; //status ++; //不允許 String str = user.userInfo(name,age); System.out.println(str);
示例中的status變數被lambda表示式捕獲之後,在lambda表示式中不能修改,在外層也不能被修改,
實際上lambda在訪問外層變數時,訪問的是變數的副本,並不是原始變數。
換句話說,lambda表示式引用的是值,而不是變數。
方法引用
深入剖析方法引用
方法引用的基本思想是:如果一個lambda表示式代表的只是直接呼叫這個方法,那麼最好還是用名稱來呼叫它,而不是去描述如何呼叫它。
方法引用提供了一種引用而不執行的方式,這種特性與lambda表示式相關,因為它也需要由相容的函式式介面構成的目標型別上下文,執行的時候,方法引用也會建立函式式介面的一個例項。
當你需要使用方法引用時,目標引用放在分隔符::前,方法名稱放在後面,例如User::getName就是引用了User類中的getName()方法,請記住,不需要括號,因為你沒有實際呼叫這個方法,它其實就是(User a)->a.getName()的快捷寫法。
下面走一個例子:
函式式介面
public interface User { String userInfo(String name,int age); }
處理使用者資訊的類
public class MyInfo { static String name(String name,int age){ if(age < 18){ name = "少年人"+name; }else if(age > 18 && age< 28){ name = "青年人"+name; }else{ name = "老年人"+name; } return name+"年齡是"+age; } }
介面呼叫方法
public static String userThink(User user,String name,int age){ return user.userInfo(name,age); }
主函式
public static void main(String[] args) { String name = "小明"; int age = 15; String outStr = userThink(MyInfo::name,name,age); System.out.println(outStr); }
觀察可以得知 MyInfo::name進入userThink方法之後變成了User介面的一個例項,MyInfo類的name方法需要兩個引數,而引用的時候並沒有傳參,因此更加證實了方法引用並非方法呼叫。
如果我這麼改一下 你可能會恍然大悟:
User user = MyInfo::name; String info = user.userInfo(name,age); System.out.println(info);
原來引用的方法就是lambda表示式的主體,牢記這一點非常重要。
因此我們這麼下結論:如果lambda表示式的主體內容是呼叫一個方法,那麼就可以使用方法引用,當然,引用的方法必須與上下文所使用的函式式介面相相容。
下面介紹幾種方法引用的例子:
lambda表示式 | 等效的方法引用 |
() -> Thread.currentThread().dumpStack() | Thread.currentThread()::dumpStack |
(str,i) -> str.substring(i) | String::substring |
(String s) -> System.out.println(s) | System.out::println |
本節示例MyInfo中的name方法是一個靜態方法,如果不是靜態方法呢?
不是靜態方法的話,那麼就需要將整個類new出來,再進行該物件的方法引用。
還有一種情況,就如上面表格中的(str,i) -> str.substring(i)為什麼可以用方法引用String::substring來替代呢?
substring是靜態方法嗎? 不是的。
String類有new出來嗎? 也沒有。
原因只有一個:String例項是傳入lambda表示式的引數,因此str本身就是String的例項,擁有String的所有方法。
如果你要引用一個物件的方法,而這個物件本身是lambda的一個引數,那麼Java允許你直接引用。
建構函式引用
構造器引用與方法引用是一樣的,只不過方法名固定為new,例如:Admin::new是Admin構造器的一個引用,它就相當於 () -> new Admin(),注意,也就是說當前使用的函式式介面的返回型別必須相容Admin。
下面走一個示例(寫示例是最頭疼的事情了 汗):
一個學生類
public class Student { String name; int grade = 0; public Student(String name) { this.name = name; } public Student(String name, int grade) { this.name = name; this.grade = grade; } public String getName() { return name; } public int getGrade() { return grade; } }
函式式介面 返回型別是Student
public interface StudentInterface { Student getStudent(String name,int grade); }
程式碼這麼走:
StudentInterface studentInterface = Student::new; Student student = studentInterface.getStudent("小明",2); System.out.println(student.getName()+student.getGrade());
思考一下,Student類中有兩個建構函式,為什麼程式碼就逮著第二個建構函式走呢?
這就是lambda的型別推斷特性了,走哪個建構函式是根據上下文的函式式介面來決定的,StudentInterface介面的入參是兩個引數,一個String一個int,返回型別是Student,出入條件都符合第二個建構函式,所以它就會走第二個建構函式。
那如果我要走第一個建構函式該怎麼做呢?
很簡單,稍微改一下
public interface StudentInterface2 { Student getStudent(String name); }
用這個函式式介面去接收方法引用即可。
泛型中的方法引用
在泛型類或泛型方法中也可以使用方法引用,再來一個例子:
這個是要引用的泛型方法
public class MyInfo2 { static <T> String name(String name,T age){ return name+"年齡是"+age; } }
函式式介面
public interface MyUser<T> { String userInfo(String name,T age); }
呼叫
String name = "小明"; int age = 16; double a = 16.6; MyUser user = MyInfo2::name; String info = user.userInfo(name,age); System.out.println(info);
這裡傳int或double都是可以的。
如果要限制只能傳int呢?
那就這樣子
MyUser<Integer> user = MyInfo2::name; //這時傳double就不行了
請注意,它的原型實際上是這樣的:
MyUser<Integer> user = MyInfo2::<Integer>name;
但是由於存在型別推斷,所以::後面的型別指定是可以省略的。
內建的函式式介面
當我們設計自己的函式式介面時,可以用註解@FunctionalInterface來標記這個介面,這樣子這個介面就只能成為函式式介面,不允許再增加別的抽象方法,另外javadoc裡也會指出這是一個函式式介面。
當然,最好還是使用Java給我們內建的函式式介面,個人認為已經可以滿足大部分程式設計需要了,並且很多介面都有非抽象的方法可以使用。
以下列出常用的函式式介面:
函式式介面 | 返回型別 | 引數型別 | 抽象方法名 |
Runnable | void | 無 | run |
Supplier<T> | T | 無 | get |
Consumer<T> | void | T | accept |
BiConsumer<T,U> | void | T,U | accept |
Function<T,R> | R | T | apply |
BiFunction<T,U,R> | R | T,U | apply |
UnaryOperator<T> | T | T | apply |
BinaryOperator<T> | T | T,T | apply |
Predicate<T> | boolean | T | test |
BiPredicate<T,U> | boolean | T,U | test |
基本型別函式式介面
如 ToLongFunction IntToLongFunction IntConsumer 等等等等 介面名稱已經代表了他們的功能
有三四十個這樣的函式式介面,我這裡就不方便出來了,詳細請查閱java.util.function包。