Java 8系列之Lambda表达式
概述
使用Lambda表達(dá)式也有一段時間了,有時候用的云里霧里的,是該深入學(xué)習(xí)Java 8新特性的時候了。作為Java最大改變之一的Lambda表達(dá)式,其是Stream的使用基礎(chǔ),那就以它開始吧。
這里,我們先明確需要解決的問題:
Lambda表達(dá)式
lambda表達(dá)式的語法由參數(shù)列表、->和函數(shù)體組成。函數(shù)體既可以是一個表達(dá)式,也可以是一個語句塊:
- 表達(dá)式:表達(dá)式會被執(zhí)行然后返回執(zhí)行結(jié)果。
- 語句塊:語句塊中的語句會被依次執(zhí)行,就像方法中的語句一樣——?
- return語句會把控制權(quán)交給匿名方法的調(diào)用者
- break和continue只能在循環(huán)中使用
- 如果函數(shù)體有返回值,那么函數(shù)體內(nèi)部的每一條路徑都必須返回值
表達(dá)式函數(shù)體適合小型lambda表達(dá)式,它消除了return關(guān)鍵字,使得語法更加簡潔。
Lambda表達(dá)式的變體
不包含參數(shù)且主體為表達(dá)式
Lambda表達(dá)式不包含參數(shù),使用空括號 ()表示沒有參數(shù)。
OnClickListener mListener = () -> System.out.println("do on Click");該Lambda表達(dá)式實現(xiàn)了OnClickListener接口,該接口也只有一個doOnClick方法,沒有參數(shù),且返回類型為void。
public interface OnClickListener {void doOnClick(); }不包含參數(shù)且主體為代碼段
該Lambda表達(dá)式實現(xiàn)了OnClickListener接口,其主體為一段代碼段,在其內(nèi)用返回或拋出異常來退出。 只有一行代碼的Lambda表達(dá)式也可使用大括號, 用以明確Lambda表達(dá)式從何處開始、到哪里結(jié)束。
? ? OnClickListener mListener_ = () -> {System.out.println("插上電源");System.out.println("打開電視");};包含一個參數(shù)且主體為表達(dá)式
Lambda表達(dá)式可以包含一個參數(shù),將參數(shù)寫在()內(nèi),如果只有一個參數(shù)可以將()省略。
OnItemClickListener mItemListener = position -> System.out.println("position = [" + position + "]");該Lambda表達(dá)式實現(xiàn)了OnItemClickListener接口,該接口也只有一個doItemClickListener方法,其參數(shù)為int類型,且返回值為void。
public interface OnItemClickListener {void doItemClickListener(int position); }包含多個參數(shù)且主體為表達(dá)式
Lambda表達(dá)式可以包含多個參數(shù),將參數(shù)寫在()內(nèi),此時()不可以省略。
IMathListener mPlusListener = (x, y) -> x + y; int sum = mPlusListener.doMathOperator(10, 5);該Lambda表達(dá)式實現(xiàn)了IMathListener接口,該接口只有一個doMathOperator方法,其參數(shù)為(int, int)類型,且返回值為int類型。
public interface IMathListener {int doMathOperator(int start, int plusValue); }包含多個參數(shù)且主體為代碼段
該Lambda表達(dá)式實現(xiàn)了IMathListener接口,該接口只有一個doMathOperator方法,在實現(xiàn)其方法時,創(chuàng)建了一個函數(shù),用來處理結(jié)果。
? ? IMathListener mMaxListener = (x, y) -> {if (x > y) {return x;} else {return y;}};包含多個參數(shù),指定參數(shù)類型且主體為代碼段
該Lambda表達(dá)式實現(xiàn)了IMathListener接口,在實現(xiàn)時指定了參數(shù)類型,此時,調(diào)用時方法時的參數(shù)類型是指定的,只能傳入相應(yīng)的類型的參數(shù),若不傳入相應(yīng)參數(shù),編譯時會報錯。
? ? IMathListener mSubListener = (int x, int y) -> x - y;盡管與之前相比, Lambda表達(dá)式中的參數(shù)需要的樣板代碼很少,但是Java 8仍然是一種靜態(tài)類型語言。
引用值, 而不是變量
在使用內(nèi)部類時,我們總是碰到這種情況,需要引用內(nèi)部類外面的變量,比如其所在方法內(nèi)的變量,或者該類的全局變量。當(dāng)使用方法內(nèi)的變量時,需要將變量聲明為final。此時,將變量聲明為final, 意味著不能為其重復(fù)賦值,同時在匿名內(nèi)部,實際上是用的使用賦給該變量的一個特定的值。
final String name = getUserName(); button.addActionListener(new ActionListener() {public void actionPerformed(ActionEvent event) {System.out.println("hi " + name);} });在Java 8中對放松了這限制,在匿名內(nèi)部,可以引用其所在方法內(nèi)的非final變量,但是該變量在既成事實上必須是final,也就是說該變量只能賦值一次。如果再次對其賦值,編譯器會報錯。
現(xiàn)在,我們暫且將在匿名內(nèi)部類內(nèi)使用的其所在方法內(nèi)的變量命名為A,不管是在匿名類內(nèi)部還是在匿名類所在的方法內(nèi),再次對A進(jìn)行賦值時,編譯器都會報如下錯誤,其意思是變量A是在內(nèi)部類中訪問的,需要聲明為final或有效的final類型。Variable ‘plusFinal’ is accessed from within inner class, needs to be final or effectively final
在Lambda表達(dá)式中,也是同樣的問題,對于其方法體內(nèi)引用的外部變量,在Lambda表達(dá)式所在方法內(nèi)對變量再次賦值時,編譯器會報同樣的錯誤。也就是意味著,換句話說,Lambda表達(dá)式引用的是值,而不是變量。
這種行為也解釋了為什么Lambda表達(dá)式也被稱為閉包。未賦值的變量與周邊環(huán)境隔離起來,進(jìn)而被綁定到一個特定的值。在Java 8中引入了閉包這一概念,并將其使用在了Lambda表達(dá)式中。眾說紛紜的計算機編程語言圈子里,Java是否擁有真正的閉包一直備受爭議,因為在 Java 中只能引用既成事實上的final變量。可以肯定的是,Lambda表達(dá)式都是靜態(tài)類型。
閉包在現(xiàn)在的很多流行的語言中都存在,例如 C++、C# 。閉包允許我們創(chuàng)建函數(shù)指針,并把它們作為參數(shù)傳遞。
函數(shù)接口
函數(shù)式接口是什么呢?函數(shù)式接口(Functional Interface)是Java 8對一類特殊類型的接口的稱呼。這類接口只定義了唯一的抽象方法的接口(除了隱含的Object對象的公共方法),用作Lambda表達(dá)式的類型。
從函數(shù)接口的定義可以看出,首先要明確的,其是一個接口,而這個接口呢,有且只有一個抽象的方法,那怎么又和函數(shù)結(jié)合在一起了呢?
public interface IMathListener {int doMathOperator(int start, int plusValue); }我們先看一個例子,對于IMathListener接口,這個接口只有一個抽象方法doMathOperator,其接收兩個int類型的參數(shù),返回值為int,這個接口可以稱為是一個函數(shù)接口。當(dāng)我們聲明其對象時,我們可以這樣做:
IMathListener mSubListener = (x, y) -> x - y; mMaxListener.doMathOperator(10, 5));// 其值:5剛才的聲明,就是用Lambda表達(dá)式聲明了IMathListener的實現(xiàn),其實現(xiàn)的意義是求兩個傳入值的差值。這個例子說明了,函數(shù)接口可以通過Lambda表達(dá)式來實現(xiàn)。下面來看它是如何和函數(shù)扯上關(guān)系的。
public class Math {public static int doIntPlus(int start, int plusValue) {return start + plusValue;} }現(xiàn)有一個Math類,其內(nèi)聲明了一個靜態(tài)方法doIntPlus,該方法接收兩個int類型的參數(shù),返回值為int,也就是說doIntPlus與IMathListener接口中的doMathOperator方法的簽名一樣。既然簽名一樣,我們可以搞些什么事情呢。往下看:
IMathListener mPlusListener = Math::doIntPlus;我們通過函數(shù)調(diào)用,直接生成了一個IMathListener對象,這里寫法不了解的,后續(xù)會做介紹,看下Java 8中的引用。我們還是接著說,通過方法引用來支持Lambda表達(dá)式。這樣現(xiàn)有函數(shù)、接口及Lambda表達(dá)式完美的結(jié)合在一起。
從前面已經(jīng)知道,Lambda表達(dá)式都是靜態(tài)類型的,也就是說其在編譯時就已經(jīng)被編譯,所以剛才被引用的方法必須是靜態(tài)的,否則編譯器會報錯。
Non-static method cannot be referenced from a static context
非靜態(tài)方法不能從靜態(tài)上下文引用
對于函數(shù)接口而言,接口中唯一方法的命名并不重要了,只要方法簽名和Lambda表達(dá)式的類別相匹配即可。當(dāng)然了,為了增加代碼的易讀性,只需在函數(shù)接口中為參數(shù)起一個代表意義的名字即可。
為了更形象的聲明接口,我們可以使用圖形來描述不同類型接口。指向函數(shù)接口的箭頭表示參數(shù), 如果箭頭從函數(shù)接口射出, 則表示方法的返回類型。若接口沒有返回值,沒有箭頭從函數(shù)接口射出。?
這里,我們應(yīng)該對函數(shù)接口有了清晰的認(rèn)識。對于一個函數(shù)接口而言,其應(yīng)該有以下特性:
- 只具有一個方法的接口
- 其可以被隱式轉(zhuǎn)換為lambda表達(dá)式
- 現(xiàn)有靜態(tài)方法可以支持lambda表達(dá)式
- 每個用作函數(shù)接口的接口都應(yīng)添加 @FunctionalInterface注解
該注解會強制 javac 檢查一個接口是否符合函數(shù)接口的標(biāo)準(zhǔn)。 如果該注解添加給一個枚舉?
類型、 類或另一個注解, 或者接口包含不止一個抽象方法, javac 就會報錯。 重構(gòu)代碼時,?
使用它能很容易發(fā)現(xiàn)問題。
類型推斷
關(guān)于類型推斷,我們在Java 7中,已經(jīng)不止一次用到了,可能你一直都沒有注意到。比如創(chuàng)建一個ArrayList,我們可以這么做:
ArrayList<String> mArrayA = new ArrayList<String>(); ArrayList<String> mArrayB = new ArrayList<>();在創(chuàng)建mArrayA時,明確指定了ArrayList為String類型,而在創(chuàng)建mArrayB時并未指定ArrayList的類型,編譯器是如何知道m(xù)ArrayB的數(shù)據(jù)類型呢?在Java 7中,有個神奇的<>操作符,它可使javac推斷出泛型參數(shù)的類型,這樣不用明確聲明泛型類型,編譯器就可以自己推斷出來,這就是它的神奇之處!
對于一個傳遞的參數(shù),編輯器也可以根據(jù)參數(shù)的類型來推斷具體傳入的參數(shù)的數(shù)據(jù)類型。比如有一個方法updateList,其參數(shù)為一個String的ArrayList,在調(diào)用該方法時,我們傳入了一個新建的ArrayList但未指定ArrayList的數(shù)據(jù)類型,此時編輯器會自行推斷傳入的ArrayList的數(shù)據(jù)類型為String,
public void updateList(ArrayList<String> values);updateList(new ArrayList<>());Lambda表達(dá)式中的類型推斷,實際上是Java 7就引入的目標(biāo)類型推斷的擴(kuò)展。javac根據(jù)Lambda 表達(dá)式上下文信息就能推斷出參數(shù)的正確類型。 程序依然要經(jīng)過類型檢查來保證運行的安全性, 但不用再顯式聲明類型罷了,這就是所謂的類型推斷。
目標(biāo)類型是指Lambda表達(dá)式所在上下文環(huán)境的類型。比如,將 Lambda 表達(dá)式賦值給一個局部變量,或傳遞給一個方法作為參數(shù),局部變量或方法參數(shù)的類型就是 Lambda 表達(dá)式的目標(biāo)類型
以之前提到的IMathListener為例,在下面表達(dá)式中,javac會自行將x和y推斷為int類型.
IMathListener mSubListener = (x, y) -> x - y;而在實際開發(fā)過程中,為了接口方法的通用性,一般都是使用泛型來指定參數(shù)的類型,比如Funtion接口,該接口接收一個F類型的參數(shù)并返回一個T類型的值。
Function<String, Integer> string2Integer = Integer::valueOf;?在這個實例中,javac可以推斷出接收的數(shù)據(jù)類型為String,返回類型為Integer。盡管類型推斷已經(jīng)相當(dāng)智能,但是其也不是無所不能的。在其自行推斷前,你需給出其推斷的標(biāo)注。比如下面的例子,javac并不能夠推斷出Function的具體數(shù)據(jù)類型:
Function string2Integer = Integer::valueOf;?上述代碼,編譯都不會通過,編譯器給出的報錯信息如下:?
Operator ‘& #x002B;’ cannot be applied to java.lang.Object, java.lang.Object.
大家都知道泛型的擦除原則,在編譯時,編譯器會擦除泛型的具體類型。從而,此時編譯器認(rèn)為參數(shù)和返回值都是java.lang.Object實例。這已經(jīng)偏離了我們的思想,就算編譯可以通過,也會造成后續(xù)邏輯的混亂,從而不知道該行代碼,到底在做什么。在使用泛型時,我們一定會指定泛型的具體的數(shù)據(jù)類型,以作為編譯器的類型推斷的標(biāo)準(zhǔn)。
方法重載帶來的煩惱
在Java中可以重載方法,造成多個方法有相同的方法名,但簽名卻不一樣,盡管這樣讓多態(tài)性展現(xiàn)的淋漓盡致,但是對于類型推斷,帶來了不少的煩惱,因為javac可能會推斷出多種類型。 這時, javac會挑出最具體的類型。比如方法overloadedMethod中,參數(shù)類型不同,返回值相同,這是一個典型的方法重載,在使用具體類型調(diào)用時,java可以根據(jù)具體類型來判斷,此時控制臺應(yīng)打印“String”。
overloadedMethod("abc");private void overloadedMethod(Object o) {System.out.print("Object"); } private void overloadedMethod(String s) {System.out.print("String"); }如果我們參數(shù)傳遞的是Lambda表達(dá)式呢?下面的表達(dá)式中,編譯器并不知道x和y的數(shù)據(jù)類型,也并未指定具體的類型,必然造成編譯異常。
overloadedMethod((x)->y);如果在Lambda表達(dá)式中指定返回值的數(shù)據(jù)類型,編譯器可以清晰的知道overloadedMethod的參數(shù)類型為String類型,根據(jù)具體的數(shù)據(jù)類型,從而調(diào)用overloadedMethod(String s) 方法,避免了類型推斷不明確的問題。
overloadedMethod((x)->(String)y);總而言之,Lambda表達(dá)式作為參數(shù)時,其類型由它的目標(biāo)類型推導(dǎo)得出,推導(dǎo)過程遵循如下規(guī)則:
- 如果只有一個可能的目標(biāo)類型,由相應(yīng)函數(shù)接口里的參數(shù)類型推導(dǎo)得出;
- 如果有多個可能的目標(biāo)類型,由最具體的類型推導(dǎo)得出;
- 如果有多個可能的目標(biāo)類型且最具體的類型不明確, 則需人為指定類型。
總結(jié)
Lambda是函數(shù)式編程的基礎(chǔ),而函數(shù)式編程是技術(shù)的發(fā)展方向。作為一個成熟的Java開發(fā)人員,學(xué)習(xí)新的編程技術(shù)那是必須的,也是值得花時間學(xué)習(xí)的。
大量的使用Lambda表達(dá)式,盡管避免了大量的使用匿名內(nèi)部類,提高了代碼的可讀性,可是對猿人們要求更高了,應(yīng)當(dāng)對相應(yīng)的接口或者框架有一定的熟悉程度,否則,看代碼就活在云里霧里了。這也是自我相逼提升的一種方式吧。
參考文檔
---------------------?
作者:行云間?
來源:CSDN?
原文:https://blog.csdn.net/io_field/article/details/54380200?
版權(quán)聲明:本文為博主原創(chuàng)文章,轉(zhuǎn)載請附上博文鏈接!
總結(jié)
以上是生活随笔為你收集整理的Java 8系列之Lambda表达式的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 也许,这样理解 HTTPS 更容易
- 下一篇: Java 8系列之Stream的强大工具