Java8 特性詳解 lambda表達(dá)式(一):使用篇
點(diǎn)擊藍(lán)色“程序員黃小斜”關(guān)注我喲
加個(gè)“星標(biāo)”,每天和你一起多進(jìn)步一點(diǎn)點(diǎn)!
在 Java 8之前,一個(gè)實(shí)現(xiàn)了只有一個(gè)抽象方法的接口的匿名類看起來更像Lambda 表達(dá)式。下面的代碼中,anonymousClass方法調(diào)用waitFor方法,參數(shù)是一個(gè)實(shí)現(xiàn)接口的Condition類,實(shí)現(xiàn)的功能為,當(dāng)滿足某些條件,Server 就會(huì)關(guān)閉。 下面的代碼是典型的匿名類的使用。
void anonymousClass() {
final Server server = new HttpServer();
waitFor(new Condition() {
@Override
public Boolean isSatisfied() {
return !server.isRunning();
}
}
復(fù)制代碼
下面的代碼用 Lambda 表達(dá)式實(shí)現(xiàn)相同的功能:
void closure() {
Server server = new HttpServer();
waitFor(() -> !server.isRunning());
}
復(fù)制代碼
其實(shí),上面的waitFor方法,更接近于下面的代碼的描述:
class WaitFor {
static void waitFor(Condition condition) throws
InterruptedException {
while (!condition.isSatisfied())
Thread.sleep(250);
}
}
復(fù)制代碼
一些理論上的區(qū)別 實(shí)際上,上面的兩種方法的實(shí)現(xiàn)都是閉包,后者的實(shí)現(xiàn)就是Lambda 表示式。這就意味著兩者都需要持有運(yùn)行時(shí)的環(huán)境。在 Java 8 之前,這就需要把匿名類所需要的一切復(fù)制給它。在上面的例子中,就需要把 server 屬性復(fù)制給匿名類。
因?yàn)槭菑?fù)制,變量必須聲明為 final 類型,以保證在獲取和使用時(shí)不會(huì)被改變。Java 使用了優(yōu)雅的方式保證了變量不會(huì)被更新,所以我們不用顯式地把變量加上 final 修飾。
Lambda 表達(dá)式則不需要拷貝變量到它的運(yùn)行環(huán)境中,從而 Lambda 表達(dá)式被當(dāng)做是一個(gè)真正的方法來對(duì)待,而不是一個(gè)類的實(shí)例。
Lambda 表達(dá)式不需要每次都要被實(shí)例化,對(duì)于 Java 來說,帶來巨大的好處。不像實(shí)例化匿名類,對(duì)內(nèi)存的影響可以降到最小。
總體來說,匿名方法和匿名類存在以下區(qū)別:
類必須實(shí)例化,而方法不必;當(dāng)一個(gè)類被新建時(shí),需要給對(duì)象分配內(nèi)存;方法只需要分配一次內(nèi)存,它被存儲(chǔ)在堆的永久區(qū)內(nèi);對(duì)象作用于它自己的數(shù)據(jù),而方法不會(huì);靜態(tài)類里的方法類似于匿名方法的功能。
一些具體的區(qū)別 匿名方法和匿名類有一些具體的區(qū)別,主要包括獲取語義和覆蓋變量。
獲取語義 this 關(guān)鍵字是其中的一個(gè)語義上的區(qū)別。在匿名類中,this 指的是匿名類的實(shí)例,例如有了內(nèi)部類為 Foo$InnerClass,當(dāng)你引用內(nèi)部類閉包的作用域時(shí),像Foo.this.x的代碼看起來就有些奇怪。在 Lambda 表達(dá)式中,this 指的就是閉包作用域,事實(shí)上,Lambda 表達(dá)式就是一個(gè)作用域,這就意味著你不需要從超類那里繼承任何名字,或是引入作用域的層級(jí)。你可以在作用域里直接訪問屬性,方法和局部變量。例如,下面的代碼中,Lambda 表達(dá)式可以直接訪問firstName變量。
public class Example {
private String firstName = "Tom";
public void example() {
Function addSurname = surname -> {
// equivalent to this.firstName
return firstName + " " + surname; // or even,
};
}
}
復(fù)制代碼
這里的firstName就是this.firstName的簡(jiǎn)寫。但是在匿名類中,你必須顯式地調(diào)用firstName,
public class Example {
private String firstName = "Jerry";
public void anotherExample() {
Function addSurname = new Function String>() {
@Override
public String apply(String surname) {
return Example.this.firstName + " " + surname;
}
};
}
}
1.lambda表達(dá)式
Java8最值得學(xué)習(xí)的特性就是Lambda表達(dá)式和Stream API,如果有python或者javascript的語言基礎(chǔ),對(duì)理解Lambda表達(dá)式有很大幫助,因?yàn)镴ava正在將自己變的更高(Sha)級(jí)(Gua),更人性化。--------可以這么說lambda表達(dá)式其實(shí)就是實(shí)現(xiàn)SAM接口的語法糖。
lambda寫的好可以極大的減少代碼冗余,同時(shí)可讀性也好過冗長(zhǎng)的內(nèi)部類,匿名類。
先列舉兩個(gè)常見的簡(jiǎn)化(簡(jiǎn)單的代碼同樣好理解)
創(chuàng)建線程

