天天看點

JDK1.9-Lambda表達式

Lambda表達式

3.1 函數式程式設計思想概述

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-ZOJfF2lb-1575278902055)(img/03-Overview.png)]

在數學中,函數就是有輸入量、輸出量的一套計算方案,也就是“拿什麼東西做什麼事情”。相對而言,面向對象過分強調“必須通過對象的形式來做事情”,而函數式思想則盡量忽略面向對象的複雜文法——強調做什麼,而不是以什麼形式做。

面向對象的思想:

​ 做一件事情,找一個能解決這個事情的對象,調用對象的方法,完成事情.

函數式程式設計思想:

​ 隻要能擷取到結果,誰去做的,怎麼做的都不重要,重視的是結果,不重視過程

3.2 備援的Runnable代碼

傳統寫法

當需要啟動一個線程去完成任務時,通常會通過

java.lang.Runnable

接口來定義任務内容,并使用

java.lang.Thread

類來啟動該線程。代碼如下:

public class Demo01Runnable {
	public static void main(String[] args) {
    	// 匿名内部類
		Runnable task = new Runnable() {
			@Override
			public void run() { // 覆寫重寫抽象方法
				System.out.println("多線程任務執行!");
			}
		};
		new Thread(task).start(); // 啟動線程
	}
}
           

本着“一切皆對象”的思想,這種做法是無可厚非的:首先建立一個

Runnable

接口的匿名内部類對象來指定任務内容,再将其交給一個線程來啟動。

代碼分析

對于

Runnable

的匿名内部類用法,可以分析出幾點内容:

  • Thread

    類需要

    Runnable

    接口作為參數,其中的抽象

    run

    方法是用來指定線程任務内容的核心;
  • 為了指定

    run

    的方法體,不得不需要

    Runnable

    接口的實作類;
  • 為了省去定義一個

    RunnableImpl

    實作類的麻煩,不得不使用匿名内部類;
  • 必須覆寫重寫抽象

    run

    方法,是以方法名稱、方法參數、方法傳回值不得不再寫一遍,且不能寫錯;
  • 而實際上,似乎隻有方法體才是關鍵所在。

3.3 程式設計思想轉換

做什麼,而不是怎麼做

我們真的希望建立一個匿名内部類對象嗎?不。我們隻是為了做這件事情而不得不建立一個對象。我們真正希望做的事情是:将

run

方法體内的代碼傳遞給

Thread

類知曉。

傳遞一段代碼——這才是我們真正的目的。而建立對象隻是受限于面向對象文法而不得不采取的一種手段方式。那,有沒有更加簡單的辦法?如果我們将關注點從“怎麼做”回歸到“做什麼”的本質上,就會發現隻要能夠更好地達到目的,過程與形式其實并不重要。

生活舉例

JDK1.9-Lambda表達式

當我們需要從北京到上海時,可以選擇高鐵、汽車、騎行或是徒步。我們的真正目的是到達上海,而如何才能到達上海的形式并不重要,是以我們一直在探索有沒有比高鐵更好的方式——搭乘飛機。

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-8VJOI10g-1575278902058)(img/02-Lambda.png)]

而現在這種飛機(甚至是飛船)已經誕生:2014年3月Oracle所釋出的Java 8(JDK 1.8)中,加入了Lambda表達式的重量級新特性,為我們打開了新世界的大門。

3.4 體驗Lambda的更優寫法

借助Java 8的全新文法,上述

Runnable

接口的匿名内部類寫法可以通過更簡單的Lambda表達式達到等效:

public class Demo02LambdaRunnable {
	public static void main(String[] args) {
		new Thread(() -> System.out.println("多線程任務執行!")).start(); // 啟動線程
	}
}
           

這段代碼和剛才的執行效果是完全一樣的,可以在1.8或更高的編譯級别下通過。從代碼的語義中可以看出:我們啟動了一個線程,而線程任務的内容以一種更加簡潔的形式被指定。

不再有“不得不建立接口對象”的束縛,不再有“抽象方法覆寫重寫”的負擔,就是這麼簡單!

3.5 回顧匿名内部類

Lambda是怎樣擊敗面向對象的?在上例中,核心代碼其實隻是如下所示的内容:

