1.1回顧匿名内部類
要啟動一個線程,需要建立一個 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();
}
}
1.2使用匿名内部類
這個 RunnableImpl 類隻是為了實作 Runnable 接口而存在的,而且僅被使用了唯一一次,是以使用匿名内部類的 文法即可省去該類的單獨定義,即匿名内部類:
public class Demo04ThreadNameless {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("多線程任務執行!");
}
}).start();
}
}
一方面,匿名内部類可以幫我們省去實作類的定義;另一方面,匿名内部類的文法——确實太複雜了!
函數式程式設計重點在函數上,在數學中,函數就是有輸入量、輸出量的一套計算方案,也就是“拿什麼東西做什麼事情 最後有什麼結果”。相對而言,面向對象過分強調“必須通過對象的形式來做事情”.
比如: 我要計算: y = 3*x+4,其中x就是輸入量 3x+4就是做什麼,最後的y就是計算的結果
而函數式思想是盡量忽略面向對象的複雜文法——強調做什麼,而不是以什麼形式做。
1.3 備援的Runnable代碼
對于 Runnable 的匿名内部類用法,可以分析出幾點内容:
- Thread 類需要 Runnable 接口作為參數,其中的抽象 run 方法是用來指定線程任務内容的核心;
- 為了指定 run 的方法體,不得不需要 Runnable 接口的實作類;
- 為了省去定義一個 RunnableImpl 實作類的麻煩,不得不使用匿名内部類;
- 必須覆寫重寫抽象 run 方法,是以方法名稱、方法參數、方法傳回值不得不再寫一遍,且不能寫錯;
- 而實際上,似乎隻有方法體才是關鍵所在。
1.4 體驗Lambda的更優寫法
借助Java 8的全新文法,上述 Runnable 接口的匿名内部類寫法可以通過更簡單的Lambda表達式達到等效:
public class Demo02LambdaRunnable {
public static void main(String[] args) {
new Thread(() ‐> System.out.println("多線程任務執行!")).start(); // 啟動線程
}
}
這段代碼和剛才的執行效果是完全一樣的,可以在1.8或更高的編譯級别下通過。從代碼的語義中可以看出:我們 啟動了一個線程,而線程任務的内容以一種更加簡潔的形式被指定。
不再有“不得不建立接口對象”的束縛,不再有“抽象方法覆寫重寫”的負擔,就是這麼簡單! Lambda是怎樣擊敗面向對象的?在上例中,核心代碼其實隻是如下所示的内容:
1.5 語義分析
仔細分析該代碼中的語義, Runnable 接口隻有一個 run 方法的定義:
public abstract void run();
即制定了一種做事情的方案(其實就是一個函數):
無參數:不需要任何條件即可執行該方案。
無傳回值:該方案不産生任何結果。
代碼塊(方法體):該方案的具體執行步驟。
前面的一對小括号即 run 方法的參數(無),代表做這件事不需要參數;
中間的一個箭頭代表将前面的參數傳遞給後面的代碼;
後面的輸出語句即業務邏輯代碼。
1.6 Lambda标準格式
Lambda省去面向對象的條條框框,格式由3個部分組成:
一些參數
一個箭頭
一段代碼
Lambda表達式的标準格式為:
(參數類型 參數名稱) ‐> { 代碼語句;return 值; }
格式說明:
小括号内的文法與傳統方法參數清單一緻:無參數則留白;多個參數則用逗号分隔。
—> 是新引入的文法格式,代表指向動作(做什麼事)。
大括号内的文法與傳統方法體要求基本一緻
1.7 示例:使用Lambda标準格式(無參無傳回)
給定一個廚子 Cook 接口,内含唯一的抽象方法 makeFood ,且無參數、無傳回值。如下:
public interface Cook {
void makeFood();
}
public class Demo05InvokeCook {
public static void main(String[] args) {
invokeCook(() ‐> { System.out.println("吃飯啦!");});
}
private static void invokeCook(Cook cook) {
cook.makeFood();
}
}
備注:小括号代表 Cook 接口 makeFood 抽象方法的參數為空,大括号代表 makeFood 的方法體。
1.8 Lambda的參數和傳回值
下面舉例示範 java.util.Comparator 接口的使用場景代碼,其中的抽象方法定義為:
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寫法
Arrays.sort(array, (Person a, Person b) ‐> {
return a.getAge() ‐ b.getAge();
});
1.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) {
invokeCalc(120, 130, (int a, int b) ‐> {
return a + b; });
}
private static void invokeCalc(int a, int b, Calculator calculator) {
int result = calculator.calc(a, b);
System.out.println("結果是:" + result);
}
}
備注:小括号代表 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的使用前提
Lambda的文法非常簡潔,完全沒有面向對象複雜的束縛。但是使用時有幾個問題需要特别注意:
- 使用Lambda必須具有接口,且要求接口中有且僅有一個抽象方法。 無論是JDK内置的 Runnable 、 Comparator 接口還是自定義的接口,隻有當接口中的抽象方法存在且唯一時,才可以使用Lambda。
- 使用Lambda必須具有上下文推斷。 也就是方法的參數或局部變量類型必須為Lambda對應的接口類型,才能使用Lambda作為該接口的執行個體。
備注:有且僅有一個抽象方法的接口,稱為“函數式接口”。
以下介紹函數式接口
1.1概念
函數式接口在Java中是指:有且僅有一個抽象方法的接口。 函數式接口,即适用于函數式程式設計場景的接口。而Java中的函數式程式設計展現就是Lambda,是以函數式接口就是可 以适用于Lambda使用的接口。隻有確定接口中有且僅有一個抽象方法,Java中的Lambda才能順利地進行推導。
備注:“文法糖”是指使用更加友善,但是原理不變的代碼文法。例如在周遊集合時使用的for-each文法,其實 底層的實作原理仍然是疊代器,這便是“文法糖”。從應用層面來講,Java中的Lambda可以被當做是匿名内部 類的“文法糖”,但是二者在原理上是不同的。
1.2函數式接口的定義
與 @Override 注解的作用類似,Java 8中專門為函數式接口引入了一個新的注解: @FunctionalInterface 。該注 解可用于一個接口的定義上:
import java.lang.FunctionalInterface;
@FunctionalInterface
public interface MyFunctionalInterface {
void myMethod();
}
一旦使用該注解來定義接口,編譯器将會強制檢查該接口是否确實有且僅有一個抽象方法,否則将會報錯。需要注 意的是,即使不使用該注解,隻要滿足函數式接口的定義,這仍然是一個函數式接口,使用起來都一樣。
1.3自定義函數式接口(無參無傳回)
對于剛剛定義好的 MyFunctionalInterface 函數式接口,典型使用場景就是作為方法的參數:
public class FunctionalInterface {
public static void doSomething(MyFunctionalInterface inner){
inner.myMethod();
}
public static void main(String[] args) {
doSomething(() -> System.out.println("Lambada表達式執行了"));
}
}
1.4自定義函數式接口(有參有傳回)
定義一個函數式接口 Sumable ,内含抽象 sum 方法,可以将兩個int數字相加傳回int結果。使用該接口作為方法 的參數,并進而通過Lambda來使用它。
import java.lang.FunctionalInterface;
@FunctionalInterface
public interface Sumable {
int sum(int a,int b);
}
public class DemoSumable {
public static void showSum(int x, int y, Sumable sumable) {
System.out.println(sumable.sum(x, y));
}
public static void main(String[] args) {
showSum(10,20,(m,n) -> m+n);
}
}
1.5 Lambda的延遲執行
一種典型的場景就是對參數進行有條件使用,例如對日志消息進行拼接後,在滿足條件的情況下進行列印輸出:
public class Demo01Logger {
private static void log(int level, String msg) {
if (level == 1) {
System.out.println(msg);
}
}
public static void main(String[] args) {
String msgA = "Hello";
String msgB = "World";
String msgC = "Java";
log(1, msgA + msgB + msgC);
}
}
這段代碼存在問題:無論級别是否滿足要求,作為 log 方法的第二個參數,三個字元串一定會首先被拼接并傳入方 法内,然後才會進行級别判斷。如果級别不符合要求,那麼字元串的拼接操作就白做了,存在性能浪費。
剛剛我們體驗了,日志案例中的性能浪費,現在我們該用函數式程式設計,即使用Lambda!! 我們把日志拼接功能交給函數式接口,調用接口中的方法來拼接,而不是我們自己拼接
使用Lambda必然需要一個函數式接口:
@FunctionalInterface
public interface MessageBuilder {
String buildMessage();
}
public class Demo02Logger {
private static void log(int level, MessageBuilder builder) {
if (level == 1) {
System.out.println(builder.buildMessage());
}
}
public static void main(String[] args) {
String msgA = "Hello";
String msgB = "World";
String msgC = "Java";
log(1, () ‐> msgA + msgB + msgC );
}
}
這樣一來,隻有當級别滿足要求的時候,才會進行三個字元串的拼接;否則三個字元串将不會進行拼接。
下面的代碼可以通過結果進行驗證
public class Demo02Logger {
private static void log(int level, MessageBuilder builder) {
if (level == 1) {
System.out.println(builder.buildMessage());
}
}
public static void main(String[] args) {
String msgA = "Hello";
String msgB = "World";
String msgC = "Java";
log(2, () -> {
System.out.println("Lambada表達式執行了");
return msgA + msgB + msgC;
});
}
}
從結果中可以看出,在不符合級别要求的情況下,Lambda将不會執行。進而達到節省性能的效果。
1.6 使用Lambda作為參數和傳回值
如果抛開實作原理不說,Java中的Lambda表達式可以被當作是匿名内部類的替代品。如果方法的參數是一個函數 式接口類型,那麼就可以使用Lambda表達式進行替代。使用Lambda表達式作為方法參數,其實就是使用函數式 接口作為方法參數。
例如 java.lang.Runnable 接口就是一個函數式接口,假設有一個 startThread 方法使用該接口作為參數,那麼就 可以使用Lambda進行傳參。這種情況其實和 Thread 類的構造方法參數為 Runnable 沒有本質差別。
public class Demo04Runnable {
public static void startThread(Runnable task){
new Thread(task).start();
}
public static void main(String[] args) {
startThread(() -> System.out.println("線程任務執行"));
}
}
類似地,如果一個方法的傳回值類型是一個函數式接口,那麼就可以直接傳回一個Lambda表達式。當需要通過一 個方法來擷取一個 java.util.Comparator 接口類型的對象作為排序器時:
import java.util.Arrays;
import java.util.Comparator;
public class Demo06Comparator {
private static Comparator<String> getComparator() {
return (a, b) -> b.length() - a.length();
}
public static void main(String[] args) {
String[] array = {"a", "ab", "a"};
Thread thread = new Thread();
System.out.println(Arrays.toString(array));
Arrays.sort(array, getComparator());
System.out.println(Arrays.toString(array));
}
}
1.7自定義Lambda參數和傳回值
自定義一個函數式接口 MySupplier ,含有無參數的抽象方法 get 得到 Object 類型的傳回值。并使用該函數式 接口分别作為方法的參數和傳回值
使用該接口作為方法的參數,并且在傳遞參數時将實際參數寫成Lambda:
@FunctionalInterface
public interface MySupplier {
Object get();
}
public class Demo1MySupplier {
private static void printParam(MySupplier supplier) {
Object obj = supplier.get();
System.out.println(obj);
}
public static void main(String[] args) {
printParam(() ‐> "Hello");
}
}
使用該接口作為方法的傳回值,也很簡單:
public class Demo2MySupplier {
private static MySupplier getData() {
return () ‐> "Hello";
}
public static void main(String[] args) {
MySupplier ms = getData();
Object obj = ms.get();
System.out.println(obj);
}
}
1.8方法的引用
備援的Lambda場景
@FunctionalInterface
public interface Printable {
void print(String str);
}
在 Printable 接口當中唯一的抽象方法 print 接收一個字元串參數,目的就是為了列印顯示它。那麼通過Lambda 來使用它的代碼很簡單:
public class PrintSimple {
private static void printString(Printable data) {
data.print("Hello, World!");
}
public static void main(String[] args) {
printString(s ‐> System.out.println(s));
}
}
其中 printString 方法隻管調用 Printable 接口的 print 方法,而并不管 print 方法的具體實作邏輯會将字元串 列印到什麼地方去。而 main 方法通過Lambda表達式指定了函數式接口 Printable 的具體操作方案為:拿到 String(類型可推導,是以可省略)資料後,在控制台中輸出它。
這段代碼的問題在于,對字元串進行控制台列印輸出的操作方案,明明已經有了現成的實作,那就是 System.out 對象中的 println(String) 方法。既然Lambda希望做的事情就是調用 println(String) 方法,那何必自己手動調 用呢?
1.9 用方法引用改進代碼
能否省去Lambda的文法格式(盡管它已經相當簡潔)呢?隻要“引用”過去就好了:
public class PrintRef {
private static void printString(Printable data) {
data.print("Hello, World!");
}
public static void main(String[] args) {
printString(System.out::println);
}
}
請注意其中的雙冒号 :: 寫法,這被稱為“方法引用”,而雙冒号是一種新的文法。
2.0方法引用符
雙冒号 :: 為引用運算符,而它所在的表達式被稱為方法引用。
如果Lambda要表達的函數方案已經存在于某個方法的實作中,那麼則可以通過雙冒号來引用該方法作為Lambda 的替代者。
語義分析
例如上例中, System.out 對象中有一個重載的 println(String) 方法恰好就是我們所需要的。那麼對于 printString 方法的函數式接口參數,對比下面兩種寫法,完全等效:
Lambda表達式寫法:
s -> System.out.println(s);
方法引用寫法: System.out::println
第一種語義是指:拿到參數之後經Lambda之手,繼而傳遞給 System.out.println 方法去處理。
第二種等效寫法的語義是指:直接讓 System.out 中的 println 方法來取代Lambda。兩種寫法的執行效果完全一 樣,而第二種方法引用的寫法複用了已有方案,更加簡潔。
推導與省略
如果使用Lambda,那麼根據“可推導就是可省略”的原則,無需指定參數類型,也無需指定的重載形式——它們都 将被自動推導。而如果使用方法引用,也是同樣可以根據上下文進行推導。
函數式接口是Lambda的基礎,而方法引用是Lambda的孿生兄弟。 下面這段代碼将會調用 println 方法的不同重載形式,将函數式接口改為int類型的參數:
@FunctionalInterface
public interface PrintableInteger {
void print(int str);
}
由于上下文變了之後可以自動推導出唯一對應的比對重載,是以方法引用沒有任何變化:
public class PrintOverload {
private static void printInteger(PrintableInteger data) {
data.print(1024);
}
public static void main(String[] args) {
printInteger(System.out::println);
}
}
這次方法引用将會自動比對到 println(int) 的重載形式。
2.1 通過對象名引用成員方法
這是常見的一種用法,與上例相同。如果一個類中已經存在了一個成員方法:
public class MethodRefObject {
public void printUpperCase(String str) {
String bigStr = str.toUpperCase();
System.out.println(bigStr);
}
}
函數式接口仍然定義為:
@FunctionalInterface
public interface Printable {
void print(String str);
}
那麼當需要使用這個 printUpperCase 成員方法來替代 Printable 接口的Lambda的時候,已經具有了 MethodRefObject 類的對象執行個體,則可以通過對象名引用成員方法,代碼為:
public class Demo04MethodRef {
private static void printString(Printable lambda) {
lambda.print("Hello");
}
public static void main(String[] args) {
MethodRefObject obj = new MethodRefObject();
printString(obj::printUpperCase);
}
}
2.2 通過類名稱引用靜态方法
由于在 java.lang.Math 類中已經存在了靜态方法 abs ,是以當我們需要通過Lambda來調用該方法時,有兩種寫 法。首先是函數式接口:
@FunctionalInterface
public interface Calcable {
int calc(int num);
}
第一種寫法是使用Lambda表達式:
public class Demo05Lambda {
private static void method(int num, Calcable lambda) {
int result = lambda.calc(num);
System.out.println(result);
}
public static void main(String[] args) {
method(‐10, n ‐> Math.abs(n));
}
}
但是使用方法引用的更好寫法是:
public class Demo06MethodRef {
private static void method(int num, Calcable lambda) {
System.out.println(lambda.calc(num));
}
public static void main(String[] args) {
method(‐10, Math::abs);
}
}
在這個例子中,下面兩種寫法是等效的:
Lambda表達式: n -> Math.abs(n) 方法引用: Math::abs
假設有一個 StringUtils 字元串工具類,其中含有靜态方法 isBlank 如下:
public final class StringUtils {
public static boolean isBlank(String str) {
return str == null || "".equals(str.trim());
}
}
定義一個函數式接口 StringChecker ,其中的抽象方法 checkBlank 的預期行為與 isBlank 一緻,并定義一個 方法使用該函數式接口作為參數。通過方法引用的形式,将 StringUtils 工具類中的 isBlank 方法作為Lambda的 實作。
@FunctionalInterface
public interface StringChecker {
boolean checkString(String str);
}
public class DemoStringChecker {
private static void methodCheck(StringChecker checker) {
System.out.println(checker.checkString(" "));
}
public static void main(String[] args) {
methodCheck(StringUtils::isBlank);
}
}
2.3 通過super引用成員方法
如果存在繼承關系,當Lambda中需要出現super調用時,也可以使用方法引用進行替代。首先是函數式接口:
@FunctionalInterface
public interface Greetable {
void greet();
}
public class Human {
public void sayHello(){
System.out.println("Hello!");
}
}
public class Man extends Human {
public void sayHello() {
method(() -> super.sayHello());
}
private void method(Greetable lambda) {
lambda.greet();
System.out.println("I'm a man!");
}
}
在這個例子中,下面兩種寫法是等效的:
Lambda表達式: () -> super.sayHello()
方法引用: super::sayHello
2.4通過this引用成員方法
this代表目前對象,如果需要引用的方法就是目前類中的成員方法,那麼可以使用“this::成員方法”的格式來使用方 法引用。首先是簡單的函數式接口:
@FunctionalInterface
public interface Richable {
void buy();
}
下面是一個丈夫 Husband 類:
public class Husband {
private void marry(Richable lambda) {
lambda.buy();
}
public void beHappy() {
marry(() ‐> System.out.println("買套房子"));
}
}
方法 beHappy 調用了結婚方法 marry ,後者的參數為函數式接口 Richable ,是以需要一個Lambda表達式。 但是如果這個Lambda表達式的内容已經在本類當中存在了,則可以對 Husband 丈夫類進行修改:
public class Husband {
private void buyHouse() {
System.out.println("買套房子");
}
private void marry(Richable lambda) {
lambda.buy();
}
public void beHappy() {
marry(() ‐> this.buyHouse());
}
}
如果希望取消掉Lambda表達式,用方法引用進行替換,則更好的寫法為:
public class Husband {
private void buyHouse() {
System.out.println("買套房子");
}
private void marry(Richable lambda) {
lambda.buy();
}
public void beHappy() {
marry(this::buyHouse);
}
}
在這個例子中,下面兩種寫法是等效的:
Lambda表達式: () -> this.buyHouse()
方法引用: this::buyHouse
2.5 類的構造器引用
由于構造器的名稱與類名完全一樣,并不固定。是以構造器引用使用 類名稱::new 的格式表示。首先是一個簡單 的 Person 類:
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
然後是用來建立 Person 對象的函數式接口:
public interface PersonBuilder {
Person buildPerson(String name);
}
要使用這個函數式接口,可以通過Lambda表達式:
public class Demo09Lambda {
public static void printName(String name, PersonBuilder builder) {
System.out.println(builder.buildPerson(name).getName());
}
public static void main(String[] args) {
printName("趙麗穎", name ‐> new Person(name));
}
}
2.6 數組的構造器引用
數組也是 Object 的子類對象,是以同樣具有構造器,隻是文法稍有不同。如果對應到Lambda的使用場景中時, 需要一個函數式接口:
@FunctionalInterface
public interface ArrayBuilder {
int[] buildArray(int length);
}
在應用該接口的時候,可以通過Lambda表達式:
public class Demo11ArrayInitRef {
private static int[] initArray(int length, ArrayBuilder builder) {
return builder.buildArray(length);
}
public static void main(String[] args) {
int[] array = initArray(10, length ‐> new int[length]);
}
}
但是更好的寫法是使用數組的構造器引用:
public class Demo12ArrayInitRef {
private static int[] initArray(int length, ArrayBuilder builder) {
return builder.buildArray(length);
}
public static void main(String[] args) {
int[] array = initArray(10, int[]::new);
}
}
在這個例子中,下面兩種寫法是等效的:
Lambda表達式: length -> new int[length]
方法引用: int[]::new
備注:數組的構造器引用,可以和Java 8的Stream API結合,在一定程度上“解決”集合中 toArray 方法的泛型擦除問題。