Java Lambda Expressions
java 萊姆達表示式是java8的新特性,java lambda表示式是java的邁向函數語言程式設計的第一步。因此,java的lambda表示式其實是一個方法,它可以不從屬於任何一個類。java表示式可以作為一個引數,就好像它是一個物件一樣。它可以在需求時被執行。
java的lambda表示式通常被用於實現簡單的事件監聽器或回撥函式,或者和javaStreamApi一起實現函數語言程式設計。
1.java Lambda 以及單一方法介面
函數語言程式設計通常被用於實現事件監聽器。java中的事件監聽器通常被定義為含有單一方法的介面,這裡有一個虛構的單一方法介面:
public interface StateChangeListener {
public void onStateChange(State oldState, State newState);
}
這個java介面定義了一個單一的方法,當狀態改變時,會呼叫這個方法。
在java7中,為了監聽狀態的改變,你就不得不實現這個介面,想象一下,你有一個類叫: StateOwner,它可以註冊狀態事件監聽器,這是一個例子:
public class StateOwner {
public void addStateListener(StateChangeListener listener) { ... }
}
在java7中,你可以使用匿名內部類去新增一個事件監聽器,就像這樣:
StateOwner stateOwner = new StateOwner();
stateOwner.addStateListener(new StateChangeListener() {
public void onStateChange(State oldState, State newState) {
// do something with the old and new state.
}
});
首先,建立一個StateOwner例項,然後把StateChangeListener介面的匿名實現類被作為一個監聽器新增到StateOwner例項中。
但是在java8中,你可以使用javaLambda表示式來新增一個事件監聽器,就像這樣:
StateOwner stateOwner = new StateOwner();
stateOwner.addStateListener((oldState, newState) -> System.out.println("State changed"));
lambda表示式就是下面的這一部分:
(oldState, newState) -> System.out.println("State changed")
我們要求lambda表示式要匹配addStateListener()方法的引數型別,如果lambda表示式匹配該引數型別(在這裡,即:StateChangeListener介面)。那麼,這個lambda表示式就會被轉化為一個實現了StateChangeListener介面的函式。
java的lambda表示式只適用於那些匹配單一方法介面的類。在上面的例子中,lambda表示式被當做引數使用,而引數型別是StateChangeListener介面。這個介面只有一個方法,因此該lambda表示式能夠成功地匹配StateChangeListener介面。
2.使lambda表示式和介面匹配matching lambdas to interface
一個單方法的介面有時也被稱為 函式式介面,把一個java表示式和一個函式式介面相匹配要分成下面三步:
.這個介面只有一個抽象方法嗎???
.lambda表示式的引數和方法的引數匹配嗎???
.lambda表示式的返回值型別和方法的返回值型別匹配嗎???
如果這三個問題的答案都是Yes的話,那麼這個給定的lambda表示式就能成功地匹配此介面。
3.含有預設和靜態方法的介面
從java8開始,一個java介面裡面可以同時包含預設方法和靜態方法。預設方法和靜態方法的實現可以直接在介面的宣告。這也就意味著,java的lambda表示式也可以適用於包含超過一個方法的介面-只要該介面裡只有一個未實現的方法即可。
換句話說,介面仍然是函式式介面,即使他包含預設函式和靜態函式,只要這個介面理由只有一個未實現的方法就行。
下面的這個介面,可以用一個lambda表示式實現:
public interface MyInterface {
void printIt(String text);
default public void printUtf8To(String text, OutputStream outputStream){
try {
outputStream.write(text.getBytes("UTF-8"));
} catch (IOException e) {
throw new RuntimeException("Error writing String as UTF-8 to OutputStream", e);
}
}
static void printItToSystemOut(String text){
System.out.println(text);
}
}
4.Lambda表示式 VS 匿名介面實現
雖然lambda表示式和匿名介面實現很相像,但是他們之間還是有些不同的。最大的區別是,一個匿名介面實現有狀態(成員變數),然而lambda表示式卻沒有。看下這個介面:
public interface MyEventConsumer {
public void consume(Object event);
}
這個介面可以使用一個匿名介面實現來實現,就像這樣:
MyEventConsumer consumer = new MyEventConsumer() {
public void consume(Object event){
System.out.println(event.toString() + " consumed");
}
};
這個匿名介面實現MyEventConsumer 可以有它自己的內部狀態,看下面這個:
MyEventConsumer myEventConsumer = new MyEventConsumer() {
private int eventCount = 0;
public void consume(Object event) {
System.out.println(event.toString() + " consumed " + this.eventCount++ + " times.");
}
};
可以看到MyEventConsumer的匿名實現現在已經有了一個欄位域eventCount。但是lambda表示式卻不能有這樣的域欄位。因此,lambda表示式又被說成是無狀態的。
5.Lambda表示式的型別推斷
在java8之前,如果你要實現一個介面實現,你不得不指定要實現哪個介面。這裡有一個匿名介面實現的例子:
stateOwner.addStateListener(new StateChangeListener() {
public void onStateChange(State oldState, State newState) {
// do something with the old and new state.
}
});
但是如果使用lambda表示式的話,它的型別可以根據附近的程式碼推到出來。例如,引數的介面型別可以從方法的宣告addStateListener()那裡推匯出來.這被稱為型別推斷。
編譯器會查詢這個引數所屬的型別-在這個例子中,就是方法定義。下面這個例子就可以看出,我們在lambda表示式中並未提及StateChangeListener介面:
stateOwner.addStateListener(
(oldState, newState) -> System.out.println("State changed")
);
在lambda表示式裡面,引數型別也能直接推斷出來。在上面這個例子中,編譯器就是根據onStateChange()方法的宣告來確定引數型別的。因此,lambda表示式中的oldState和newState的型別也可以從onStateChange()方法的宣告中推斷出來。
6.Lambda引數
由於java的lambda表示式實際上就是一個方法,所以,lambda表示式可以像方法一樣去接收引數。 上面例子中的lambda表示式中的(oldState,newState)就表明了lambda表示式要接收的引數。這些引數都得和介面中方法的引數保持一致。在這個例子中,這些引數必須要匹配StateChangeListener介面的onStateChange()方法中的引數。
public void onStateChange(State oldState, State newState);
7.零引數
如果和lambda表示式匹配的方法不接收任何引數,那你可以寫個像這樣的lambda表示式:
() -> System.out.println("Zero parameter lambda");
這就是表明該lambda表示式不接收任何引數。
8. 一個引數
如果匹配lambda表示式的方法只接收一個引數的話,你可以寫一個這樣的lambda表示式:
(param) -> System.out.println("One parameter: " + param);
可以注意到,引數被放在圓括號裡面。另外,當一個lambda表示式接收一個引數時,你可以省略圓括號,就像這樣:
param -> System.out.println("One parameter: " + param);
9.多個引數
如果lambda表示式匹配的方法接收多個引數的話,那麼引數需要放置到圓括號裡,就像下面的這樣:
(p1, p2) -> System.out.println("Multiple parameters: " + p1 + ", " + p2);
只有當方法只接收一個引數時,圓括號才能被省略。
10.引數型別
有時為lambda表示式指定引數型別是非常必要的(如果這個編譯器不能從介面方法中推斷出引數型別的話)。不要擔心,如果出現這種情況,編譯器會提示你。這裡有個lambda表示式引數型別的例子:
(Car car) -> System.out.println("The car is: " + car.getName());
正如你看到的,名為car的引數型別被寫在了引數名字的前面,就像你在方法裡面宣告瞭一個引數一樣。
11.Lambda函式體
lambda表示式體,也即是:它所表示的方法體被指定在了"->"的右邊,這裡就是一個例子:
(oldState, newState) -> System.out.println("State changed")
如果你的lambda表示式需要由多行構成的話,你可以把lambda表示式體放在{}中括號裡面包起來,這裡有個例子:
(oldState, newState) -> {
System.out.println("Old state: " + oldState);
System.out.println("New state: " + newState);
}
12.從Lambda表示式返回一個值
你可以從lambda表示式返回值,就像你可以從方法裡面獲取一個返回值一樣。你只需要在lambda函式體裡面增加一個return語句就可以啦。
(param) -> {
System.out.println("param: " + param);
return "return value";
}
在這個例子中,你lambda表示式做的事就是計算出一個返回值,然後把它返回,你也能用一個更簡短方式指定返回值:
除了這樣寫:
(a1, a2) -> { return a1 > a2; }
你也可以這樣寫:
(a1, a2) -> a1 > a2;
編譯器會計算出來 a1 > a2是一個lambda表示式的返回值。
13.變數捕捉
在某些情況下,lambda表示式也能夠訪問那些宣告在lambda函式體外面的變數。我這裡有一段程式碼:
Java 可以獲取以下三種型別的變數:
.區域性變數
.例項變數
.靜態變數
我們將在下個章節中詳細講述。
13.1本地區域性變數捕獲
java的lambda表示式可以獲取在lambda函式體外宣告的本地區域性變數的值。為了展示它,我們先看一個單一方法的介面:
public interface MyFactory {
public String create(char[] chars);
}
現在,我們看一下,lambda表示式如何實現這個介面的:
MyFactory myFactory = (chars) -> {
return new String(chars);
};
現在這個lambda表示式僅僅是引用了傳遞給它的引數值,但是我們也能換一種寫法,下面有一個更新版本:
String myString = "Test";
MyFactory myFactory = (chars) -> {
return myString + ":" + new String(chars);
};
正如你所看到的,在lambda表示式體重引用了本地區域性變數myString的值。
3.2 例項變數捕獲
lambda表示式也能使用lambda表示式所在物件的例項變數。下面有個例子:
public class EventConsumerImpl {
private String name = "MyConsumer";
public void attach(MyEventProducer eventProducer){
eventProducer.listen(e -> {
System.out.println(this.name);
});
}
}
可以注意到,對name的引用this.name位於lambda表示式的內部。這樣做的話,lambda表示式就能夠在EventConsumerImpl的內部訪問name例項量(域變數)。更甚之,在獲取到它的訪問之後,我們也能夠改變此例項變數的值,並且它的值也能被對映到lambda表示式的內部。
這裡的this,實際上是一種域,它和java的匿名介面實現有所不同。一個匿名介面實現也可以有它自己例項變數,並且可以通過this應用進行訪問。但是,對於lambda表示式來說,lambda表示式不能有自己的例項變數,因此,this總是指向當前物件。
注意: 以上的事件消費者的設計不是特別的優美簡潔,我那樣做僅僅是為了描述一個訪問例項變數的場景。
13.3 靜態變數捕捉
一個lambda表示式也能捕捉靜態變數。這並不吃驚,因為靜態變數在整個應用的內部都能訪問靜態變數(只要該靜態變數是可訪問的 )。
下面有一個例子:
public class EventConsumerImpl {
private static String someStaticVar = "Some text";
public void attach(MyEventProducer eventProducer){
eventProducer.listen(e -> {
System.out.println(someStaticVar);
});
}
}
靜態變數的值在被lambda表示式捕捉過之後,也允許修改。
14.方法引用作為lambda表示式
在上面的例子中,你的lambda表示式所做的事情就是 呼叫另一個方法並把引數傳遞給該lambda表示式。但是java的lambda表示式實現,提供了一種更簡單的方式去表示一個方法呼叫。
首先,這裡有一個單一方法的介面:
public interface MyPrinter{
public void print(String s);
}
緊接著,我們建立一個實現了MyPrinter介面的lambda表示式例項:
MyPrinter myPrinter = (s) -> { System.out.println(s); };
由於該lambda表示式只有一個單一的語句構成,所以實際上我們可以不用{}中括號來包裹。並且,由於這裡lambda表示式只有一個引數,所以我們
可以忽略掉引數的()標籤。就像這樣:
MyPrinter myPrinter=s -> System.out.println(s);
由於上面的lambda表示式做的事情就是把string引數轉寄給System.out.println()方法,所以我們可以用一個方法引用來替換上面的lambda表示式。
just looks:
MyPrinter myPrinter = System.out::println;
可以看到,倆個冒號:: ,這就是向編譯器表明,這是一個方法引用並且這個方法引用就是倆個冒號::之後的東西。不管擁有該引用方法的是類還是物件,它都是位於倆個冒號::之前的類或者物件。
你可以參考下面的幾種方法型別:
.靜態方法
.例項方法
.引數物件上的例項方法
.構造器
上面的每一種方法引用都會在下面的章節中講述。
14.1 靜態方法引用
最簡單的方法引用就是靜態方法引用。這是一個簡單的單函式介面:
public interface Finder {
public int find(String s1, String s2);
}
這有一個靜態方法:
public class MyClass{
public static int doFind(String s1, String s2){
return s1.lastIndexOf(s2);
}
}
下面我們就用一個lambda表示式來呼叫這個方法:
Finder finder = MyClass::doFind;
由於Finder.find()方法的引數和MyClass.doFind()方法匹配,所以就能建立一個實現Finder.find()方法的lambda表示式,同時 引用了MyClass.doFind()方法。
14.2 引數方法引用
14.3例項方法引用
第三點,我們也能從一個lambda表示式的定義處引用一個例項方法,首先,先看一個單方法介面定義:
public interface Deserializer {
public int deserialize(String v1);
}
這個介面代表了一個能把一個String序列化為一個int的元件。
現在,看看這個StringConverter類:
public class StringConverter {
public int convertToInt(String v1){
return Integer.valueOf(v1);
}
}
convertToInt()方法和Deserializer類的deserialize()方法有著同樣的方法簽名,正是由於這樣,我們可以建立一個StringConverter
例項,,並且從一個lambda表示式中引用它:
StringConverter stringConverter = new StringConverter();
Deserializer des = stringConverter::convertToInt;
第一行的程式碼建立了一個StringConverter例項物件,第二行的lambda表示式引用了StringConverter 例項物件的convertToInt()方法。
14.4構造方法引用
最後,我們也能引用一個類的構造方法,你只需要在::new 的前面寫上類名即可。就像這樣:
MyClass::new
如果想看一下,作為lambda表示式如何使用構造方法的話,看下這個介面的定義:
public interface Factory {
public String create(char[] val);
}
這個create()方法能夠匹配String類的某一個構造方法的方法簽名,因此,這個構造方法可以作為一個lambda表示式使用。這裡有一個例子:
Factory factory = String::new;
上面的這段程式碼其實就等同於:
Factory factory = chars -> new String(chars);
相關文章
- lambda expressions are not supported at this language level intellijExpressIntelliJ
- Understand Lambda Expressions in 3 minutes(翻譯)Express
- Java Lambda StreamJava
- 如何解決 case expressions must be constant expressionsExpress
- Java | Lambda表示式Java
- Java8-lambdaJava
- Lambda表示式(Java)Java
- java lambda 表示式Java
- Java Lambda表示式Java
- [轉]Java 8 的 lambda 表示式 Java 8 的 lambda 表示式Java
- Expressions v1.3.6Express
- Java之lambda表示式Java
- Java的Lambda表示式Java
- Java 8 Lambda 表示式Java
- Java基礎系列-LambdaJava
- java 8 lambda表示式Java
- Java Lambda 表示式初探Java
- Java 8 Lambda 揭祕Java
- Java Lambda 使用備忘Java
- Java8新特性 - LambdaJava
- Java 基礎 —— Lambda 表示式Java
- Java 中的 Lambda 表示式Java
- Java lambda表示式基本使用Java
- Java8-Lambda表示式Java
- java8 lambda表示式Java
- 掌握 Java 8 Lambda 表示式Java
- Java筆記:Lambda表示式Java筆記
- Using Regular Expressions in Oracle DatabaseExpressOracleDatabase
- 學習Java8系列-LambdaJava
- Java8 Lambda 之 Collection StreamJava
- Java8的Lambda表示式Java
- Java中lambda表示式詳解Java
- 深入理解 Java 中的 LambdaJava
- Java中Lambda表示式的使用Java
- Java8新特性系列-LambdaJava
- Java8新特性系列(Lambda)Java
- Java 8 中的 lambda 表示式Java
- 深入探索 Java 8 Lambda 表示式Java