排序

lambda表達(dá)式配合Java8新特性Stream API可以將業(yè)務(wù)功能通過函數(shù)式編程簡(jiǎn)潔的實(shí)現(xiàn)。(為后面的例子做鋪墊)
例如:

這段代碼就是對(duì)一個(gè)字符串的列表,把其中包含的每個(gè)字符串都轉(zhuǎn)換成全小寫的字符串。注意代碼第四行的map方法調(diào)用,這里map方法就是接受了一個(gè)lambda表達(dá)式。
1.1lambda表達(dá)式語法
1.1.1lambda表達(dá)式的一般語法
(Type1 param1, Type2 param2, ..., TypeN paramN) -> {
statment1;
statment2;
//.............
return statmentM;
}這是lambda表達(dá)式的完全式語法,后面幾種語法是對(duì)它的簡(jiǎn)化。
1.1.2單參數(shù)語法
param1 -> {
statment1;
statment2;
//.............
return statmentM;
}當(dāng)lambda表達(dá)式的參數(shù)個(gè)數(shù)只有一個(gè),可以省略小括號(hào)
例如:將列表中的字符串轉(zhuǎn)換為全小寫
List
1.1.3單語句寫法
param1 -> statment
當(dāng)lambda表達(dá)式只包含一條語句時(shí),可以省略大括號(hào)、return和語句結(jié)尾的分號(hào)
例如:將列表中的字符串轉(zhuǎn)換為全小寫
List
List
1.1.4方法引用寫法
(方法引用和lambda一樣是Java8新語言特性,后面會(huì)講到)
Class or instance :: method
例如:將列表中的字符串轉(zhuǎn)換為全小寫
List
List
1.2lambda表達(dá)式可使用的變量
先舉例:
//將為列表中的字符串添加前綴字符串 String waibu = "lambda :"; List
輸出:
lambda :Ni -----:1474622341604 lambda :Hao -----:1474622341604 lambda :Lambda -----:1474622341604
變量waibu :外部變量
變量chuandi :傳遞變量
變量zidingyi :內(nèi)部自定義變量
lambda表達(dá)式可以訪問給它傳遞的變量,訪問自己內(nèi)部定義的變量,同時(shí)也能訪問它外部的變量。
不過lambda表達(dá)式訪問外部變量有一個(gè)非常重要的限制:變量不可變(只是引用不可變,而不是真正的不可變)。
當(dāng)在表達(dá)式內(nèi)部修改waibu = waibu + " ";時(shí),IDE就會(huì)提示你:
Local variable waibu defined in an enclosing scope must be final or effectively final
編譯時(shí)會(huì)報(bào)錯(cuò)。因?yàn)樽兞縲aibu被lambda表達(dá)式引用,所以編譯器會(huì)隱式的把其當(dāng)成final來處理。
以前Java的匿名內(nèi)部類在訪問外部變量的時(shí)候,外部變量必須用final修飾?,F(xiàn)在java8對(duì)這個(gè)限制做了優(yōu)化,可以不用顯示使用final修飾,但是編譯器隱式當(dāng)成final來處理。
1.3lambda表達(dá)式中的this概念
在lambda中,this不是指向lambda表達(dá)式產(chǎn)生的那個(gè)SAM對(duì)象,而是聲明它的外部對(duì)象。
例如:
public class WhatThis {
public void whatThis(){
//轉(zhuǎn)全小寫
List proStrs = Arrays.asList(new String[]{"Ni","Hao","Lambda"});
List 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
2.方法引用和構(gòu)造器引用
本人認(rèn)為是進(jìn)一步簡(jiǎn)化lambda表達(dá)式的聲明的一種語法糖。
前面的例子中已有使用到:execStrs.forEach(System.out::println);
2.1方法引用
objectName::instanceMethod
ClassName::staticMethod
ClassName::instanceMethod
前兩種方式類似,等同于把lambda表達(dá)式的參數(shù)直接當(dāng)成instanceMethod|staticMethod的參數(shù)來調(diào)用。比如System.out::println等同于x->System.out.println(x);Math::max等同于(x, y)->Math.max(x,y)。
最后一種方式,等同于把lambda表達(dá)式的第一個(gè)參數(shù)當(dāng)成instanceMethod的目標(biāo)對(duì)象,其他剩余參數(shù)當(dāng)成該方法的參數(shù)。比如String::toLowerCase等同于x->x.toLowerCase()。
可以這么理解,前兩種是將傳入對(duì)象當(dāng)參數(shù)執(zhí)行方法,后一種是調(diào)用傳入對(duì)象的方法。
2.2構(gòu)造器引用
構(gòu)造器引用語法如下:ClassName::new,把lambda表達(dá)式的參數(shù)當(dāng)成ClassName構(gòu)造器的參數(shù) 。例如BigDecimal::new等同于x->new BigDecimal(x)。
3.Stream語法
兩句話理解Stream:
1.Stream是元素的集合,這點(diǎn)讓Stream看起來用些類似Iterator;2.可以支持順序和并行的對(duì)原Stream進(jìn)行匯聚的操作;
大家可以把Stream當(dāng)成一個(gè)裝飾后的Iterator。原始版本的Iterator,用戶只能逐個(gè)遍歷元素并對(duì)其執(zhí)行某些操作;包裝后的Stream,用戶只要給出需要對(duì)其包含的元素執(zhí)行什么操作,比如“過濾掉長(zhǎng)度大于10的字符串”、“獲取每個(gè)字符串的首字母”等,具體這些操作如何應(yīng)用到每個(gè)元素上,就給Stream就好了!原先是人告訴計(jì)算機(jī)一步一步怎么做,現(xiàn)在是告訴計(jì)算機(jī)做什么,計(jì)算機(jī)自己決定怎么做。當(dāng)然這個(gè)“怎么做”還是比較弱的。
例子:
//Lists是Guava中的一個(gè)工具類 List
上面這段代碼是獲取一個(gè)List中,元素不為null的個(gè)數(shù)。這段代碼雖然很簡(jiǎn)短,但是卻是一個(gè)很好的入門級(jí)別的例子來體現(xiàn)如何使用Stream,正所謂“麻雀雖小五臟俱全”。我們現(xiàn)在開始深入解刨這個(gè)例子,完成以后你可能可以基本掌握Stream的用法!

圖片就是對(duì)于Stream例子的一個(gè)解析,可以很清楚的看見:原本一條語句被三種顏色的框分割成了三個(gè)部分。紅色框中的語句是一個(gè)Stream的生命開始的地方,負(fù)責(zé)創(chuàng)建一個(gè)Stream實(shí)例;綠色框中的語句是賦予Stream靈魂的地方,把一個(gè)Stream轉(zhuǎn)換成另外一個(gè)Stream,紅框的語句生成的是一個(gè)包含所有nums變量的Stream,進(jìn)過綠框的filter方法以后,重新生成了一個(gè)過濾掉原nums列表所有null以后的Stream;藍(lán)色框中的語句是豐收的地方,把Stream的里面包含的內(nèi)容按照某種算法來匯聚成一個(gè)值,例子中是獲取Stream中包含的元素個(gè)數(shù)。如果這樣解析以后,還不理解,那就只能動(dòng)用“核武器”–圖形化,一圖抵千言!

使用Stream的基本步驟:
1.創(chuàng)建Stream;2.轉(zhuǎn)換Stream,每次轉(zhuǎn)換原有Stream對(duì)象不改變,返回一個(gè)新的Stream對(duì)象(可以有多次轉(zhuǎn)換);3.對(duì)Stream進(jìn)行聚合(Reduce)操作,獲取想要的結(jié)果;
3.1怎么得到Stream
最常用的創(chuàng)建Stream有兩種途徑:
1.通過Stream接口的靜態(tài)工廠方法(注意:Java8里接口可以帶靜態(tài)方法);2.通過Collection接口的默認(rèn)方法(默認(rèn)方法:Default method,也是Java8中的一個(gè)新特性,就是接口中的一個(gè)帶有實(shí)現(xiàn)的方法)–stream(),把一個(gè)Collection對(duì)象轉(zhuǎn)換成Stream
3.1.1 使用Stream靜態(tài)方法來創(chuàng)建Stream
1. of方法:有兩個(gè)overload方法,一個(gè)接受變長(zhǎng)參數(shù),一個(gè)接口單一值
Stream
2. generator方法:生成一個(gè)無限長(zhǎng)度的Stream,其元素的生成是通過給定的Supplier(這個(gè)接口可以看成一個(gè)對(duì)象的工廠,每次調(diào)用返回一個(gè)給定類型的對(duì)象)
Stream.generate(new Supplier
Stream.generate(() -> Math.random()); Stream.generate(Math::random); 三條語句的作用都是一樣的,只是使用了lambda表達(dá)式和方法引用的語法來簡(jiǎn)化代碼。每條語句其實(shí)都是生成一個(gè)無限長(zhǎng)度的Stream,其中值是隨機(jī)的。這個(gè)無限長(zhǎng)度Stream是懶加載,一般這種無限長(zhǎng)度的Stream都會(huì)配合Stream的limit()方法來用。
3. iterate方法:也是生成無限長(zhǎng)度的Stream,和generator不同的是,其元素的生成是重復(fù)對(duì)給定的種子值(seed)調(diào)用用戶指定函數(shù)來生成的。其中包含的元素可以認(rèn)為是:seed,f(seed),f(f(seed))無限循環(huán)
Stream.iterate(1, item -> item + 1).limit(10).forEach(System.out::println); 這段代碼就是先獲取一個(gè)無限長(zhǎng)度的正整數(shù)集合的Stream,然后取出前10個(gè)打印。千萬記住使用limit方法,不然會(huì)無限打印下去。
3.1.2通過Collection子類獲取Stream
Collection接口有一個(gè)stream方法,所以其所有子類都都可以獲取對(duì)應(yīng)的Stream對(duì)象。
public interface Collection
3.2轉(zhuǎn)換Stream
轉(zhuǎn)換Stream其實(shí)就是把一個(gè)Stream通過某些行為轉(zhuǎn)換成一個(gè)新的Stream。Stream接口中定義了幾個(gè)常用的轉(zhuǎn)換方法,下面我們挑選幾個(gè)常用的轉(zhuǎn)換方法來解釋。1. distinct: 對(duì)于Stream中包含的元素進(jìn)行去重操作(去重邏輯依賴元素的equals方法),新生成的Stream中沒有重復(fù)的元素;

2. filter: 對(duì)于Stream中包含的元素使用給定的過濾函數(shù)進(jìn)行過濾操作,新生成的Stream只包含符合條件的元素;

3. map: 對(duì)于Stream中包含的元素使用給定的轉(zhuǎn)換函數(shù)進(jìn)行轉(zhuǎn)換操作,新生成的Stream只包含轉(zhuǎn)換生成的元素。這個(gè)方法有三個(gè)對(duì)于原始類型的變種方法,分別是:mapToInt,mapToLong和mapToDouble。這三個(gè)方法也比較好理解,比如mapToInt就是把原始Stream轉(zhuǎn)換成一個(gè)新的Stream,這個(gè)新生成的Stream中的元素都是int類型。之所以會(huì)有這樣三個(gè)變種方法,可以免除自動(dòng)裝箱/拆箱的額外消耗;

4. flatMap:和map類似,不同的是其每個(gè)元素轉(zhuǎn)換得到的是Stream對(duì)象,會(huì)把子Stream中的元素壓縮到父集合中;

flatMap給一段代碼理解:
Stream> inputStream = Stream.of(
Arrays.asList(1),
Arrays.asList(2, 3),
Arrays.asList(4, 5, 6)
);
StreamoutputStream = inputStream.
flatMap((childList) -> childList.stream());
flatMap 把 input Stream 中的層級(jí)結(jié)構(gòu)扁平化,就是將最底層元素抽出來放到一起,最終 output 的新 Stream 里面已經(jīng)沒有 List 了,都是直接的數(shù)字。
5. peek: 生成一個(gè)包含原Stream的所有元素的新Stream,同時(shí)會(huì)提供一個(gè)消費(fèi)函數(shù)(Consumer實(shí)例),新Stream每個(gè)元素被消費(fèi)的時(shí)候都會(huì)執(zhí)行給定的消費(fèi)函數(shù);

6. limit: 對(duì)一個(gè)Stream進(jìn)行截?cái)嗖僮?,獲取其前N個(gè)元素,如果原Stream中包含的元素個(gè)數(shù)小于N,那就獲取其所有的元素;

7. skip: 返回一個(gè)丟棄原Stream的前N個(gè)元素后剩下元素組成的新Stream,如果原Stream中包含的元素個(gè)數(shù)小于N,那么返回空Stream;

整體調(diào)用例子:
List
這段代碼演示了上面介紹的所有轉(zhuǎn)換方法(除了flatMap),簡(jiǎn)單解釋一下這段代碼的含義:給定一個(gè)Integer類型的List,獲取其對(duì)應(yīng)的Stream對(duì)象,然后進(jìn)行過濾掉null,再去重,再每個(gè)元素乘以2,再每個(gè)元素被消費(fèi)的時(shí)候打印自身,在跳過前兩個(gè)元素,最后去前四個(gè)元素進(jìn)行加和運(yùn)算(解釋一大堆,很像廢話,因?yàn)榛究戳朔椒椭酪鍪裁戳?。這個(gè)就是聲明式編程的一大好處!)。大家可以參考上面對(duì)于每個(gè)方法的解釋,看看最終的輸出是什么。
可能會(huì)有這樣的疑問:在對(duì)于一個(gè)Stream進(jìn)行多次轉(zhuǎn)換操作,每次都對(duì)Stream的每個(gè)元素進(jìn)行轉(zhuǎn)換,而且是執(zhí)行多次,這樣時(shí)間復(fù)雜度就是一個(gè)for循環(huán)里把所有操作都做掉的N(轉(zhuǎn)換的次數(shù))倍啊。其實(shí)不是這樣的,轉(zhuǎn)換操作都是lazy的,多個(gè)轉(zhuǎn)換操作只會(huì)在匯聚操作(見下節(jié))的時(shí)候融合起來,一次循環(huán)完成。我們可以這樣簡(jiǎn)單的理解,Stream里有個(gè)操作函數(shù)的集合,每次轉(zhuǎn)換操作就是把轉(zhuǎn)換函數(shù)放入這個(gè)集合中,在匯聚操作的時(shí)候循環(huán)Stream對(duì)應(yīng)的集合,然后對(duì)每個(gè)元素執(zhí)行所有的函數(shù)。
3.3匯聚(Reduce)Stream
匯聚操作(也稱為折疊)接受一個(gè)元素序列為輸入,反復(fù)使用某個(gè)合并操作,把序列中的元素合并成一個(gè)匯總的結(jié)果。比如查找一個(gè)數(shù)字列表的總和或者最大值,或者把這些數(shù)字累積成一個(gè)List對(duì)象。Stream接口有一些通用的匯聚操作,比如reduce()和collect();也有一些特定用途的匯聚操作,比如sum(),max()和count()。注意:sum方法不是所有的Stream對(duì)象都有的,只有IntStream、LongStream和DoubleStream是實(shí)例才有。
下面會(huì)分兩部分來介紹匯聚操作:
可變匯聚:把輸入的元素們累積到一個(gè)可變的容器中,比如Collection或者StringBuilder;其他匯聚:除去可變匯聚剩下的,一般都不是通過反復(fù)修改某個(gè)可變對(duì)象,而是通過把前一次的匯聚結(jié)果當(dāng)成下一次的入?yún)?,反?fù)如此。比如reduce,count,allMatch;
3.3.1可變匯聚
可變匯聚對(duì)應(yīng)的只有一個(gè)方法:collect,正如其名字顯示的,它可以把Stream中的要有元素收集到一個(gè)結(jié)果容器中(比如Collection)。先看一下最通用的collect方法的定義(還有其他override方法):
List
第一個(gè)函數(shù)生成一個(gè)新的ArrayList實(shí)例;第二個(gè)函數(shù)接受兩個(gè)參數(shù),第一個(gè)是前面生成的ArrayList對(duì)象,二個(gè)是stream中包含的元素,函數(shù)體就是把stream中的元素加入ArrayList對(duì)象中。第二個(gè)函數(shù)被反復(fù)調(diào)用直到原stream的元素被消費(fèi)完畢;第三個(gè)函數(shù)也是接受兩個(gè)參數(shù),這兩個(gè)都是ArrayList類型的,函數(shù)體就是把第二個(gè)ArrayList全部加入到第一個(gè)中;但是上面的collect方法調(diào)用也有點(diǎn)太復(fù)雜了,沒關(guān)系!我們來看一下collect方法另外一個(gè)override的版本,其依賴Collector](http://docs.oracle.com/javase/8/docs/api/java/util/stream/Collector.html "Collector (Java Platform SE 8 )"))。
List
3.3.2其他匯聚
– reduce方法:reduce方法非常的通用,后面介紹的count,sum等都可以使用其實(shí)現(xiàn)。reduce方法有三個(gè)override的方法,本文介紹兩個(gè)最常用的。先來看reduce方法的第一種形式,其方法定義如下:
Optional
List

reduce方法還有一個(gè)很常用的變種:
T reduce(T identity, BinaryOperator
List
List
– 搜索相關(guān) – allMatch:是不是Stream中的所有元素都滿足給定的匹配條件 – anyMatch:Stream中是否存在任何一個(gè)元素滿足匹配條件 – findFirst: 返回Stream中的第一個(gè)元素,如果Stream為空,返回空Optional – noneMatch:是不是Stream中的所有元素都不滿足給定的匹配條件 – max和min:使用給定的比較器(Operator),返回Stream中的最大|最小值 下面給出allMatch和max的例子,剩下的方法讀者當(dāng)成練習(xí)。
查看源代碼打印幫助 List
參考文章
Java 中的 Lambda 表達(dá)式 - 掘金
Java8特性詳解 lambda表達(dá)式 Stream - aoeiuv - 博客園
微信公眾號(hào)【程序員黃小斜】作者是前螞蟻金服Java工程師,專注分享Java技術(shù)干貨和求職成長(zhǎng)心得,不限于BAT面試,算法、計(jì)算機(jī)基礎(chǔ)、數(shù)據(jù)庫、分布式、spring全家桶、微服務(wù)、高并發(fā)、JVM、Docker容器,ELK、大數(shù)據(jù)等。關(guān)注后回復(fù)【book】領(lǐng)取精選20本Java面試必備精品電子書。
—?【 THE END 】— 公眾號(hào)[程序員黃小斜]全部博文已整理成一個(gè)目錄,請(qǐng)?jiān)诠娞?hào)里回復(fù)「m」獲取! 最近面試BAT,整理一份面試資料《Java面試BATJ通關(guān)手冊(cè)》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點(diǎn)“在看”,關(guān)注公眾號(hào)并回復(fù) PDF?領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。
謝謝支持喲 (*^__^*)
