文章目錄
3.1 函數式編程思想概述
在數學中,函數就是有輸入量、輸出量的一套計算方案,也就是“拿什麼東西做什麼事情”。相對而言,面向對象過分強調“必須通過對象的形式來做事情”,而函數式思想則儘量忽略面向對象的複雜語法——強調做什麼,而不是以什麼形式做。
面向對象的思想:
做一件事情,找一個能解決這個事情的對象,調用對象的方法,完成事情.
函數式編程思想:
只要能獲取到結果,誰去做的,怎麼做的都不重要,重視的是結果,不重視過程
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
類知曉。
生活舉例
當我們需要從北京到上海時,可以選擇高鐵、汽車。我們的真正目的是到達上海,而如何才能到達上海的形式並不重要,所以我們一直在探索有沒有比高鐵更好的方式——搭乘飛機。
而現在這種飛機(甚至是飛船)已經誕生: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(); // 啓動線程
}
}
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);
}
在下面的代碼中,請使用Lambda的標準格式調用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標準格式的基礎上,使用省略寫法的規則爲:
- 小括號內參數的類型可以省略;
- 如果小括號內有且僅有一個參,則小括號可以省略;
- 如果大括號內有且僅有一個語句,則無論是否有返回值,都可以省略大括號、return關鍵字及語句分號。
備註:掌握這些省略規則後,請對應地回顧本章開頭的多線程案例。
3.11 練習:使用Lambda省略格式
題目
仍然使用前文含有唯一makeFood
抽象方法的廚子Cook
接口,在下面的代碼中,請使用Lambda的省略格式調用invokeCook
方法,打印輸出“吃飯啦!”字樣:
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的使用前提
Lambda的語法非常簡潔,完全沒有面向對象複雜的束縛。但是使用時有幾個問題需要特別注意:
- 使用Lambda必須具有接口,且要求接口中有且僅有一個抽象方法。
無論是JDK內置的Runnable
、Comparator
接口還是自定義的接口,只有當接口中的抽象方法存在且唯一時,纔可以使用Lambda。 - 使用Lambda必須具有上下文推斷。
也就是方法的參數或局部變量類型必須爲Lambda對應的接口類型,才能使用Lambda作爲該接口的實例。
備註:有且僅有一個抽象方法的接口,稱爲“函數式接口”。