1. 程式人生 > >Java8_02_lambda表示式

Java8_02_lambda表示式

一、前言

這一節我們來了解下lambda表示式,主要關注以下幾點:

  • 行為引數化
  • 匿名類
  • Lambda 表示式
  • 方法 引用

二、行為引數化

1.概念

行為引數化(behavior parameterization)是用來處理頻繁更改的需求的一種軟體開發模式,可以將一段程式碼塊當做引數傳給另一個方法,稍後執行。這樣,這個方法的行為就基於那塊程式碼被引數化了。

也就是說 行為引數化: 讓方法接受多種行為( 或戰略) 作為引數, 並在內部使用, 來完成不同的行為

2.需求頻繁更改的例項

現在,我們來看一個需求不斷變化的場景:

我們需要為一個農場主開發一個農場庫存程式

2.1 篩選綠蘋果

一開始,農場主提出需要一個從列表中篩選出綠蘋果的功能。

第一個解決方案可能是這樣的:

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

2.2 將顏色作為引數

但是現在農民改主意了, 他還想要篩選紅蘋果。 你該怎麼做呢? 簡單的解決辦法就是複製這個方法, 把名字改成 filterRedApples, 然後更改 if 條件來匹配紅蘋果。 然而, 要是農民想要篩選多種顏色: 淺綠色、 暗紅 色、 黃色 等, 這種方法就應付不了了。

一個良好的原則是在編寫類似的程式碼之後,嘗試將其抽象化

一種做法是給方法加一個引數,把顏色變成引數,這樣就能靈活地適應變了:

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

現在, 只要像下面這樣呼叫方法,農民朋友就會滿意了:

List< Apple> greenApples = filterApplesByColor( inventory, "green"); 
List< Apple> redApples = filterApplesByColor( inventory, "red");

2.3 對你能想到的每個屬性做篩選

過了幾天, 這位 農民又跑回來和你說:“ 要是能區分輕的蘋果和重的蘋果 就太好了。 重的蘋果一般是重量大於150克。”

你 可以 將 顏色 和 重量 結合 為 一個 方法, 稱為 filter。 不過 就算 這樣, 你 還是 需要 一種 方式 來 區分 想要 篩選 哪個 屬性。 你 可以 加上 一個 標誌 來 區分 對 顏色 和 重量 的 查詢( 但 絕不 要 這樣做! 我們 很快 會 解釋 為什麼)。

一種 把 所有 屬性 結合 起來 的 笨拙 嘗試 如下 所示:

public static List<Apple> filterApples(List<Apple> inventory,String color, int weight, boolean flag){
        List<Apple> result = new ArrayList<>();
        for(Apple apple : inventory){
            if((flag&&apple.getColor().equals(color)) || 
                    (!flag && apple.getWeight() > weight)){
                result.add(apple);
            }
        }
        return result;
}

你 可以 這麼 用( 但 真的 很 笨拙):

List< Apple> greenApples = filterApples( inventory, "green", 0, true); 
List< Apple> heavyApples = filterApples( inventory, "", 150, false);


這個 解決 方案 再 差 不 過了:

  • 首先, 客戶 端 程式碼 看上去 糟透 了。 true 和 false 是什麼 意思?
  • 此外, 這個 解決 方案 還是 不能 很好 地 應對 變化 的 需求。
    • 如果 這位 農民 要求 你對 蘋果 的 不同 屬性 做 篩選, 比如 大小、 形狀、產地 等, 又 怎麼辦?
    • 而且, 如果 農民 要求 你 組合 屬性, 做 更 複雜 的 查詢, 比如 綠色 的 重 蘋果, 又 該 怎麼辦?
      你會 有好 多個 重複 的 filter 方法, 或 一個 巨大 的 非常 複雜 的 方法。

2.4 利用策略模式,根據抽象篩選

讓我 們 後退 一步 來看 看 更高 層次 的 抽象。

一種 可能 的 解決 方案 是對 你的 選擇 標準 建模: 你 考慮 的 是 蘋果, 需要 根據 Apple 的 某些 屬性( 比如 它是 綠色 的 嗎? 重量 超過 150 克 嗎?) 來 返回 一個 boolean 值。 我們 把 它 稱為 謂詞( 即 一個 返回 boolean 值 的 函式)。

在這裡插入圖片描述

如上圖:讓我 們 定義 一個 介面 來 對 選擇 標準 建模:

public interface ApplePredicate{
    boolean test(Apple apple);
}

現在 你就 可 以用 ApplePredicate 的 多個 實現 代表 不同 的 選擇 標準 了,

//select only heavy apple
public class AppleHeavyWeightPredicate implements ApplePredicate{ 
    public boolean test(Apple apple){
        return apple.getWeight() > 150;
    }
}
 
//select only green apple
public class AppleGreenColorPredicate implements ApplePredicate{ 
    public boolean test(Apple apple){
        return "green".equals(apple.getColor);
    }
}

