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