Java8 特性詳解 lambda表達(dá)式(一):使用篇
點(diǎn)擊藍(lán)色“程序員黃小斜”關(guān)注我喲
加個“星標(biāo)”,每天和你一起多進(jìn)步一點(diǎn)點(diǎn)!
在 Java 8之前,一個實(shí)現(xiàn)了只有一個抽象方法的接口的匿名類看起來更像Lambda 表達(dá)式。下面的代碼中,anonymousClass方法調(diào)用waitFor方法,參數(shù)是一個實(shí)現(xiàn)接口的Condition類,實(shí)現(xiàn)的功能為,當(dāng)滿足某些條件,Server 就會關(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)行時的環(huán)境。在 Java 8 之前,這就需要把匿名類所需要的一切復(fù)制給它。在上面的例子中,就需要把 server 屬性復(fù)制給匿名類。
因?yàn)槭菑?fù)制,變量必須聲明為 final 類型,以保證在獲取和使用時不會被改變。Java 使用了優(yōu)雅的方式保證了變量不會被更新,所以我們不用顯式地把變量加上 final 修飾。
Lambda 表達(dá)式則不需要拷貝變量到它的運(yùn)行環(huán)境中,從而 Lambda 表達(dá)式被當(dāng)做是一個真正的方法來對待,而不是一個類的實(shí)例。
Lambda 表達(dá)式不需要每次都要被實(shí)例化,對于 Java 來說,帶來巨大的好處。不像實(shí)例化匿名類,對內(nèi)存的影響可以降到最小。
總體來說,匿名方法和匿名類存在以下區(qū)別:
類必須實(shí)例化,而方法不必;當(dāng)一個類被新建時,需要給對象分配內(nèi)存;方法只需要分配一次內(nèi)存,它被存儲在堆的永久區(qū)內(nèi);對象作用于它自己的數(shù)據(jù),而方法不會;靜態(tài)類里的方法類似于匿名方法的功能。
一些具體的區(qū)別 匿名方法和匿名類有一些具體的區(qū)別,主要包括獲取語義和覆蓋變量。
獲取語義 this 關(guān)鍵字是其中的一個語義上的區(qū)別。在匿名類中,this 指的是匿名類的實(shí)例,例如有了內(nèi)部類為 Foo$InnerClass,當(dāng)你引用內(nèi)部類閉包的作用域時,像Foo.this.x的代碼看起來就有些奇怪。在 Lambda 表達(dá)式中,this 指的就是閉包作用域,事實(shí)上,Lambda 表達(dá)式就是一個作用域,這就意味著你不需要從超類那里繼承任何名字,或是引入作用域的層級。你可以在作用域里直接訪問屬性,方法和局部變量。例如,下面的代碼中,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的簡寫。但是在匿名類中,你必須顯式地調(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ǔ),對理解Lambda表達(dá)式有很大幫助,因?yàn)镴ava正在將自己變的更高(Sha)級(Gua),更人性化。--------可以這么說lambda表達(dá)式其實(shí)就是實(shí)現(xiàn)SAM接口的語法糖。
lambda寫的好可以極大的減少代碼冗余,同時可讀性也好過冗長的內(nèi)部類,匿名類。
先列舉兩個常見的簡化(簡單的代碼同樣好理解)
創(chuàng)建線程

排序

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

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

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

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

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

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

4. flatMap:和map類似,不同的是其每個元素轉(zhuǎn)換得到的是Stream對象,會把子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 中的層級結(jié)構(gòu)扁平化,就是將最底層元素抽出來放到一起,最終 output 的新 Stream 里面已經(jīng)沒有 List 了,都是直接的數(shù)字。
5. peek: 生成一個包含原Stream的所有元素的新Stream,同時會提供一個消費(fèi)函數(shù)(Consumer實(shí)例),新Stream每個元素被消費(fèi)的時候都會執(zhí)行給定的消費(fèi)函數(shù);

6. limit: 對一個Stream進(jìn)行截斷操作,獲取其前N個元素,如果原Stream中包含的元素個數(shù)小于N,那就獲取其所有的元素;

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

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

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