//select red and heavy apple
public	static class AppleRedAndHeavyPredicate implements ApplePredicate{
		public boolean test(Apple apple){
			return "red".equals(apple.getColor()) 
					&& apple.getWeight() > 150; 
		}
	}

你 需要 filterApples 方法 接受 ApplePredicate 物件, 對 Apple 做 條件 測試。 這就 是 行為引數化: 讓方法接受多種行為( 或戰略) 作為引數, 並在內部使用, 來完成不同的行為

利用 ApplePredicate 改過 之後, filter 方法 看起來 是 這樣 的:


	public static List<Apple> filter(List<Apple> inventory, ApplePredicate p){
		List<Apple> result = new ArrayList<>();
		for(Apple apple : inventory){
			if(p.test(apple)){
				result.add(apple);
			}
		}
		return result;
	}       

這時就能 引數化 filterApples 的行為, 並可以通過傳遞不同的篩選策略 來滿足不同的篩選需求。

List<Apple> greenApples2 = filter(inventory, new AppleColorPredicate());

List<Apple> heavyApples = filter(inventory, new AppleWeightPredicate());

List<Apple> redAndHeavyApples = filter(inventory, new AppleRedAndHeavyPredicate());

這時我們完成了一件很酷的事: filterApples 方法的行為取決於你通過ApplePredicate 物件傳遞的程式碼。 換句話說, 你把 filterApples 方法的行為引數化了`

這時,如果看完後面的lambda再來看這裡,就能知道: lambda內部的實現肯定也是使用策略模式來實現行為引數化的

不過這裡有一個缺陷:

由於 該 filterApples 方法 只能 接受 物件, 所以 你 必須 把 程式碼 包裹 在 ApplePredicate 物件 裡。 你的 做法 就 類似於 在 內聯“ 傳遞 程式碼”, 因為 你是 通過 一個 實現 了 test 方法 的 物件 來 傳遞 布林 表示式 的。
而通過使用lambda則可以解決這個問題。

三、匿名類

目前,當要把新的行為傳遞給 filterApples 方法的時候, 你不得不宣告幾個實現 ApplePredicate 介面的類, 然後例項化好幾個只會提到 一次的 ApplePredicate 物件。

使用匿名類,可以避免這個問題。

1. 匿名類定義

使用匿名類能簡化程式碼,能讓你同時宣告並例項化它[1]

匿名類定義格式:

new 父類構造器(引數列表)| 實現介面() 
{ 
     //匿名內部類的類體部分 
}

2. 匿名類的用法:

首先得有一個給定的抽象類或者介面,然後我們通過匿名類去實現它。

3. 匿名類特點

  • 它必須繼承一個類或者實現一個介面,而不能顯示的使用extends或者implements,沒有父類。
  • 匿名類沒有構造方法。通過new<父類名> 建立物件,匿名類定義與建立物件是同時進行的。
  • 匿名類只能一次性的建立,並有父類控制代碼持有。
  • 匿名類不需要初始化,只有預設的構造方法

匿名內部類還有如下兩條規則:

  • 匿名內部類不能是抽象類,因為系統在建立匿名內部類的時候,會立即建立內部類的物件。因此不允許將匿名內部類定義成抽象類。
  • 匿名內部類不能定義構造器,因為匿名內部類沒有類名,所以無法定義構造器,但匿名內部類可以定義例項初始化塊,通過例項初始化塊來完成構造器需要完成的事情。

4. 匿名類使用示例

以下程式碼來自官方文件,展示了通過匿名類來實現介面的用法。

public class HelloWorldAnonymousClasses {
  
    interface HelloWorld {
        public void greet();
        public void greetSomeone(String someone);
    }
  
    public void sayHello() {
        
        class EnglishGreeting implements HelloWorld {
            String name = "world";
            public void greet() {
                greetSomeone("world");
            }
            public void greetSomeone(String someone) {
                name = someone;
                System.out.println("Hello " + name);
            }
        }
      
        HelloWorld englishGreeting = new EnglishGreeting();
        
        //匿名類實現介面
        HelloWorld frenchGreeting = new HelloWorld() {
            String name = "tout le monde";
            public void greet() {
                greetSomeone("tout le monde");
            }
            public void greetSomeone(String someone) {
                name = someone;
                System.out.println("Salut " + name);
            }
        };
        
         //匿名類實現介面
        HelloWorld spanishGreeting = new HelloWorld() {
            String name = "mundo";
            public void greet() {
                greetSomeone("mundo");
            }
            public void greetSomeone(String someone) {
                name = someone;
                System.out.println("Hola, " + name);
            }
        };
        englishGreeting.greet();
        frenchGreeting.greetSomeone("Fred");
        spanishGreeting.greet();
    }

    public static void main(String... args) {
        HelloWorldAnonymousClasses myApp =
            new HelloWorldAnonymousClasses();
        myApp.sayHello();
    }            
}

5. 用匿名類改造農場示例

當有新的規則時,我們可以使用匿名類來實現 ApplePredicate 介面,來指定相應的過濾規則。

List<Apple> redApples2 = filter(inventory, new ApplePredicate() {
		public boolean test(Apple a){
			return a.getColor().equals("red"); 
		}
	});

四、Lambda 表示式

我們在前面通過匿名類進一步優化了我們的程式碼,儘管如此,還是有一些囉嗦。
若是我們使用lambda表示式的話,就會更簡潔:

List< Apple> result = filterApples( inventory, (Apple apple) -> "red". equals( apple. getColor()));

在前面,我們瞭解了利用行為引數化來傳遞程式碼有助於應對不斷變化的需求。 它允許你定義一個程式碼塊來表示一個行為, 然後傳遞它。這樣,我們就可以編寫更為靈活且可重複使用的程式碼了。

1. 函式式介面與函式描述符

(1)函式式介面
函式式介面就是隻定義一個抽象方法的介面。

public interface Predicate< T>{ 
	boolean test (T t);
}

(2)函式描述符
函式式介面的抽象方法的簽名基本上就是 Lambda 表示式的簽名。 我們將這種 抽象方法叫作函式描述符。

請注意這個概念:

函式描述符就是 Lambda 表示式的簽名

2. lambda定義

定義:

可以把 Lambda 表示式 理解為 簡潔地表示可傳遞的匿名函式的一種方式: 它沒有名稱, 但它有引數列表函式主體返回型別, 可能還有一個可以丟擲的異常列表

注意:

(1)Lambda 表示式允許你直接內聯, 為函式式介面的抽象方法提供實現, 並且將整個表示式作為函式式介面的一個例項

(2)可以將lambda表示式看作匿名類功能
(3)它其實就是為函式式介面生成了一個例項。

基本語法如下:

(parameters) -> expression

(parameters) -> { statements; }

3.lambda示例

// 1.函式式介面
@FunctionalInterface 
public interface Predicate< T>{ 
    //2.函式描述符
	boolean test( T t); 
}


//過濾方法
public static < T> List< T> filter( List< T> list, Predicate< T> p) {
	List< T> results = new ArrayList<>();
	for( T s: list){
		if( p. test( s)){
			results. add( s); } } return results;
} 


// lambda表示式與函式描述符對應
Predicate< String> nonEmptyStringPredicate = (String s) -> !s. isEmpty();

List< String> nonEmpty = filter( listOfStrings, nonEmptyStringPredicate);

4.使用範圍

可以在函式式介面上使用 Lambda 表示式。

5.型別推斷

Lambda 的型別是從使用 Lambda 的上下文推斷出來的。
上下文( 比如,接受它傳遞的方法的引數, 或 接受它的值的區域性變數)中Lambda表示式需要的型別稱為目標型別。

//不用型別推斷
Comparator< Apple> c = (Apple a1, Apple a2) -> a1. getWeight(). compareTo( a2. getWeight());

//使用型別推斷
Comparator< Apple> c = (a1, a2) -> a1. getWeight(). compareTo( a2. getWeight());

五、方法引用

(1)方法引用可以被看作僅僅呼叫特定方法的 Lambda 的一種快捷寫法。

(2)它的基本思想是, 如果一個 Lambda代表的只是“ 直接呼叫這個方法”, 那最好還是用名稱來呼叫它, 而不是去描述如何呼叫它。

(3)方法引用就是讓你根據已有的方法實現來建立 Lambda 表示式

(4)你可以把方法引用看作針對僅僅涉及單一方法的 Lambda 的語法糖

1. 格式

目標引用 :: 方法名

2.三類方法引用

方法引用主要有三類:

  • (1) 指向靜態方法的方法引用( 例如 Integer 的 parseInt 方法, 寫作 Integer:: parseInt)。
  • (2) 指向任意型別例項方法的方法引用( 例如 String 的 length 方法, 寫作 String:: length)。
  • (3) 指向現有物件的例項方法的方法引用( 假設你有一個區域性變數 expensiveTransaction 用於存放 Transaction 型別的物件, 它支援例項方法 getValue, 那麼你就可以寫 expensiveTransaction:: getValue)。

類似於 String:: length 的 第二 種 方法 引用 的 思想 就是 你在 引用 一個 物件 的 方法, 而這 個 物件 本身 是 Lambda 的 一個 引數。 例如, Lambda 表示式( String s) -> s. toUppeCase() 可以 寫作 String:: toUpperCase。
但 第三 種 方法 引用 指的 是, 你在 Lambda 中 呼叫 一個 已經 存在 的 外部 物件 中的 方法。 例如, Lambda 表示式()-> expensiveTransaction. getValue() 可以 寫作 expensiveTransaction:: getValue。

在這裡插入圖片描述

3.示例

在這裡插入圖片描述

六、參考資料

  1. Anonymous Classes - The Java™ Tutorials
    Java匿名物件和匿名類總結