JAVA8新特性一:lambda表達式

大家好,最近入職了新的公司,所以很久都沒有寫新博客了(其實就是懶)
新公司大量的使用了一些JAVA8的新特性,所以也惡補了一下JAVA8的知識(好吧我知道JAVA14都出來了我還在寫JAVA8的特性確實有點落伍了)但最近看了一下實戰系列的JAVA8實戰,感覺很受啓發,作者的高度帶給了我不一樣的角度來看這些特性。

Lambda表達式

Lambda表達式概述

首先讓我們來康康JAVA8最大的一個變動,支持了Lambda表達式。其實這個變動也是JAVA不得已而爲之,因爲他的競爭對手都支持了這種方便的寫法,不思進取的話只能落得和cobol前輩一樣的下場。
用最簡單的話說,Lambda其實就是把一段代碼作爲了一個變量,在JAVA8之前變量只能是那幾大基礎類型+引用類型,而把一段代碼作爲變量有什麼好處呢?好處是可以很簡潔的描述匿名內部類,讓我們來看一個《JAVA8實戰》中的例子:

假設你在設計一個農場庫存程序,你必須實現一個從列表中篩選綠蘋果的功能。

好,是不是很簡單?讓我們來實現它

第一版:單純的綠蘋果篩選
public static List<Apple> filterGreenApples(List<Apple> inventory) { List<Apple> result = new ArrayList<Apple>();
for(Apple apple: inventory){
  if( "green".equals(apple.getColor() ) { result.add(apple);
} }
    return result;
}

OK,非常簡單,遍歷List逐個比較顏色,把符合條件的放進新List。
現在農民伯伯提出需求:
他還想要篩選紅蘋果。 你該怎麼做呢?

第二版:抽象顏色
public static List<Apple> filterApplesByColor(List<Apple> inventory, String color) {
    List<Apple> result = new ArrayList<Apple>();
    for (Apple apple: inventory){
if ( apple.getColor().equals(color) ) { result.add(apple);
} }
    return result;
}

我們多加了個參數叫color,用以抽象顏色,這樣農民伯伯無論要什麼顏色的蘋果,都可以篩選。

但是,農民伯伯又來了:“要是能區分 輕的蘋果和重的蘋果就太好了。重的蘋果一般是重量大於150克。”你握緊了拳頭,對農民伯伯抱拳以示尊敬。馬上坐下來又開始編寫:

第三版:單純的重量篩選

public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weight) {
        List<Apple> result = new ArrayList<Apple>();
        For (Apple apple: inventory){
if ( apple.getWeight() > weight ){ result.add(apple);
} }
        return result;
    }

OK,還是很簡單。但你已經開始注意到事情沒對了,水溫逐漸升高,作爲一隻老練的青蛙你發現你寫了一個很類似顏色篩選的方法,單純只是從比較顏色變成了比較重量。

第四版:重量或顏色篩選(通過flag來控制)
public static List<Apple> filterApples(List<Apple> inventory, String color, int weight, boolean flag) {
     List<Apple> result = new ArrayList<Apple>();
    for (Apple apple: inventory){
if ( (flag && apple.getColor().equals(color)) || (!flag && apple.getWeight() > weight) ){ result.add(apple);
} }
    return result;
}

你通過一個flag來表示是要比較顏色還是比較重量,這樣這個方法可以篩選顏色或重量,but。。。你覺不覺得那個flag很醜?再說,如果農民伯伯又跑過來說我還需要篩選形狀和大小,難道你需要加更多的flag來控制到底篩選哪個嗎?

第五版:策略模式
//重量篩選器
public class AppleHeavyWeightPredicate implements ApplePredicate{ 
	public boolean test(Apple apple){
	return apple.getWeight() > 150; 
	}
}
//顏色篩選器
public class AppleGreenColorPredicate implements ApplePredicate{
	public boolean test(Apple apple){
	return "green".equals(apple.getColor());
 	 } 
}
//我們要比什麼,就放什麼篩選器進去
public static List<Apple> filterApples(List<Apple> inventory,
                                           ApplePredicate p){
        List<Apple> result = new ArrayList<>();
        for(Apple apple: inventory){
			if(p.test(apple)){ result.add(apple);
			} 
		}
    return result;
}

好了,到這一步策略模式也用上了,感覺越來越高級了哈。之後如果要再篩選什麼,我們直接寫一些篩選器就好了,看起來非常好。
但能不能再改進呢?好像也不是不行,如果硬要挑骨頭的話,我們會覺得每次要新建好多篩選器類啊,如果有的篩選器類就用得到一次,確實在我們工程裏很不妥,刪又刪不得,但又沒啥其他的作用。
其實我們不用特地聲明篩選器啊,在要篩選的時候,用一個匿名類不就好了嗎?

第六版:匿名類
List<Apple> redApples = filterApples(inventory, new ApplePredicate() { 
public boolean test(Apple apple){
	return "red".equals(apple.getColor());
	}
});