() -> System.out.println("多線程任務執行!")
           

為了了解Lambda的語義,我們需要從傳統的代碼起步。

使用實作類

要啟動一個線程,需要建立一個

Thread

類的對象并調用

start

方法。而為了指定線程執行的内容,需要調用

Thread

類的構造方法:

  • public Thread(Runnable target)

為了擷取

Runnable

接口的實作對象,可以為該接口定義一個實作類

RunnableImpl

public class RunnableImpl implements Runnable {
	@Override
	public void run() {
		System.out.println("多線程任務執行!");
	}
}
           

然後建立該實作類的對象作為

Thread

類的構造參數:

public class Demo03ThreadInitParam {
	public static void main(String[] args) {
		Runnable task = new RunnableImpl();
		new Thread(task).start();
	}
}
           

使用匿名内部類

這個

RunnableImpl

類隻是為了實作

Runnable

接口而存在的,而且僅被使用了唯一一次,是以使用匿名内部類的文法即可省去該類的單獨定義,即匿名内部類:

public class Demo04ThreadNameless {
	public static void main(String[] args) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println("多線程任務執行!");
			}
		}).start();
	}
}
           

匿名内部類的好處與弊端

一方面,匿名内部類可以幫我們省去實作類的定義;另一方面,匿名内部類的文法——确實太複雜了!

語義分析

仔細分析該代碼中的語義,

Runnable

接口隻有一個

run

方法的定義:

  • public abstract void run();

即制定了一種做事情的方案(其實就是一個函數):

  • 無參數:不需要任何條件即可執行該方案。
  • 無傳回值:該方案不産生任何結果。
  • 代碼塊(方法體):該方案的具體執行步驟。

同樣的語義展現在

Lambda

文法中,要更加簡單:

() -> System.out.println("多線程任務執行!")
           
  • 前面的一對小括号即

    run

    方法的參數(無),代表不需要任何條件;
  • 中間的一個箭頭代表将前面的參數傳遞給後面的代碼;
  • 後面的輸出語句即業務邏輯代碼。

3.6 Lambda标準格式

Lambda省去面向對象的條條框框,格式由3個部分組成:

  • 一些參數
  • 一個箭頭
  • 一段代碼

Lambda表達式的标準格式為:

(參數類型 參數名稱) -> { 代碼語句 }
           

格式說明:

  • 小括号内的文法與傳統方法參數清單一緻:無參數則留白;多個參數則用逗号分隔。
  • ->

    是新引入的文法格式,代表指向動作。
  • 大括号内的文法與傳統方法體要求基本一緻。

3.7 練習:使用Lambda标準格式(無參無傳回)

題目

給定一個廚子

Cook

接口,内含唯一的抽象方法

makeFood

,且無參數、無傳回值。如下:

public interface Cook {
    void makeFood();
}
           

在下面的代碼中,請使用Lambda的标準格式調用

invokeCook

方法,列印輸出“吃飯啦!”字樣:

public class Demo05InvokeCook {
    public static void main(String[] args) {
        // TODO 請在此使用Lambda【标準格式】調用invokeCook方法
    }

    private static void invokeCook(Cook cook) {
        cook.makeFood();
    }
}
           

解答

public static void main(String[] args) {
    invokeCook(() -> {
      	System.out.println("吃飯啦!");
    });
}
           
備注:小括号代表

Cook

接口

makeFood

抽象方法的參數為空,大括号代表

makeFood

的方法體。

3.8 Lambda的參數和傳回值

需求:
    使用數組存儲多個Person對象
    對數組中的Person對象使用Arrays的sort方法通過年齡進行升序排序
           

下面舉例示範

java.util.Comparator<T>

接口的使用場景代碼,其中的抽象方法定義為:

  • public abstract int compare(T o1, T o2);

當需要對一個對象數組進行排序時,

Arrays.sort

方法需要一個

Comparator

接口執行個體來指定排序的規則。假設有一個

Person

類,含有

String name

int age

兩個成員變量:

public class Person { 
    private String name;
    private int age;
    
    // 省略構造器、toString方法與Getter Setter 
}
           

如果使用傳統的代碼對

