JAVA編程講義之JDK17.JDK8 Lambda表達式
Lambda表達式(Lambda Expression)是JDK8新增的功能,它顯著增強了Java,繼續(xù)保持自身的活力和創(chuàng)造性。它基于數學中的演算得名,是一個匿名函數,即沒有函數名的函數,主要優(yōu)點在于簡化代碼、增強代碼可讀性、并行操作集合等。Lambda表達式正在重塑Java,將影響到后續(xù)Java技術的使用。方法引用可以理解為Lambda表達式的快捷寫法,它比Lambda表達式更加的簡潔,可讀性更高,有更好的重用性。本章將對Lambda表達式和方法引用展開詳細講解。
10.1 Lambda表達式入門
在數學計算中,Lambda表達式指的是一個函數:對于輸入值的部分或全部組合來說,它會指定一個輸出值。在C#、JavaScript里面也都提供了Lambda語法,不同語言對于Lambda的定義可能不太相同,但相同點是Lambda都可當作一個方法,可以輸入不同值來返回輸出值。在Java中,沒有辦法編寫獨立的函數,需要使用方法來代替函數,不過它總是作為對象或類的一部分而存在。現(xiàn)在,Java語言提供的Lambda表達式類似獨立函數,可以看成一種匿名方法,擁有更為簡潔的語法,可以省略修飾符、返回類型、throws語句等,在某些情況下還可以省略參數。Lambda表達式常用于匿名類并實現(xiàn)方法的地方,以便讓Java語法更加簡潔。
10.1.1 函數式編程思想
函數式編程思想是將計算機運算作為函數的計算,函數的計算可隨時調用。函數式編程語言則是一種編程規(guī)范,它將計算機運算視為數學上的函數計算,并且避免使用程序狀態(tài)以及易變對象;函數除了可以被調用以外,還可以作為參數傳遞給一個操作,或者作為操作的結果返回。函數式編程語言重點描述的是程序需要完成什么功能,而不是如何一步一步地完成這些功能。總結來看,函數式編程思想是一種將操作與操作的實施過程進行分離的思想。
誕生50多年之后,函數式編程語言(functional programming)開始獲得越來越多的關注。不僅最古老的函數式語言Lisp重獲青春,而且新的函數式語言層出不窮,如Erlang、clojure、Scala、F#等。目前,最當紅的Python、Javascript、Ruby等語言對函數式編程的支持都很強,就連老牌的面向對象語言Java、面向過程語言PHP,都忙不迭地加入對匿名函數的支持。越來越多的跡象表明,函數式編程已經不僅是學術界的最愛,開始大踏步地在業(yè)界投入實用。
函數式編程與面向對象編程有很大的區(qū)別,它將程序代碼當作數學中的函數,函數本身作為另一個函數的參數或返回值,而面向對象編程則是按照真實世界客觀事物的自然規(guī)律進行分析,客觀世界中存在什么樣的實體,在軟件系統(tǒng)就存在什么樣的實體。函數式編程只是對Java語言的補充。簡而言之,函數式編程盡量忽略面向對象的復雜語法,強調做什么,而不是以什么形式去做;面向對象編程則強調以現(xiàn)實對象的角度來解決問題。
函數式編程思想是Java實現(xiàn)并行處理的基礎,所以Java語言引入了Lambda表達式,開啟了Java語言支持函數式編程 (Functional Programming)新時代,現(xiàn)在很多語言都支持Lambda表達式(不同語言可能叫法不同),如C#、Swift和JavaScript等。為什么Lambda表達式這這么受歡迎呢?這是因為Lambda表達式是實現(xiàn)支持函數式編程的技術基礎。
10.1.2 Lambda表達式語法
第7.3.4節(jié)已經講過匿名內部類,Lambda表達式作用則主要是用于匿名內類的方法實現(xiàn)。為了更好的理解Lambda表達式的概念,這里先從一個案例開始。如例10-1所示,先來使用匿名內部類實現(xiàn)加法運算和減法運算的功能。
例10-1 Demo1001.java
1 ?package com.aaa.p100102;
1
1 ?interface Calc { // 可計算接口
1 ? int calcInt(int x, int y); // 兩個int類型參數
1 ?}
1 ?public class Demo1001 {
1 ? public static Calc calculate(char opr) {
1 ? Calc result;
1 ? if (opr == '+') {
1 ? result = new Calc() { // 匿名內部類實現(xiàn)Calc接口
1 ? @Override
1 ? public int calcInt(int x, int y) { // 實現(xiàn)加法運算方法
1 ? return a + b;
1 ? }
1 ? };
1 ? } else {
1 ? result = new Calc() { // 匿名內部類實現(xiàn)Calc接口
1 ? @Override
1 ? public int calcInt(int x, int y) { // 實現(xiàn)減法運算方法
1 ? return a - b;
1 ? }
1 ? };
1 ? }
1 ? return result;
1 ? }
1 ? public static void main(String[] args) {
1 ? int n1 = 10;
1 ? int n2 = 5;
1 ? Calc f1 = calculate('+'); // 實現(xiàn)加法計算
1 ? Calc f2 = calculate('-'); // 實現(xiàn)減法計算
1 ? System.out.println(n1 + "+" + n2 + "=" + f1.calcInt(n1, n2));
1 ? System.out.println(n1 + "-" + n2 + "=" + f2.calcInt(n1, n2));
1 ? }
1 ?}
程序的運行結果如下:
10+5=15
10-5=5
例10-1中,calculate()方法的參數是具體的操作數,返回值類型是Calc接口,代碼第12行和第19行都采用匿名內部類實現(xiàn)了Calc接口的calcInt()方法,第13行實現(xiàn)加法運算,第20行實現(xiàn)減法運算。
但是,上述使用匿名內部類實現(xiàn)通用方法calculate()的代碼很臃腫。現(xiàn)在,我們采用 Lambda 表達式來替代匿名內部類,修改Demo1001類,修改之后calculate()的代碼如下:
1 ?/**
2 ?* 通過操作符,進行計算
3 ?* @param opr 操作符
4 ?* @return 實現(xiàn)Calc接口對象
5 ?*/
6 ?public static Calc calculate(char opr) {
7 ? Calc result;
8 ? if (opr == '+') {
9 ? result = (int a, int b) -> { // Lambda表達式實現(xiàn)Calc接口
10 ? return a + b;
11 ? };
12 ? } else {
13 ? result = (int a, int b) -> { // Lambda表達式實現(xiàn)Calc接口
14 ? return a - b;
15 ? };
16 ? }
17 ? return result;
18 ?}
代碼第9行和第13行用 Lambda 表達式替代匿名內部類,程序運行結果和案例10-1相同。因為Lambda表達式和匿名類都是為了作為傳遞給方法的參數而設立的,它們都可以把功能像對象一樣傳遞給方法。使用匿名類是向方法傳遞了一個對象,而使用Lambda表達式不需要創(chuàng)建對象,只需要將Lambda表達式傳遞給方法即可。
通過上述演示,可以給Lambda表達式一個定義:Lambda表達式是一個匿名函數(方法)代碼塊,可以作為表達式、方法參數和方法返回值。
完整的Lambda表達式有3個要素,分別是參數列表、箭頭符號、代碼塊,語法格式如下:
(參數列表) -> {
… // Lambda表達式體
}
這里,針對Lambda表達式的3個要素說明如下:
? ?參數列表:當只有一個參數時無需定義圓括號,但無參數或多個參數需要定義圓括號。
? ?箭頭符號:箭頭符號“->”指向后面要做的事情。
? ?代碼塊:如果主體包含了一個語句,就不需要使用大括號{};如果是多行語句,則該語句塊會像方法體一樣被執(zhí)行,并由一個return語句返回到調用者。
下面是幾個Lambda表達式的簡單示例:
(int a,int b) -> a + b; // 兩個參數a和b,返回二者的和
()-79; // 沒有參數,返回整數79
(String str) -> {System.out.println(str);} // String類型參數,打印到控制臺
(int a) -> {return a + 1;} // 以一個整數為參數,返回該數加1后的值
觀察上述幾個簡單示例,除了剛才必備的3個要素之外,發(fā)現(xiàn)Lambda表達式像沒有名字的方法,并且它沒有返回類型、throws子句。實際上,返回類型和異常是由Java編譯器自動從Lambda表達式的代碼塊得到的,如上述示例中最后一個Lambda表達式,由于a為int類型,故而返回類型是int,而throws子句為空。因此,Lambda表達式真正缺少的是方法名稱,從這個角度來講,Lambda表達式可以視為一種匿名方法,這點和匿名類相似。
使用匿名類需要向方法傳遞一個對象,而使用Lambda表達式則不需要創(chuàng)建對象,只需要將表達式傳遞給方法。所以,Lambda表達式語法上比匿名類更加簡單、代碼更少、邏輯上更清晰。
10.2 函數式接口
由于Lambda表達式的返回值類型由代碼塊決定,所以Lambda表達式可以作為“任意類型”的對象傳遞給調用者,具體作為何種類型的對象,取決于調用者的需要。為了能夠確定Lambda表達式的類型,而又不對Java的類型系統(tǒng)做大的修改,Java利用現(xiàn)有的interface接口來作為Lamba表達式的目標類型,這種接口被稱為函數式接口。函數式接口本質上就是只包含一個抽象方法的接口,也可以包含多個默認方法、類方法、但只能聲明一個抽象方法,如果聲明多個抽象方法,則會發(fā)生編譯錯誤。
查看Java 8之后的API文檔,可以發(fā)現(xiàn)大量的函數式接口,如Runnable、ActionListener等。JDK8之后,為函數式接口提供了一個新注解@FunctionalInterface,放在定義的接口前面,用于告知編譯器執(zhí)行更嚴格的檢查,防止在函數式接口中聲明多個抽象方法,即檢查該接口必須是函數式接口,否則編譯器報錯。
由于Lambda表達式的結果被作為對象,在程序中完全可以使用Lambda表達式進行賦值,參考如下代碼:
@FunctionalInterface
public interface Runnable { // Runnable是Java提供的一個接口
public abstract void run(); // Runnable在接口中只包含一個無參數的方法
}
Runnable runnable = () -> {
for(var i = 0;i < 99;i++){
System.out.println(i);
}
};
通過上述代碼可以發(fā)現(xiàn),Lambda表達式代表的匿名方法實現(xiàn)了Runnable接口中唯一的、無參數的方法。
為了保證Lambda表達式的目標類型是一個明確的函數式接口,有如下3種常見方式:
? ?將Lambda表達式賦值給函數式接口類型的變量。
? ?將Lambda表達式作為函數式接口類型的參數傳給某個方法。
? ?使用函數式接口對Lambda表達式進行強制類型轉換。
Lambda表達式可以自行定義函數接口,如果是常用的功能,則有點太麻煩。為了方便開發(fā)者使用,Java已經定義了幾種通用的函數接口,用戶可以基于這些通用接口來編寫程序。JDK1.8新增的函數式接口都放在java.util.function包下,最常用的有4類,如表10.1所示。
表10.1 Java提供的函數式接口
函數式接口
方法名
描述
Consumer
void accept(T t)
消費性接口,提供的是無返回值的抽象方法
Supplier
T get()
提供的是有返無參的抽象方法
Function
R apply(T t)
提供的是 有參 有返的抽象方法
Predicate
boolean test(T t)
提供的有參有返回的方法,返回的是boolean類型的返回值
這里,以Predicate
接下來,通過案例來演示函數式接口傳遞Lambda表達式的功能,如例10-2所示。
例10-2 Demo1002.java
1 ?import java.util.ArrayList;
2 ?import java.util.List;
3
4 ?public class Demo1002 {
5 ? public static void main(String[] args) {
6 ? List
7 ? list.add("Java"); // 向list集合追加數據
8 ? list.add("Es6");
9 ? list.add(null);
10 ? list.add("Html5");
11 ? list.add(null);
12
13 ? System.out.println(list); // 輸出集合中的數據
14 ? list.removeIf((e)-> { // 使用Predicate
15 ? return e == null;}
16 ? );
17 ? System.out.println(list); // 輸出刪除之后的數據
18 ? }
19 ?}
程序運行結果如下:
[Java, Es6, null, Html5, null]
[Java, Es6, Html5]
例10-2中,先創(chuàng)建一個名字為list的ArrayList集合,然后向list中添加了5個元素,接著輸出list集合中元素,然后又通過調用list的removeIf()方法,傳遞符合Predicate函數式接口的Lambda表達式。因為傳遞的元素是判斷參數等于null,所以會刪除list中值為null的元素,接下來在第17行代碼輸出list中的數據,結果發(fā)現(xiàn)值null的元素被刪除了。
綜上所述,函數式接口帶給我們最大的好處就是:可以使用極簡的lambda表達式實例化接口,這點在實際的開發(fā)中很有好處,往往一兩行代碼能夠解決很復雜的場景需求。
注意:@FunctionalInterface注解加或不加對于接口是不是函數式接口沒有影響,該注解只是提醒編譯器去檢查該接口是否僅包含一個抽象方法。
10.3 Lambda表達式的簡化形式
Lambda表達式的核心原則是:只要可以推導,都可以省略,即可以根據上下文推導出來的內容,都可以省略書寫,這樣簡化了代碼,但潛在的問題是有可能會使代碼可讀性變差。本節(jié)介紹Lambda表達式的幾種常用簡化形式。
1.省略大括號
在Lambda表達式中,如果程序代碼塊只包含了一條語句,就可以省略大括號。標準格式和省略形式對比如下:
() -> {System.out.prnitln("一起來跟我學習JAVA的Lambda表達式");} // 標準格式
() -> System.out.prnitln("一起來跟我學習JAVA的Lambda表達式"); // 省略格式
2.省略參數類型
Lambda表達式可以根據上下文環(huán)境推斷出參數類型,所以可以省略參數類型。標準格式和省略形式對比如下:
1 ?interface Calc { // 可計算接口
2 ? int calcInt(int x, int y); // 兩個int類型參數
3 ?}
4 ?public static Calc calculate(char opr) {
5 ? Calc result;
6 ? if (opr == '+') {
7 ? result = (int x,int y) -> { // 標準格式
8 ? return x + y;
9 ? };
10 ? } else {
11 ? result = (x, y) -> { // 省略形式
12 ? return x - y;
13 ? };
14 ? }
15 ? return result;
16 ?}
3.省略小括號
當Lambda表達式中參數只有一個的時候,可以省略參數小括號。下面代碼段使用到了第10.2節(jié)的Consumer函數式接口,Lambda表達式標準格式和省略小括號的格式對比如下:
Consumer
Consumer
consumer.accept("一起來學習Java"); // 簡化形式的調用
4.省略return和大括號
當Lambda表達式的代碼塊中有返回值且有只有一條語句時,那么可以省略return和大括號。注意,二者需要同時省略,否則編譯報錯。下面代碼使用到了Comparator接口,該接口包含compare(T o1,T o2)方法,此方法有兩個泛型參數可以進行比較,會返回int類型值。返回值大于0,表示第1個參數大;返回值等于0,表示兩個參數相同;返回值小于0,表示第2個參數較大。Lambda表達式標準格式和省略形式對比如下:
Comparator
Comparator
System.out.println(com.compare(3,3)); // 簡化形式的調用
上述4種方式是Lambda表達式的簡化形式,代碼簡潔了,對于初學者而言會增加理解難度,一般建議初學者使用標準形式,等熟練掌握Lambda表達式后逐步使用簡化形式。
10.4 訪問變量
Lambda表達式中可以訪問其外層作用域中定義的變量。例如,可以使用其外層類定義的實例或靜態(tài)變量以及調用其外層類定義的方法,也可以顯式或隱式地訪問this變量。
10.4.1 訪問成員變量
成員變量包括實例成員變量和靜態(tài)成員變量。在Lambda表達式中,可以訪問這些成員變量,此時的Lambda表達式與普通方法一樣,可以讀取成員變量,也可以修改成員變量。
接下來,通過案例演示Lambda表達式訪問成員變量的功能,如例10-3所示。
例10-3 Demo1003.java
1 ?package com.aaa.p100401;
2
3 ?interface Calc {
4 ? int calcInt(int x, int y);
5 ?}
6 ?public class Demo1003{
7 ? private int count = 1; // 實例成員變量
8 ? private static int num = 2; // 靜態(tài)成員變量
9 ? public static Calc add() { // 靜態(tài)方法,進行加法運算
10 ? Calc result = (int x, int y) -> {
11 ? num++; // 訪問靜態(tài)成員變量,不能訪問實例成員變量
12 ? int c = x + y + num; // 修改為x + y + num+this.count會報錯
13 ? return c;
14 ? };
15 ? return result;
16 ? }
17 ? public Calc mul() { // 實例方法,進行乘法運算
18 ? Calc result = (int x, int y) -> {
19 ? num++; // 訪問靜態(tài)成員變量和實例成員變量
20 ? this.count++;
21 ? int c = x * y - num - this.count;
22 ? return c;
23 ? };
24 ? return result;
25 ? }
26
27 ? // 測試方法
28 ? public static void main(String[] args) {
29 ? System.out.println("靜態(tài)方法,加法運算:" + add().calcInt(4, 3));
30 ? System.out.println("實例方法,減法運算:" + new Demo1003().mul().calcInt(4, 3));
31 ? }
32 ?}
程序的運行結果如下:
運行結果為:13
從程序運行結果來看,例10-3中聲明了一個實例成員變量count和一個靜態(tài)成員變量num。此外,還聲明了靜態(tài)方法add()和實例方法sub()。add()方法是靜態(tài)方法,靜態(tài)方法中不能訪問實例成員變量,所以第12行代碼的Lambda表達式中也不能訪問實例成員變量,在代碼“x+y+num”后加上this.count會報錯。sub()方法是實例方法,實例方法中能夠訪問靜態(tài)成員變量和實例成員變量,所以第18行代碼的Lambda表達式中可以訪問這些變量,當然實例方法和靜態(tài)方法也可以訪問,當訪問實例成員變量或實例方法時可以使用this,在不與局部變量發(fā)生沖突情況下可以省略this。
10.4.2 捕獲局部變量
對于成員變量的訪問,Lambda表達式與普通方法沒有區(qū)別,但是有時候Lambda表達式需要訪問外部作用域代碼中的變量,這些變量不是在函數體內定義的,是在Lambda表達式所處的上下文中定義的,這稱為變量捕獲或變量綁定。當Lambda表達式發(fā)生變量捕獲時,系統(tǒng)編譯器會將變量當成final的。因此,這些變量在聲明時,可以不定義成final,并且Lambda表達式中不能修改那些捕獲的變量。
接下來,通過案例演示如何捕獲局部變量,如例10-4所示。
例10-4 Demo1004.java
1 ?package com.aaa.p100402;
2 ?import java.util.Comparator;
3
4 ?public class Demo1004 {
5 ? public static void main(String[] args) {
6 ? test();
7 ? }
8 ? static void test(){
9 ? Integer a = 222;
10 ? Comparator
11 ? // 如果取消注釋會報錯,下面會詳細解釋
12 ? // a++;
13 ? return Integer.compare(x,y);
14 ? };
15 ? System.out.println("兩個數字比較結果為:" + com.compare(22,33));
16 ? }
17 ?}
程序的運行結果如下:
兩個數字比較結果為:-1
例10-4中,使用Comparator
Lambda表達式的代碼塊可以訪問外部作用域的變量,意味著Lambda表達式的方法體與外部作用域的代碼塊有相同的作用域范圍,所以在Lambda表達式范圍內不允許聲明一個與局部變量名相同的參數或局部變量。如果把第10行代碼中的變量x修改為a,程序編譯報錯,提示“Variable 'a' is already defined in the scope”,中文含義為“變量 a已經在局部范圍內定義”,出現(xiàn)錯誤的原因就是,在方法test()內部不能有兩個同名的變量,所以不能在Lambda表達式中定義已經存在的變量。
10.5 方法引用
方法引用是用來直接訪問類或者實例的已經存在的方法或者構造方法。方法引用提供了一種引用而不執(zhí)行方法的方式,它需要由兼容的函數式接口構成目標類型上下文。計算時,方法引用會創(chuàng)建函數式接口的一個實例。
當Lambda表達式中只是執(zhí)行一個方法調用時,不用Lambda表達式,直接通過方法引用的形式可讀性更高一些。方法引用可以理解為lambda表達式的快捷寫法,它比lambda表達式更加簡潔,可讀性更高,有更好的重用性。如果Lambda表達式的代碼塊只有一條代碼,可以在代碼塊中使用方法引用?。方法引用本質上是一個Lambda表達式,使用的是雙冒號"::"操作符。方法引用有4種形式,如表10.2所示。
表10.2 方法引用的四種形式
方法引用方式
示例
引用類的靜態(tài)方法
ClassName::staticMethodName
引用對象的實例方法
Object::instanceMethodName
引用類的實例方法
ClassName::methodName
引用構造方法
ClassName::new
10.5.1 引用類的靜態(tài)方法
引用類方法,其實就是引用類的靜態(tài)方法,語法格式和示例如下:
格式: ClassName::staticMethodName
示例: String::valueOf
如果函數式接口的實現(xiàn)恰好可以通過調用一個靜態(tài)方法來實現(xiàn),那么就可以使用靜態(tài)方法引用。此時,類是靜態(tài)方法動作的發(fā)起者。假如 Lambda 表達式符合如下格式:
([變量1, 變量2, ...]) -> 類名.靜態(tài)方法名([變量1, 變量2, ...])
可以簡寫成如下格式:
類名::靜態(tài)方法名([變量1, 變量2, ...])
注意,這里靜態(tài)方法名后面不需要加括號,也不用加參數,因為編譯器都可以推斷出來。具體參考下列等價代碼格式:
String::valueOf等價于Lambda表達式 (str) -> String.valueOf(str)
Math::pow等價于Lambda表達式 (a,b) -> Math.pow(a, b);
接下來,通過案例來演示引用類的靜態(tài)方法,如例10-5所示。
例10-5 Demo1005.java
1 ?package com.aaa.p100501;
1
2 ?interface Converter { // 接口負責將String類型轉換為Integer
3 ? Integer change(String s);
4 ?}
5 ?public class Demo1005 {
6 ? public static void main(String[] args) {
7 ? Converter c1 = s -> Integer.parseInt(s); // Lambda表達式的寫法
8 ? Integer v1 = c1.change("8319");
9 ? System.out.println("Lambda表達式輸出:" + v1);
10 ? Converter c2 = Integer::parseInt; // 引用方法的寫法;
11 ? Integer v2 = c2.change("8319");
12 ? System.out.println("方法引用輸出:" + v1);
13 ? }
14 ?}
程序的運行結果如下:
Lambda表達式輸出:8319
方法引用輸出:8319
例10-5中,第3~5行定義了一個函數式接口Converter,在接口中定義了一個change()抽象方法,該方法負責將String參數轉換為Integer類型。第8行代碼使用Lambda表達式來創(chuàng)建一個Converter對象,由于其代碼塊只有一條語句,因此省略了大括號,并將這條語句的值作為返回值。第9行代碼調用Converter的對象c1,因為c1對象是Lambda表達式創(chuàng)建的,c1的change()方法體就是Lambda表達式的代碼塊部分,因此第10行代碼輸出結果為8319。
由于上面的Lambda表達式的代碼塊只有一行調用類方法的代碼,所以可以用類的方法引用來替換。第11行代碼也就是調用Integer類的parseInt()方法來實現(xiàn)Converter函數式接口中唯一的抽象方法change(),當調用change()方法時,調用參數會傳給Integer類的parseInt()類方法,因此第13行代碼輸出結果也為8319。
從程序運行結果可以看到,使用引用類方法和Lambda表達式,效果是一致的。可以說引用類方法是Lambda表達式的孿生兄弟,二者可以起到異曲同工的效果。
10.5.2 引用類的實例方法
引用對象的實例方法,其實就是引用類中的成員方法,語法格式和示例如下:
格式: Object::instanceMethodName
示例: String::substring
這和類調用靜態(tài)方法不相同,動作的發(fā)起者是ClassName類所創(chuàng)建的任意一個對象,只不過在方法調用的時候需要將引用對象作為參數輸入方法中,并且規(guī)定此對象一定要位于方法參數的第1個。如果 Lambda 表達式的“->”的右邊要執(zhí)行的表達式是調用的“->”的左邊第1個參數的某個實例方法,并且從第2個參數開始(或無參)對應到該實例方法的參數列表時,就可以使用這種方法。假如Lambda 表達式符合如下格式:
(變量1[, 變量2, ...]) -> 變量1.實例方法([變量2, ...])
代碼就可以簡寫成如下格式:
變量1對應的類名::實例方法名
接下來,通過實例來演示引用類的實例方法,如例10-6所示。
例10-6 Demo1006.java
1 ?package com.aaa.p100502;
1
2 ?interface MyString{
3 ? // 從開始位置startX開始到endX截取字符串
4 ? String mySubString(String s,int startX,int endX);
5 ?}
6 ?public class Demo1006 {
7 ? private static void useMyString(MyString myStr){
8 ? String str = myStr.mySubString("AAA軟件教育歡迎您!",0,7);
9 ? System.out.println(str);
10 ? }
11 ? public static void main(String[] args) {
12 ? // 方式1:使用Lambda表達式的標準格式
13 ? useMyString((String s,int startX,int endX)->{
14 ? return s.substring(startX,endX);
15 ? });
16
17 ? useMyString((s,x,y)->s.substring(x,y)); // 方式2:使用Lambda表達式的簡化格式
18 ? useMyString(String::substring); // 方式3:使用類的實例方法引用格式
19 ? }
20 ?}
程序的運行結果如下:
AAA軟件教育
AAA軟件教育
AAA軟件教育
例10-6中,定義了一個接口MyString,包含一個抽象方法mySubString(),該方法負責根據String、int、int這3個參數生成一個String類型的返回值。第8~11行代碼定義了一個靜態(tài)的方法useMyString(),方法的參數是MyString類型的接口,因為Lambda表達式可以作為函數式接口的參數。在下面的main()方法中對靜態(tài)方法useMyString()進行了3次調用,分別是Lambda表達式的標準格式、簡化格式、方法引用格式。
從程序運行結果來看,從Lambda表達式到引用類的實例方法,結果都是一樣的。Lambda表達式的簡化格式是對標準格式的精簡,這個在第10.3節(jié)已經詳細講解過。而引用類的實例方法格式更為簡單,Lambda表達式被引用類的實例方法替代的時候,第1個調用參數作為substring()方法的調用者,剩下的參數會作為substring()實例方法的調用參數。
10.5.3 引用對象的實例方法
引用對象的實例方法,其實就引用類中的成員方法。這種語法與引用靜態(tài)方法的語法類似,只不過這里使用對象引用而不是類名,語法格式和示例如下:
格式:Object::instanceMethodName
范例:"helloWorld":: toUpperCase
此時,對象是方法動作的發(fā)起者。當要執(zhí)行的表達式是調用某個對象的方法,并且這個方法的參數列表和接口里抽象函數的參數列表一一對應時,就可以采用引用對象的方法的格式。
假如 Lambda 表達式符合如下格式:
([變量1, 變量2, ...]) -> 對象引用.方法名([變量1, 變量2, ...])
以簡寫成如下格式:
對象引用::方法名
例如,"helloWorld":toUpperCase()等價于Lambda表達式 () ->str.toUpperCase(),該實例方法引用就是調用了"helloWorld"的toUpperCase()實例方法來實現(xiàn)之前Lambda表達式中的抽象方法。
接下來,通過案例來演示引用對象的實例方法,如例10-7所示。
例10-7 Demo1007.java
1 ?package com.aaa.p100503;
2 ?import java.util.Arrays;
3 ?import java.util.List;
4
5 ?interface Converter{
6 ? Integer change(String from);
7 ?}
8 ?public class Demo1007 {
9 ? public static void main(String[] args) {
10 ? Converter c1 = from -> "www.3adazuo.cn".indexOf(from);
11 ? Integer value = c1.change("it");
12 ? System.out.println("3a在www.3adazuo.cn中的位置:" + value);
13 ? // 方法引用形式的輸出
14 ? Converter c2 = "www.3adazuo.cn"::indexOf;
15 ? System.out.println("3a在www.3adazuo.cn中的位置:" + c2.change("3a"));
16 ? }
17 ?}
程序的運行結果如下:
3a在www.3adazuo.cn中的位置:4
3a在www.3adazuo.cn中的位置:4
例10-7中,定義了一個接口Converter,包含一個change()抽象方法,負責將String參數轉換為Integer參數。第10行的Lambda表達式實現(xiàn)實現(xiàn)了該接口的change()方法,所以可以將表達式中代碼塊的值作為返回值。接著,第11行代碼將調用c1對象的change()方法將字符串轉換為整數了,由于c1對象是Lambda表達式創(chuàng)建的,change()方法的執(zhí)行體就是Lambda表達式的代碼塊部分,所以第12行代碼的輸出結果返回“3a”在字符串“www.3adazuo.cn”中的位置4。第14行代碼的實例方法引用,表示調用“www.3adazuo.cn”對象的indexOf()實例方法來實現(xiàn)Converter函數式接口中唯一的抽象方法change(),當調用該接口中的change()方法時,參數“3a”會傳遞給“www.3adazuo.cn”的indexOf()實例方法,相當于"www.3adazuo.cn".indexOf("3a"),同樣輸出結果為4。
10.5.4 引用構造方法
方法引用用來重用現(xiàn)有API的方法流程,JDK還提供了構造方法引用,用來重用現(xiàn)有API的對象構建流程。構造器引用同方法引用類似,不同的是在構造器引用中方法名是new,語法格式和示例如下:
格式:類名::new
范例:Customer::new。
對于擁有多個構造器的類,選擇使用哪個構造器取決于上下文。方法引用有返回值類型,構造方法在語法上沒有返回值類型。事實上,每個構造方法都會有返回值類型,也就是該類自身。
接下來,通過案例來演示引用構造方法,如例10-8所示。
例10-8 Demo1008.java
1 ?package com.aaa.p100504;
2 ?import java.util.ArrayList;
3 ?import java.util.*;
4 ?import java.util.function.Function;
5
6 ?class Customer {
7 ? String name;
8 ? public Customer(String name) {
9 ? this.name = name;
10 ? }
11 ? @Override
12 ? public String toString() {
13 ? return "Customer{" + "name='" + name + '\'' + '}';
14 ? }
15 ?}
16 ?public class Demo1008 {
17 ? static
List list, Function mapper){ 18 ? List 19 ? for(int n = 0;n < list.size();n++){ 20 ? mapped.add(mapper.apply(list.get(n))); 21 ? } 22 ? return mapped; 23 ? } 24 ? public static void main(String[] args) { 25 ? List 26 ? List 27 ? for(Customer c:customers) 28 ? System.out.println(c); 29 ? } 30 ?} 程序的運行結果如下: Customer{name='扁鵲'} Customer{name='華佗'} 例10-8中,第6~15行代碼定義了Customer 類,包含一個name成員變量,帶一個參數的構造方法和toString()方法。第17~23行代碼定義了一個map()方法,使用了Function函數式接口,該接口定義的一個apply(T t)抽象方法必須重寫,該方法的作用是指定將獲取的P類型數據轉換為R類型,這里是先將數據放到mapped集合,然后轉換為Customer的實例。第25行代碼使用Arrays類的asList()方法對集合進行了賦值操作,第26行代碼調用map()方法及其構造器引用將names集合轉換為Customer的實例。第27行和第28行代碼針對集合中的數據進行了遍歷輸出。 從程序運行結果來看,構造器引用在使用new關鍵字的時候,不需要使用類名和參數,由類內的方法直接調用Lambda表達式進行構造器方法調用即可。 知識點撥:如果有多個同名的重栽方法,編譯器就會嘗試從上下文中找出你指的那一個方法。例如,Math.max 方法有4個版本,參數類型分別是int、long、float、double。選擇哪一個版本取決于Math::max 轉換為哪個函數式接口的方法參數。類似于 lambda 表達式,方法引用不能獨立存在,總是會轉換為函數式接口的實例。 10.6 Lambda表達式調用Arrays的類方法 Arrays類的一些方法需要Comparator、 XxxOperator、XxxFunction等接口的實例,這些接口都是函數式接口,因此可以使用Lambda表達式來調用Arrays的方法。 接下來,通過案例來演示使用Lambda表達式調用Arrays的類方法,如10-11所示。 例10-11 Demo1009.java 1 ?package com.aaa.p1006; 1 ?import java.util.Arrays; 1 1 ?public class Demo1009 { 1 ? public static void main(String[] args) { 1 ? String[] arr = new String[] { "CSDN","51CTO", "ITEye", "cnblogs" }; 1 ? Arrays.parallelSort(arr,(s1, s2) -> s1.length() - s2.length()); 1 ? System.out.println("排序:"+Arrays.toString(arr)); 1 ? int[] intArray = new int[] {3, 9, 8, 0}; 1 ? // left代表數組中前一個索引處的元素,計算第1個元素時left為1 1 ? // right代表數組中當前索引處的元素 1 ? Arrays.parallelPrefix(intArray, (left, right) -> left * right); 1 ? System.out.println("累積:" + Arrays.toString(intArray)); 1 ? long[] longArray = new long[5]; 1 ? // operand代表正在計算的元素索引 1 ? Arrays.parallelSetAll(longArray, operand -> operand * 5); 1 ? System.out.println("索引*5:" + Arrays.toString(longArray)); 1 ? } 1 ?} 程序的運行結果如下: 排序:[CSDN, 51CTO, ITEye, CNBLOGS] 累積:[3, 27, 216, 0] 索引*5:[0, 5, 10, 15, 20] 通過程序運行結果可以發(fā)現(xiàn),第7行代碼的Lambda表達式的目標類型是Comparator,該接口指定了判斷字符串大小的標準;第12行代碼的Lambda表達式的目標類型是IntBinaryOperator,該對象將會根據前后兩個元素來計算當前元素的值;第16行代碼Lambda表達式的目標類型是IntToLongFunction,該對象將根據元素的索引來計算當前元素的值。通過本案例可以發(fā)現(xiàn),Lambda表達式能夠使程序更加簡潔,代碼更加簡單。 10.7 本章小結 ? ?了解函數式編程思想是一種將操作與操作的實施過程進行分離的思想。 ? ?Lambda表達式是匿名類的一種簡化,可以部分取代匿名類的作用。了解Lambda的使用場景,多用于匿名類的替代,方法引用等。 ? ?掌握Lambda表達式的基本語法,理解Lambda表達式的簡化使用方式。 ? ?理解Lambda對于函數式接口的關系,如何作為該接口的目標類型使用,了解四類常見函數式接口。 ? ?理解Lambda表達式如何訪問成員變量和捕獲局部變量。 ? ?理解方法引用的概念,掌握方法引用的使用。 ? ?理解Lambda表達式調用Arrays的類方法。 ? ?了解Lambda表達式是后續(xù)集合章節(jié),后繼框架技術的基礎。 10.8 理論試題與實踐練習 1.填空題 1.1 Lambda表達式允許創(chuàng)建只有一個____________的接口。 1.2 函數式接口可以包含多個_______、________,但只能聲明一個抽象方法。 1.3 方法引用和_________可以讓Lambda表達式的代碼塊更加簡單。 1.4 Lambda表達式重用API現(xiàn)有的方法,也稱為_____________________。 2.選擇題 2.1 方法引用不包括下面哪個( ) A.引用類方法 B.引用對象的實例方法 C.引用類的實例方法 D.迭代方法 2.2 下面哪個不是Arrays 函數式接口的實例( ) A.Comparator B.XXXFunction C.XXXOperator D.list 3.編程題 3.1 請從鍵盤隨機輸入10個整數保存到List中,并使用Lambda表達式進行遍歷。 3.2 定義一個函數式接口,使用Lambda表達式來實現(xiàn)其實例。 Java JDK
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。