更好的閱讀體驗
\huge{\color{red}{更好的閱讀體驗}}
更好的閱讀體驗Lambda
表達式是一種匿名函數(shù),它可以作為參數(shù)傳遞給方法或存儲在變量中。在 Java8
中,它和函數(shù)式接口一起,共同構(gòu)建了函數(shù)式編程的框架。
什么是函數(shù)式編程
函數(shù)式編程是一種編程范式,也是一種思想。
它將計算視為函數(shù)求值的過程,并強調(diào)函數(shù)的純粹性和不可變性。在函數(shù)式編程中,函數(shù)被視為一等公民,可以作為參數(shù)傳遞、存儲在變量中,并且函數(shù)的執(zhí)行不會產(chǎn)生副作用。
例如,我們想要輸出 List
中的全部元素,命令式編程看起來是下面這樣:
public class Main {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
for (Integer item : list) {
System.out.println(item);
}
}
}
而在函數(shù)式編程的思想下,代碼則看起來是下面這樣:
public class Main {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
list.forEach(System.out::println);
}
}
從以上的兩個例子中,可以看出,命令式編程需要我們自己去實現(xiàn)具體的邏輯細節(jié)。而函數(shù)式編程則是調(diào)用 API
完成需求的實現(xiàn),將原本命令式的代碼寫成一系列嵌套的函數(shù)調(diào)用。
由此可見,在函數(shù)式編程的思想下,我們將功能的具體細節(jié)隱藏,將其抽象為了函數(shù)式接口,這就使得具有規(guī)范、穩(wěn)定、可組合、高復(fù)用的特點。
Lambda 與匿名內(nèi)部類
既然函數(shù)式編程需要將功能抽象為接口,那么我們來回顧一下接口的使用。
接口作為 java
中的一種抽象類型,它定義了一組方法的簽名(方法名、參數(shù)列表和返回類型),但沒有具體的實現(xiàn)。
因此,要使用接口,就必須提供相應(yīng)的實現(xiàn)類,或者包含實現(xiàn)接口的對象返回。例如,要想使用 List
接口,我們可以使用實現(xiàn)了該接口的實現(xiàn)類 ArrayList
、LinkdeList
等,或者像上節(jié)例子一樣,使用 Arrays.asList
的工廠方法返回了一個實現(xiàn)了 List
接口的 ArrayList
對象。
其中,對于實現(xiàn)類來說,由于接口只需要實現(xiàn)某種功能,我們完全可以使用匿名內(nèi)部類來實現(xiàn),例如,我們把輸出 List
的全部元素抽象為一個接口 Show
,其中提供了一個函數(shù)方法 ShowAllItems
。
public interface Show {
void ShowAllItems(List<Integer> arrayList);
}
繼續(xù)沿用之前代碼示例,現(xiàn)在要求輸出 List
中的全部元素:
public class Main {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Show show = new Show() {
@Override
public void ShowAllItems(List<Integer> arrayList) {
for (Integer item : arrayList) System.out.println(item);
}
};
show.ShowAllItems(list);
}
}
上述代碼中,由于接口 Show
其中只有一個抽象方法 ShowAllItems
,如果單獨為該接口實現(xiàn)一個類未免顯得太過笨拙,因此我們在使用時直接使用匿名內(nèi)部類的實現(xiàn),通過這種方式創(chuàng)建一個臨時的實現(xiàn)子類,這就令接口的使用更加靈活。
那么問題來了,如果我們后續(xù)仍要使用多次該接口,每次使用都以匿名內(nèi)部類的方式來實現(xiàn),會導(dǎo)致我們的代碼太過臃腫,有沒有更好的解決辦法呢?
當然有的,這就是我們今天討論的主人公—— Lambda
表達式,如果一個接口中有且只有一個待實現(xiàn)的抽象方法,那么我們可以將匿名內(nèi)部類簡寫為 Lambda
表達式:
public class Main {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Show show = param -> {
for (Integer item : param) System.out.println(item);
};
show.ShowAllItems(list);
}
}
也許上面的示例會使你感到困惑,下面我們來詳細探討一下 Lambda
表達式的基礎(chǔ)語法。
Lambda 表達式基礎(chǔ)語法
- 標準格式為:
([參數(shù)類型 參數(shù)名稱,]...) ‐> { 代碼語句,包括返回值 }
- 和匿名內(nèi)部類不同,
Lambda
表達式僅支持接口,不支持抽象類。 - 接口內(nèi)部必須有且僅有一個抽象方法(可以有多個方法,但是必須保證其他方法有默認實現(xiàn),必須留一個抽象方法出來)
-
Lambda
表達式可以在函數(shù)體中引用外部的變量,從而實現(xiàn)了閉包,但對進入閉包的變量有final
的限制。
接下來,我們看一個簡單示例,假設(shè)接口 Test
中有且僅有如下抽象方法:
public interface Test {
String showTestNumber(Integer param);
}
利用上述接口,我們使用如下匿名內(nèi)部類來實現(xiàn)該方法:
public class Main {
public static void main(String[] args) {
Test test = new Test() {
@Override
public String showTestNumber(Integer param) {
return "Test number is " + param;
}
};
System.out.println(test.showTestNumber(114));
}
}
如果將其轉(zhuǎn)換為 Lambda
的標準格式,則為:
public class Main {
public static void main(String[] args) {
Test test = (Integer param) -> {
return "Test number is " + param;
};
System.out.println(test.showTestNumber(514));
}
}
由于該方法只需傳遞一個參數(shù),因此可以省略參數(shù)類型及其括號:
public class Main {
public static void main(String[] args) {
Test test = param -> {
return "Test number is " + param;
};
System.out.println(test.showTestNumber(1919));
}
}
又因為方法實現(xiàn)只有一條 return
語句,則后面的 { ... }
也可以省略:
public class Main {
public static void main(String[] args) {
Test test = param -> "Test number is " + param;
System.out.println(test.showTestNumber(810));
}
}
此外,如果方法已經(jīng)實現(xiàn),我們可以利用方法引用:
public class Main {
public static void main(String[] args) {
Test test = Main::showTestNumber;
System.out.println(test.showTestNumber(721));
}
// 提取方法實現(xiàn)
private static String showTestNumber(Integer param) {
return "Test number is " + param;
}
}
在上述示例代碼中,Main::showTestNumber
是一個方法引用,它引用了 Main
類中的靜態(tài)方法 showTestNumber
。該方法被賦值給 Test
接口的實例變量 test
。
關(guān)于方法引用的使用,我們在后面還會重新提到。但這里我需要先介紹一下關(guān)于閉包的特性。
閉包是一個函數(shù)(或過程),它可以訪問并操作其作用域外部的變量。在
Java
中,可以通過Lambda
表達式或方法引用來創(chuàng)建閉包。
其實,在 main
方法中,我們還可以通過調(diào)用 test.showTestNumber
來調(diào)用閉包。閉包中的方法 showTestNumber
可以訪問并操作其作用域外部的變量。
為了更清晰地展示 Lambda
的閉包過程,我們使用如下示例:
public class Main {
public static void main(String[] args) {
String Claim = "Test number is ";
Test test = param -> Claim + param;
System.out.println(test.showTestNumber(2333));
}
}
在上述示例代碼中,Lambda
表達式捕獲了外部變量 Claim
,并在 Lambda
表達式的范圍之外(main()方法內(nèi)部)調(diào)用閉包時仍然可以訪問和使用該變量。
注意:Java8
不要求顯式將閉包變量聲明為 final
,但如果你嘗試修改閉包變量的值,則會報錯。
public class Main {
public static void main(String[] args) {
String Claim = "Test number is ";
Claim = "Yeah~ The number is "; // 從lambda 表達式引用的本地變量必須是最終變量或?qū)嶋H上的最終變量
Test test = param -> Claim + param;
System.out.println(test.showTestNumber(2333));
}
}
Lambda 的應(yīng)用
好了,你已經(jīng)學會 1 + 1 = 2 1 + 1 = 2 1+1=2 了,現(xiàn)在來康康更實際的東西吧(
無參的函數(shù)式接口
以最常用的 Runnable
接口為例:
在 Java8
之前,如果需要新建一個線程,使用匿名內(nèi)部類的寫法是這樣:
public class Main {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("哼哼哼啊啊啊~");
}
};
runnable.run();
}
}
如果使用 Lambda
表達式則看起來是這樣;
public class Main {
public static void main(String[] args) {
Runnable runnable = () -> System.out.println("哼哼哼啊啊啊~");
runnable.run();
}
}
我們來看一下具體的 Runnable
接口:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
可以看到該接口上面有 @FunctionalInterface
注解,該注解標識了一個接口是函數(shù)式接口。因此,我們可以使用 Lambda
表達式將匿名內(nèi)部類進行替換。
值得注意的是,@FunctionalInterface
注解并不是必須的,它只是作為一種提示和約束的工具。當我們在定義接口時,如果希望該接口只包含一個抽象方法,以便可以使用 Lambda
表達式或方法引用進行函數(shù)式編程,可以選擇添加 @FunctionalInterface
注解來明確表達這個意圖。
即使沒有添加 @FunctionalInterface
注解,只要該接口符合函數(shù)式接口的定義(只有一個抽象方法),它仍然可以用于函數(shù)式編程。
帶參的函數(shù)式接口
這里假設(shè)我們需要對一個數(shù)組進行排序:
在 Java8
之前,對數(shù)組進行排序可以使用 Arrays.sort
方法,如果需要指定排序規(guī)則,只需要實現(xiàn)其中的 Comparator
方法即可:
public class Main {
public static void main(String[] args) {
Integer[] array = new Integer[]{4, 5, 9, 3, 2, 8, 1, 0, 6};
Arrays.sort(array, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
System.out.println(Arrays.toString(array)); //按從小到大的順序排列
}
}
轉(zhuǎn)換為 Lambda
表達式可以是下面這樣:
public class Main {
public static void main(String[] args) {
Integer[] array = new Integer[]{4, 5, 9, 3, 2, 8, 1, 0, 6};
Arrays.sort(array, (o1, o2) -> o1 - o2);
System.out.println(Arrays.toString(array)); //按從小到大的順序排列
}
}
方法引用
Java
方法引用是一種簡化 Lambda
表達式的語法,用于直接引用已經(jīng)存在的方法。方法引用可以通過以下幾種方式來表示:
-
靜態(tài)方法引用:引用靜態(tài)方法,使用類名或者接口名作為前綴,后面跟上方法名。例如我們在之前例子中介紹過的
Main::showTestNumber
。 -
實例方法引用:引用非靜態(tài)方法,使用對象名或者對象引用作為前綴,后面跟上方法名。例如,
objectName::instanceMethodName
。 -
特定類的任意對象方法引用:引用特定類的實例方法,使用類名作為前綴,后面跟上方法名。例如,
ClassName::instanceMethodName
。 -
構(gòu)造方法引用:引用構(gòu)造方法,使用類名后面跟上
new
關(guān)鍵字。例如,ClassName::new
。 -
數(shù)組構(gòu)造方法引用:引用數(shù)組的構(gòu)造方法,使用數(shù)組類型后面跟上
new
關(guān)鍵字。例如,TypeName[]::new
。
需要注意的是,方法引用的適用條件是被引用的方法的簽名(參數(shù)類型和返回類型)必須與函數(shù)式接口中的抽象方法的參數(shù)類型和返回類型相匹配。
我們使用上節(jié)數(shù)組排序的情景進行舉例,即使我們已經(jīng)利用 Lambda
表達式進行了大幅度的簡化,但是這還不夠,我們觀察 Integer
類,其中有一個叫做 compare
的靜態(tài)方法:
public static int compare(int x, int y) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
該方法是一個靜態(tài)方法,但是它卻和 Comparator
需要實現(xiàn)的方法返回值和參數(shù)定義一模一樣,因此我們直接進行方法引用:
public class Main {
public static void main(String[] args) {
Integer[] array = new Integer[]{4, 5, 9, 3, 2, 8, 1, 0, 6};
Arrays.sort(array, Integer::compare);
System.out.println(Arrays.toString(array)); //按從小到大的順序排列
}
}
如果不使用靜態(tài)方法,而使用普通的成員方法,即在 Comparator
中,我們需要實現(xiàn)的方法為:
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
其中 o1
和 o2
都是 Integer
類型,而在 Integer
類中有一個 compareTo
方法:
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
如果是以匿名內(nèi)部類的方式實現(xiàn),那么代碼如下:
public class Main {
public static void main(String[] args) {
Integer[] array = new Integer[]{4, 5, 9, 3, 2, 8, 1, 0, 6};
Arrays.sort(array, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
});
System.out.println(Arrays.toString(array)); //按從小到大的順序排列
}
}
繼續(xù)將上述匿名內(nèi)部類替換為 Lambda
表達式如下:
public class Main {
public static void main(String[] args) {
Integer[] array = new Integer[]{4, 5, 9, 3, 2, 8, 1, 0, 6};
Arrays.sort(array, (o1, o2) -> o1.compareTo(o2));
System.out.println(Arrays.toString(array)); //按從小到大的順序排列
}
}
由于該方法并非靜態(tài)方法,而是所屬的實例對象所有,如果我們想要引用該方法,我們需要進行實例方法引用:
public class Main {
public static void main(String[] args) {
Integer[] array = new Integer[]{4, 5, 9, 3, 2, 8, 1, 0, 6};
Arrays.sort(array, Integer::compareTo);
System.out.println(Arrays.toString(array)); //按從小到大的順序排列
}
}
雖然看起來和剛才的靜態(tài)方法引用沒有什么區(qū)別,但實際上,當我們使用非靜態(tài)方法時,會使用抽象方參數(shù)列表的第一個作為目標對象,后續(xù)參數(shù)作為目標對象成員方法的參數(shù),即 o1
作為目標對象,o2
作為參數(shù),正好匹配了 compareTo
方法。
對于構(gòu)造方法引用,假設(shè)接口 Test
中有抽象方法 newTest
:
public interface Test {
String newTest(String param);
}
對于普通的 Lambda
替換,代碼如下:
public class Main {
public static void main(String[] args) {
Test test = param -> param;
System.out.println(test.newTest("哼哼哼啊啊啊~"));
}
}
而我們注意到該方法其實就是 String
中的構(gòu)造方法,因此我們直接進行構(gòu)造方法引用:
public class Main {
public static void main(String[] args) {
Test test = String::new;
System.out.println(test.newTest("哼哼哼啊啊啊~"));
}
}
Lambda 表達式的本質(zhì)
經(jīng)過上面的學習,相信你已經(jīng)可以熟練地使用 Lambda
表達式了,看起來 Lambda
只是一種簡化匿名內(nèi)部類進行實現(xiàn)接口的語法糖,但實際上,它們是兩種本質(zhì)不同的事物:
- 匿名內(nèi)部類本質(zhì)是一個類,只是不需要我們顯示地指定類名,編譯器會自動為該類取名。
- 而
Lambda
表達式本質(zhì)是一個函數(shù),當然,編譯器也會為它取名,在JVM
層面,這是通過invokedynamic
指令實現(xiàn)的,編譯器會將Lambda
表達式轉(zhuǎn)化為一個私有方法,并在需要的時候動態(tài)地生成一個函數(shù)式接口的實例。
假設(shè)我們使用上述 Runnable
的匿名內(nèi)部類的代碼進行編譯,可以看到結(jié)果如下:
可以看到, Main$1.class
實際上就是 Main
類中生成的匿名內(nèi)部類文件,而將其替換為 Lambda
表達式后編譯的結(jié)果如下:
文章來源:http://www.zghlxwxcb.cn/news/detail-677680.html
沒有生成單獨的類文件,即,匿名內(nèi)部類對應(yīng)的是一個 class
文件,而 Lambda
表達式對應(yīng)的是它所在主類的一個私有方法。文章來源地址http://www.zghlxwxcb.cn/news/detail-677680.html
參考文獻
- Java中的函數(shù)式編程
- Java Lambda 表達式介紹
- 在Java代碼中寫Lambda表達式是種怎樣的體驗
到了這里,關(guān)于淺談 Java 中的 Lambda 表達式的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!