Good,這樣我們就不用新建很多篩選器類了,但尷尬的是代碼好像沒有減少啊。。。代碼只是從一個單獨的類移到了方法入參。。。。

這時候就該。。。。。Lambda救場!

第七版:Lambda
List<Apple> result =
filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));

仔細看,Lambda用(Apple apple) -> “red”.equals(apple.getColor())取代了匿名類的聲明,這就是它的簡潔之處。

當然,現在可能你還不知道這行帶箭頭的代碼是什麼意思,但沒事,我們已經一步一步引出了Lambda的必要性與優越性。

Lambda語法初探

我們以一個例子來講解Lambda
在這裏插入圖片描述
Lambda分三部分:

參數列表——這裏它採用了Comparator中compare方法的參數,兩個Apple。
箭頭——箭頭->把參數列表與Lambda主體分隔開。
Lambda主體——比較兩個Apple的重量。表達式就是Lambda的返回值了。

大概知道了這三部分後,我將引用一篇知乎的文章來講解Lambda,當時我看了如醍醐灌頂,知乎鏈接在這裏,也可以直接去看:
Lambda 表達式有何用處?如何使用? - Mingqi的回答 - 知乎
知乎地址

我們知道,對於一個Java變量,我們可以賦給其一個“值”。如果你想把“一塊代碼”賦給一個Java變量,應該怎麼做呢?比如,我想把右邊那塊代碼,賦給一個叫做aBlockOfCode的Java變量:在Java 8之前,這個是做不到的。但是Java 8問世之後,利用Lambda特性,就可以做到了。當然,這個並不是一個很簡潔的寫法。所以,爲了使這個賦值操作更加elegant, 我們可以移除一些沒用的聲明。這樣,我們就成功的非常優雅的把“一塊代碼”賦給了一個變量。而“這塊代碼”,或者說“這個被賦給一個變量的函數”,就是一個Lambda表達式。但是這裏仍然有一個問題,就是變量aBlockOfCode的類型應該是什麼?在Java 8裏面,所有的Lambda的類型都是一個接口,而Lambda表達式本身,也就是”那段代碼“,需要是這個接口的實現。這是我認爲理解Lambda的一個關鍵所在,簡而言之就是,Lambda表達式本身就是一個接口的實現。直接這樣說可能還是有點讓人困擾,我們繼續看看例子。我們給上面的aBlockOfCode加上一個類型:這種只有一個接口函數需要被實現的接口類型,我們叫它”函數式接口“。爲了避免後來的人在這個接口中增加接口函數導致其有多個接口函數需要被實現,變成"非函數接口”,我們可以在這個上面加上一個聲明@FunctionalInterface, 這樣別人就無法在裏面添加新的接口函數了。這樣,我們就得到了一個完整的Lambda表達式聲明:

---------------引用完畢
所以lambda裏代表的方法,就是函數式接口裏的那個唯一的方法。
當然,你可能會想:“爲什麼只有在需要函數式接口的時候纔可以傳遞Lambda呢?”語言的設計者 也考慮過其他辦法,例如給Java添加函數類型(有點兒像我們介紹的描述Lambda表達式簽名的特 殊表示法)。但是他們選擇了現在這種方式,因爲這種方式自然且能避免語言變得更復雜。

JAVA自帶的函數式接口

Lambda必須要有函數式接口才能用,JAVA設計師也設計了幾個內嵌的函數式接口供我們使用:
Predicate
java.util.function.Predicate接口定義了一個名叫test的抽象方法,它接受泛型 T對象,並返回一個boolean。

consumer
java.util.function.Consumer定義了一個名叫accept的抽象方法,它接受泛型T 的對象,沒有返回(void)。你如果需要訪問類型T的對象,並對其執行某些操作,就可以使用 這個接口。

Function
java.util.function.Function<T, R>接口定義了一個叫作apply的方法,它接受一個 泛型T的對象,並返回一個泛型R的對象。如果你需要定義一個Lambda,將輸入對象的信息映射 到輸出,就可以使用這個接口(比如提取蘋果的重量,或把字符串映射爲它的長度)。

局部變量的限制

當我們在Lambda裏使用局部變量時,一定要注意必須加上final修飾符的局部變量纔可以在Lambda中使用(當然大多數情況其實也不會使用到局部變量)

原因如下:
第一,實例變量和局部變量背後的實現有一 個關鍵不同。實例變量都存儲在堆中,而局部變量則保存在棧上。如果Lambda可以直接訪問局 部變量,而且Lambda是在一個線程中使用的,則使用Lambda的線程,可能會在分配該變量的線 程將這個變量收回之後,去訪問該變量。因此,Java在訪問自由局部變量時,實際上是在訪問它 的副本,而不是訪問原始變量。如果局部變量僅僅賦值一次那就沒有什麼區別了——因此就有了 這個限制。

第二,這一限制不鼓勵你使用改變外部變量的典型命令式編程模式

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章