Person[]

數組進行排序,寫法如下:

import java.util.Arrays;
import java.util.Comparator;

public class Demo06Comparator {
    public static void main(String[] args) {
      	// 本來年齡亂序的對象數組
        Person[] array = {
        	new Person("古力娜紮", 19),
        	new Person("迪麗熱巴", 18),
       		new Person("馬爾紮哈", 20) };

      	// 匿名内部類
        Comparator<Person> comp = new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge() - o2.getAge();
            }
        };
        Arrays.sort(array, comp); // 第二個參數為排序規則,即Comparator接口執行個體

        for (Person person : array) {
            System.out.println(person);
        }
    }
}
           

這種做法在面向對象的思想中,似乎也是“理所當然”的。其中

Comparator

接口的執行個體(使用了匿名内部類)代表了“按照年齡從小到大”的排序規則。

下面我們來搞清楚上述代碼真正要做什麼事情。

  • 為了排序,

    Arrays.sort

    方法需要排序規則,即

    Comparator

    接口的執行個體,抽象方法

    compare

    是關鍵;
  • compare

    Comparator

  • ComparatorImpl

  • compare

  • 實際上,隻有參數和方法體才是關鍵。

Lambda寫法

import java.util.Arrays;

public class Demo07ComparatorLambda {
    public static void main(String[] args) {
        Person[] array = {
          	new Person("古力娜紮", 19),
          	new Person("迪麗熱巴", 18),
          	new Person("馬爾紮哈", 20) };

        Arrays.sort(array, (Person a, Person b) -> {
          	return a.getAge() - b.getAge();
        });

        for (Person person : array) {
            System.out.println(person);
        }
    }
}
           

3.9 練習:使用Lambda标準格式(有參有傳回)

給定一個電腦

Calculator

接口,内含抽象方法

calc

可以将兩個int數字相加得到和值:

public interface Calculator {
    int calc(int a, int b);
}
           

invokeCalc

方法,完成120和130的相加計算:

public class Demo08InvokeCalc {
    public static void main(String[] args) {
        // TODO 請在此使用Lambda【标準格式】調用invokeCalc方法來計算120+130的結果ß
    }

    private static void invokeCalc(int a, int b, Calculator calculator) {
        int result = calculator.calc(a, b);
        System.out.println("結果是:" + result);
    }
}
           

public static void main(String[] args) {
    invokeCalc(120, 130, (int a, int b) -> {
      	return a + b;
    });
}
           

Calculator

calc

抽象方法的參數,大括号代表

calc

3.10 Lambda省略格式

可推導即可省略

Lambda強調的是“做什麼”而不是“怎麼做”,是以凡是可以根據上下文推導得知的資訊,都可以省略。例如上例還可以使用Lambda的省略寫法:

public static void main(String[] args) {
  	invokeCalc(120, 130, (a, b) -> a + b);
}
           

省略規則

在Lambda标準格式的基礎上,使用省略寫法的規則為:

  1. 小括号内參數的類型可以省略;
  2. 如果小括号内有且僅有一個參,則小括号可以省略;
  3. 如果大括号内有且僅有一個語句,則無論是否有傳回值,都可以省略大括号、return關鍵字及語句分号。
備注:掌握這些省略規則後,請對應地回顧本章開頭的多線程案例。

3.11 練習:使用Lambda省略格式

public class Demo09InvokeCook {
    public static void main(String[] args) {
        // TODO 請在此使用Lambda【省略格式】調用invokeCook方法
    }

    private static void invokeCook(Cook cook) {
        cook.makeFood();
    }
}
           

public static void main(String[] args) {
  	invokeCook(() -> System.out.println("吃飯啦!"));
}
           

3.12 Lambda的使用前提

  1. 使用Lambda必須具有接口,且要求接口中有且僅有一個抽象方法。

    無論是JDK内置的

    Runnable

    Comparator

    接口還是自定義的接口,隻有當接口中的抽象方法存在且唯一時,才可以使用Lambda。
  2. 使用Lambda必須具有上下文推斷。

    也就是方法的參數或局部變量類型必須為Lambda對應的接口類型,才能使用Lambda作為該接口的執行個體。