作者:Deepak Vohra
學(xué)習(xí)如何充分利用 lambda 和虛擬擴(kuò)展方法。
2013 年 8 月發(fā)布
Lambda 表達(dá)式也稱為閉包,是匿名類的簡(jiǎn)短形式。Lambda 表達(dá)式簡(jiǎn)化了單一抽象方法聲明接口的使用,因此 lambda 表達(dá)式也稱為功能接口。在 Java SE 7 中,單一方法接口可使用下列選項(xiàng)之一實(shí)現(xiàn)。
可以使用 lambda 表達(dá)式實(shí)現(xiàn)功能接口,無(wú)需創(chuàng)建類或匿名類。Lambda 表達(dá)式只能用于單一方法聲明接口。
Lambda 表達(dá)式旨在支持多核處理器架構(gòu),這種架構(gòu)依賴于提供并行機(jī)制的軟件,而該機(jī)制可以提高性能、減少完成時(shí)間。
Lambda 表達(dá)式具有以下優(yōu)點(diǎn):
要跟隨本文中的示例,請(qǐng)下載并安裝以下軟件:
Lambda 表達(dá)式的語(yǔ)法如下所示。
(formal parameter list) ->{ expression or statements }
參數(shù)列表是一個(gè)逗號(hào)分隔的形式參數(shù)列表,這些參數(shù)與功能接口中單一方法的形式參數(shù)相對(duì)應(yīng)。指定參數(shù)類型是可選項(xiàng);如果未指定參數(shù)類型,將從上下文推斷。
參數(shù)列表必須用括號(hào)括起來(lái),但當(dāng)指定的單一參數(shù)不帶參數(shù)類型時(shí)除外;指定單一形式參數(shù)時(shí)可以不帶括號(hào)。如果功能接口方法不指定任何形式參數(shù),則必須指定空括號(hào)。
參數(shù)列表后面是 ->
運(yùn)算符,然后是 lambda 主體,即單一表達(dá)式或語(yǔ)句塊。Lambda 主體的結(jié)果必須是下列值之一:
void
,如果功能接口方法的結(jié)果是 void
Lambda 主體根據(jù)以下選項(xiàng)之一返回結(jié)果:
return
語(yǔ)句返回值。void
,可以提供一個(gè) return
語(yǔ)句,但這不是必需的。語(yǔ)句塊必須包含在大括號(hào) ({}
) 內(nèi),除非語(yǔ)句塊是一個(gè)方法調(diào)用語(yǔ)句,而其調(diào)用的方法的結(jié)果是 void
。Lambda 主體的結(jié)果必須與功能接口中單一方法的結(jié)果相同。例如,如果功能接口方法的結(jié)果是 void
,則 lambda 表達(dá)式主體不能返回值。如果功能接口方法具有返回類型 String
,則 lambda 表達(dá)式主體必須返回 String
。如果 lambda 主體是一條語(yǔ)句,并且該方法具有一個(gè)返回類型,則該語(yǔ)句必須是 return
語(yǔ)句。調(diào)用 lambda 表達(dá)式時(shí),將運(yùn)行 lambda 主體中的代碼。
Lambda 表達(dá)式與功能接口一起使用,功能接口實(shí)際上是一種只有一個(gè)抽象方法的接口;功能接口可以包含一個(gè)同時(shí)也存在于 Object
類中的方法。功能接口的示例有 java.util.concurrent.Callable
(具有單一方法 call()
)和 java.lang.Runnable
(具有單一方法 run()
)。
區(qū)別在于,匿名接口類需要指定一個(gè)實(shí)例創(chuàng)建表達(dá)式,以便接口和編譯器用來(lái)創(chuàng)建接口實(shí)現(xiàn)類的實(shí)例。與指定接口類型(或類類型)的匿名類不同,lambda 表達(dá)式不指定接口類型。從上下文推斷為其調(diào)用 lambda 表達(dá)式的功能接口,也稱為 lambda 表達(dá)式的目標(biāo)類型。
Lambda 表達(dá)式有一個(gè)隱式的目標(biāo)類型與之關(guān)聯(lián),因?yàn)槲疵鞔_指定接口類型。在 lambda 表達(dá)式中,lambda 轉(zhuǎn)換的目標(biāo)類型必須是一個(gè)功能接口。從上下文推斷目標(biāo)類型。因此,lambda 表達(dá)式只能用在可以推斷目標(biāo)類型的上下文中。此類上下文包括
要在 Eclipse IDE 中使用 Java 8,您需要下載一個(gè)支持 JDK 8 的 Eclipse 版本。
圖 1
接下來(lái),我們將通過(guò)一些示例討論如何使用 lambda 表達(dá)式。
Hello
應(yīng)用程序我們都很熟悉 Hello
應(yīng)用程序,當(dāng)我們提供一個(gè)姓名時(shí),它會(huì)輸出一條消息。Hello
類聲明了兩個(gè)字段、兩個(gè)構(gòu)造函數(shù)和一個(gè) hello()
方法來(lái)輸出消息,如下所示。
public class Hello { String firstname; String lastname; public Hello() {} public Hello(String firstname, String lastname) { this.firstname = firstname; this.lastname = lastname;} public void hello() { System.out.println("Hello " + firstname + " " + lastname);} public static void main(String[] args) { Hello hello = new Hello(args[0], args[1]); hello.hello(); }}
現(xiàn)在,我們來(lái)看看 lambda 表達(dá)式如何簡(jiǎn)化 Hello
示例中的語(yǔ)法。首先,我們需要?jiǎng)?chuàng)建一個(gè)功能接口,該接口包含一個(gè)返回“Hello”消息的方法。
interface HelloService {String hello(String firstname, String lastname); }
創(chuàng)建一個(gè) lambda 表達(dá)式,它包含兩個(gè)參數(shù),與接口方法的參數(shù)相匹配。在 lambda 表達(dá)式的主體中,使用 return
語(yǔ)句創(chuàng)建并返回根據(jù) firstname
和 lastname
構(gòu)造的“Hello”消息。返回值的類型必須與接口方法的返回類型相同,并且 lambda 表達(dá)式的目標(biāo)必須是功能接口 HelloService
。參見(jiàn)清單 1。
public class Hello { interface HelloService { String hello(String firstname, String lastname); } public static void main(String[] args) { HelloService helloService=(String firstname, String lastname) -> { String hello="Hello " + firstname + " " + lastname; return hello; };System.out.println(helloService.hello(args[0], args[1])); }}
清單 1
我們需要先為 hello()
的方法參數(shù)提供一些程序參數(shù),然后才能運(yùn)行 Hello
應(yīng)用程序。在 Package Explorer 中右鍵單擊 Hello.java
,然后選擇 Run As > Run Configurations。在 Run Configurations 中,選擇 Arguments 選項(xiàng)卡,在 Program arguments 字段中指定參數(shù),然后單擊 Apply。然后單擊 Close。
要運(yùn)行 Hello.java
應(yīng)用程序,在 Package Explorer 中右鍵單擊 Hello.java
,然后選擇 Run As > Java Application。應(yīng)用程序的輸出將顯示在 Eclipse 控制臺(tái)中,如圖 2 所示。
圖 2
Lambda 表達(dá)式不會(huì)定義新的作用域;lambda 表達(dá)式的作用域與封閉作用域相同。例如,如果 Lambda 主體聲明的局部變量與封閉作用域內(nèi)的變量重名,將產(chǎn)生編譯器錯(cuò)誤 Lambda expression's local variable i cannot re-declare another local variable defined in an enclosing scope
,如圖 3 所示。
圖 3
局部變量無(wú)論是在 lambda 表達(dá)式主體中聲明,還是在封閉作用域中聲明,使用之前都必須先初始化。要證明這一點(diǎn),請(qǐng)?jiān)诜忾]方法中聲明一個(gè)局部變量:
int i;
在 lambda 表達(dá)式中使用該局部變量。將產(chǎn)生編譯器錯(cuò)誤 The local variable i may not have been initialized
,如圖 4 所示。
圖 4
lambda 表達(dá)式中使用的變量必須處于終態(tài)或等效終態(tài)。要證明這一點(diǎn),請(qǐng)聲明并初始化局部變量:
int i=5;
給 lambda 表達(dá)式主體中的變量賦值。將產(chǎn)生編譯器錯(cuò)誤 Variable i is required to be final or effectively final
,如圖 5 所示。
圖 5
可按如下方式將變量 i
聲明為終態(tài)。
final int i=5;
否則,該變量必須為等效終態(tài),即不能在 lambda 表達(dá)式中對(duì)該變量賦值。封閉上下文中的方法參數(shù)變量和異常參數(shù)變量也必須處于終態(tài)或等效終態(tài)。
Lambda 主體中的 this
和 super
引用與封閉上下文中一樣,因?yàn)?lambda 表達(dá)式不會(huì)引入新的作用域,這與匿名類不同。
Lambda 表達(dá)式實(shí)際上是一種匿名方法實(shí)現(xiàn);指定形式參數(shù),并使用 return
語(yǔ)句返回值。匿名方法必須按照以下規(guī)則所規(guī)定的與其實(shí)現(xiàn)的功能接口方法兼容。
void
,則 lambda 主體必須與 void 兼容。如果返回一個(gè)值,則 lambda 主體必須與值兼容。返回值的類型可以是功能接口方法聲明中返回類型的子類型。throws
子句中聲明了異常類型或異常超類型的異常。要證明如果功能接口方法返回一個(gè)結(jié)果,lambda 表達(dá)式也必須返回一個(gè)結(jié)果,請(qǐng)?jiān)谀繕?biāo)類型為 HelloService
的 lambda 表達(dá)式中注釋掉 return
語(yǔ)句。因?yàn)楣δ芙涌?HelloService
中的 hello()
方法具有一個(gè) String
返回類型,因此產(chǎn)生編譯器錯(cuò)誤,如圖 6 所示。
圖 6
如果功能接口方法將結(jié)果聲明為 void
,而 lambda 表達(dá)式返回了一個(gè)值,那么將產(chǎn)生編譯器錯(cuò)誤,如圖 7 所示。
圖 7
如果 lambda 表達(dá)式簽名與功能接口方法簽名不完全相同,將產(chǎn)生編譯器錯(cuò)誤。要證明這一點(diǎn),請(qǐng)使 lambda 表達(dá)式參數(shù)列表為空,同時(shí)功能接口方法聲明兩個(gè)形式參數(shù)。將產(chǎn)生編譯器錯(cuò)誤 Lambda expression's signature does not match the signature of the functional interface method
,如圖 8 所示。
圖 8
可變參數(shù)與數(shù)組參數(shù)之間不作區(qū)分。例如,功能接口方法按如下方式聲明數(shù)組類型參數(shù):
interface Int { void setInt(int[] i); }
Lambda 表達(dá)式的參數(shù)列表可以聲明可變參數(shù):
Int int1 =(int... i)->{};
Lambda 表達(dá)式主體拋出的異常不能超出功能接口方法的 throws
子句中指定的異常數(shù)。如果 lambda 表達(dá)式主體拋出異常,功能接口方法的 throws
子句必須聲明相同的異常類型或其超類型。
要證明這一點(diǎn),在 HelloService
接口的 hello
方法中不聲明 throws
子句,從 lambda 表達(dá)式主體拋出異常。將產(chǎn)生編譯器錯(cuò)誤 Unhandled exception type Exception
,如圖 9 所示。
圖 9
如果在功能接口方法中添加與所拋出異常相同的異常類型,編譯器錯(cuò)誤將得以解決,如圖 10 所示。但如果使用以 lambda 表達(dá)式結(jié)果賦值的引用變量來(lái)調(diào)用 hello
方法,將產(chǎn)生編譯器錯(cuò)誤,因?yàn)?main
方法中未對(duì)異常進(jìn)行處理,如圖 10 所示。
圖 10
Lambda 表達(dá)式的類型是從目標(biāo)類型推導(dǎo)出來(lái)的類型。相同的 lambda 表達(dá)式在不同的上下文中可以有不同的類型。此類表達(dá)式稱為多態(tài)表達(dá)式。要證明這一點(diǎn),請(qǐng)定義與 HelloService
具有相同抽象方法簽名的另一功能接口,例如:
interface HelloService2 { String hello(String firstname, String lastname); }
例如,相同的 lambda 表達(dá)式(下面的表達(dá)式)可用于所聲明的方法具有相同簽名、返回類型以及 throws
子句的兩個(gè)功能接口:
(String firstname, String lastname) -> { String hello = "Hello " + firstname + " " + lastname; return hello; }
沒(méi)有上下文,前面的 lambda 表達(dá)式?jīng)]有類型,因?yàn)樗鼪](méi)有目標(biāo)類型。但是,如果在具有目標(biāo)類型的上下文中使用,則 lambda 表達(dá)式可以根據(jù)目標(biāo)類型具有不同的類型。在以下兩種情況下,前面的 lambda 表達(dá)式具有不同的類型,因?yàn)槟繕?biāo)類型不同:HelloService
和 HelloService2
。
HelloService helloService =(String firstname, String lastname) -> { String hello = "Hello " + firstname + " " + lastname; return hello; };
HelloService2 helloService2 =(String firstname, String lastname) -> { String hello = "Hello " + firstname + " " + lastname; return hello; };
不支持泛型 Lambda。Lambda 表達(dá)式不能引入類型變量。
java.awt
軟件包中的 GUI 組件使用 java.awt.event.ActionListener
接口注冊(cè)組件的操作事件。java.awt.event.ActionListener
接口是一個(gè)只有一個(gè)方法的功能接口:actionPerformed(ActionEvent e)
。
使用 addActionListener(ActionListener l)
方法將 java.awt.event.ActionListener
注冊(cè)到組件。例如,可以使用應(yīng)用程序中的匿名內(nèi)部類按如下方式將 java.awt.event.ActionListener
注冊(cè)到 java.awt.Button
組件,用以計(jì)算 Button
對(duì)象(稱為 b
)被單擊的次數(shù)。(更多詳細(xì)信息,請(qǐng)參見(jiàn)“如何編寫操作監(jiān)聽(tīng)程序”)
b.addActionListener (new ActionListener() { int numClicks = 0; public void actionPerformed(ActionEvent e) { numClicks++; text.setText("Button Clicked " + numClicks + " times"); } });
可以用 Lambda 表達(dá)式代替匿名內(nèi)部類,使語(yǔ)法更加簡(jiǎn)潔。以下是使用 lambda 表達(dá)式將 ActionListener
注冊(cè)到 ActionListener
組件的一個(gè)示例:
b.addActionListener(e -> { numClicks++; text.setText("Button Clicked " + numClicks + " times"); }); }
對(duì)于只有一個(gè)參數(shù)的 lambda 表達(dá)式,用于指定參數(shù)的括號(hào)可以省略。Lambda 表達(dá)式的目標(biāo)類型(功能接口 ActionListener
)是從上下文(即一個(gè)方法調(diào)用)推斷出來(lái)的。
在本節(jié)中,我們將討論一些常見(jiàn)的功能接口如何與 lambda 表達(dá)式結(jié)合使用。
FileFilter
接口FileFilter
接口具有單一方法 accept()
,用于篩選文件。在 Java 教程 ImageFilter
示例中,ImageFilter
類實(shí)現(xiàn)了 FileFilter
接口,并提供了 accept()
方法的實(shí)現(xiàn)。accept()
方法用來(lái)使用 Utils
類只接受圖像文件(和目錄)。
我們可以使用一個(gè)返回 boolean
的 lambda 表達(dá)式提供 FileFilter
接口的實(shí)現(xiàn),如清單 2 所示。
import java.io.FileFilter;import java.io.File;public class ImageFilter { public static void main(String[] args) { FileFilter fileFilter = (f) -> { String extension = null; String s = f.getName(); int i = s.lastIndexOf('.'); if (i > 0 && i << s.length() - 1) { extension = s.substring(i + 1).toLowerCase(); } if (extension != null) { if (extension.equals("tiff") || extension.equals("tif") || extension.equals("gif") || extension.equals("jpeg") || extension.equals("jpg") || extension.equals("png") || extension.equals("bmp")) { return true; } else { return false; } } return false; }; File file = new File("C:/JDK8/Figure10.bmp"); System.out.println("File is an image file: " + fileFilter.accept(file)); }}
清單 2
Eclipse 控制臺(tái)顯示了 ImageFilter
類的輸出,如圖 11 所示。
圖 11
Runnable
接口在 Java 教程“定義和啟動(dòng)線程”一節(jié)中,HelloRunnable
類實(shí)現(xiàn)了 Runnable
接口,并使用 HelloRunnable
類的實(shí)例創(chuàng)建了 Thread
??梢允褂?lambda 表達(dá)式創(chuàng)建 Thread
的 Runnable
,如清單 3 所示。Lambda 表達(dá)式?jīng)]有 return
語(yǔ)句,因?yàn)?Runnable
中 run()
方法的結(jié)果是 void
。
import java.lang.Runnable;public class HelloRunnable { public static void main(String args[]) { (new Thread(() -> { System.out.println("Hello from a thread"); })).start(); }}
HelloRunnable
類的輸出如圖 12 所示。
圖 12
Callable
接口如果我們創(chuàng)建了一個(gè)實(shí)現(xiàn) java.util.concurrent.Callable<V>
泛型功能接口的類,該類需要實(shí)現(xiàn) call()
方法。在清單 4 中,HelloCallable
類實(shí)現(xiàn)了參數(shù)化類型 Callable<String>
。
package lambda;import java.util.concurrent.Callable;public class HelloCallable implements Callable<String> { @Override public String call() throws Exception { return "Hello from Callable"; } public static void main(String[] args) { try { HelloCallable helloCallable = new HelloCallable(); System.out.println(helloCallable.call()); } catch (Exception e) { System.err.println(e.getMessage()); } }}
清單 4
我們可以使用 lambda 表達(dá)式提供 call()
泛型方法的實(shí)現(xiàn)。由于 call()
方法不需要任何參數(shù),因此 lambda 表達(dá)式中的括號(hào)為空;由于該方法為參數(shù)化類型 Callable<String>
返回 String
,所以 lambda 表達(dá)式必須返回 String
。
import java.util.concurrent.Callable;public class HelloCallable { public static void main(String[] args) { try { Callable<String> c = () -> "Hello from Callable"; System.out.println(c.call()); } catch (Exception e) { System.err.println(e.getMessage()); } }}
清單 5
HelloCallable
的輸出如圖 13 所示。
圖 13
PathMatcher
接口java.nio.file.PathMatcher
接口用于匹配路徑。該功能接口具有單一方法 matches(Path path)
,用于匹配 Path
。我們可以使用 lambda 表達(dá)式提供 matches()
方法的實(shí)現(xiàn),如清單 6 所示。Lambda 表達(dá)式返回 boolean
,目標(biāo)類型是功能接口 PathMatcher
。
import java.nio.file.PathMatcher;import java.nio.file.Path;import java.nio.file.FileSystems;public class FileMatcher { public static void main(String[] args) { PathMatcher matcher = (f) -> { boolean fileMatch = false; String path = f.toString(); if (path.endsWith("HelloCallable.java")) fileMatch = true; return fileMatch; }; Path filename = FileSystems.getDefault().getPath( "C:/JDK8/HelloCallable.java"); System.out.println("Path matches: " + matcher.matches(filename)); }}
清單 6
FileMatcher 類的輸出如圖 14 所示。
圖 14
Comparator
接口功能接口 Comparator
具有單一方法:compares()
。雖然該接口還有 equals()
方法,但 equals()
方法也存在于 Object
類中。除了另一個(gè)方法外,功能接口還可以有 Object
類方法。如果我們使用 Comparator
比較 Employee
實(shí)體的實(shí)例,首先需要定義 Employee
POJO,它具有 empId
、firstName
和 lastName
屬性和針對(duì)這些屬性的 getter/setter 方法,如清單 7 所示。
import java.util.*;public class Employee { private int empId; private String lastName; private String firstName; public Employee() { } public Employee(int empId, String lastName, String firstName) { this.empId = empId; this.firstName = firstName; this.lastName = lastName; } // setters and getters public int getEmpId() { return empId; } public void setEmpId(int empId) { this.empId = empId; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public int compareByLastName(Employee x, Employee y) { return x.getLastName().compareTo(y.getLastName()); } /** * * public static int compareByLastName(Employee x, Employee y) { return x.getLastName().compareTo(y.getLastName()); } */}
清單 7
如清單 8 所示,創(chuàng)建一個(gè)名為 EmployeeSort
的類,根據(jù) lastName
對(duì) Employee
實(shí)體的 List
進(jìn)行排序。在 EmployeeSort
類中,創(chuàng)建 List
對(duì)象并向其添加 Employee
對(duì)象。使用 Collections.sort
方法對(duì) List
進(jìn)行排序,并使用匿名內(nèi)部類為 sort()
方法創(chuàng)建 Comparator
對(duì)象。
package lambda;import java.util.*;public class EmployeeSort { public static void main(String[] args) { Employee e1 = new Employee(1, "Smith", "John"); Employee e2 = new Employee(2, "Bloggs", "Joe"); List<Employee> list = new ArrayList<Employee>(); list.add(e1); list.add(e2); Collections.sort(list, new Comparator<Employee>() { public int compare(Employee x, Employee y) { return x.getLastName().compareTo(y.getLastName()); } }); ListIterator<Employee> litr = list.listIterator(); while (litr.hasNext()) { Employee element = litr.next(); System.out.print(element.getLastName() + " "); } }}
清單 8
可以用 lambda 表達(dá)式替換匿名內(nèi)部類,該表達(dá)式有兩個(gè) Employee
類型參數(shù),根據(jù) lastName
比較返回 int
值,如清單 9 所示。
import java.util.*;public class EmployeeSort { public static void main(String[] args) { Employee e1 = new Employee(1, "Smith", "John"); Employee e2 = new Employee(2, "Bloggs", "Joe"); List<Employee> list = new ArrayList<Employee>(); list.add(e1); list.add(e2); Collections.sort(list, (x, y) -> x.getLastName().compareTo(y.getLastName())); ListIterator<Employee> litr = list.listIterator(); while (litr.hasNext()) { Employee element = litr.next(); System.out.print(element.getLastName() + " "); } }}
清單 9
EmployeeSort
的輸出如圖 15 所示。
圖 15
對(duì)于 lambda 表達(dá)式,從上下文推斷目標(biāo)類型。因此,lambda 表達(dá)式只能用在可以推斷目標(biāo)類型的上下文中。這類上下文包括:變量聲明、賦值語(yǔ)句、return
語(yǔ)句、數(shù)組初始值設(shè)定項(xiàng)、方法或構(gòu)造函數(shù)的參數(shù)、lambda 表達(dá)式主體、條件表達(dá)式和轉(zhuǎn)換表達(dá)式。
Lambda 的形式參數(shù)類型也從上下文推斷。除了清單 1 中的 Hello
示例,在前面的所有示例中,參數(shù)類型都是從上下文推斷出來(lái)的。在后續(xù)章節(jié)中,我們將討論可以使用 lambda 表達(dá)式的上下文。
return
語(yǔ)句中的 Lambda 表達(dá)式Lambda 表達(dá)式可以用在 return
語(yǔ)句中。return
語(yǔ)句中使用 lambda 表達(dá)式的方法的返回類型必須是一個(gè)功能接口。例如,返回 Runnable
的方法的 return
語(yǔ)句中包含一個(gè) lambda 表達(dá)式,如清單 10 所示。
import java.lang.Runnable;public class HelloRunnable2 { public static Runnable getRunnable() { return () -> { System.out.println("Hello from a thread"); }; } public static void main(String args[]) { new Thread(getRunnable()).start(); }}
清單 10
Lambda 表達(dá)式未聲明任何參數(shù),因?yàn)?Runnable
接口的 run()
方法沒(méi)有聲明任何形式參數(shù)。Lambda 表達(dá)式不返回值,因?yàn)?run()
方法的結(jié)果是 void
。清單 10 的輸出如圖 16 所示。
圖 16
Lambda 表達(dá)式本身可以用作內(nèi)部 lambda 表達(dá)式的目標(biāo)類型。Callable
中的 call()
方法返回一個(gè) Object
,但是 Runnable
中的 run()
方法沒(méi)有返回類型。在 HelloCallable2
類中,內(nèi)部 lambda 表達(dá)式的目標(biāo)類型是 Runnable
,外部 lambda 表達(dá)式的目標(biāo)類型是 Callable
。目標(biāo)類型是從上下文(對(duì) Callable<Runnable>
類型的引用變量進(jìn)行賦值)推斷出來(lái)的。
在清單 11 中,內(nèi)部 lambda 表達(dá)式 () -> {System.out.println("Hello from Callable");}
的類型被推斷為 Runnable
,因?yàn)閰?shù)列表為空,并且結(jié)果是 void
;匿名方法簽名和結(jié)果與 Runnable
接口中的 run()
方法相同。外部 lambda 表達(dá)式 () -> Runnable
的類型被推斷為 Callable<Runnable>
,因?yàn)?Callable<V>
中的 call()
方法沒(méi)有聲明任何形式參數(shù),并且結(jié)果類型是類型參數(shù) V
。
import java.util.concurrent.Callable;public class HelloCallable2 { public static void main(String[] args) { try { Callable<Runnable> c = () -> () -> { System.out.println("Hello from Callable"); }; c.call().run(); } catch (Exception e) { System.err.println(e.getMessage()); } }}
清單 11
HelloCallable2
的輸出如圖 17 所示。
圖 17
Lambda 表達(dá)式可以用在數(shù)組初始值設(shè)定項(xiàng)中,但不能使用泛型數(shù)組初始值設(shè)定項(xiàng)。例如,以下泛型數(shù)組初始值設(shè)定項(xiàng)中的 lambda 表達(dá)式將產(chǎn)生編譯器錯(cuò)誤:
Callable<String>[] c=new Callable<String>[]{ ()->"a", ()->"b", ()->"c" };
將產(chǎn)生編譯器錯(cuò)誤 Cannot create a generic array of Callable<String>
,如圖 18 所示。
圖 18
要在數(shù)組初始值設(shè)定項(xiàng)中使用 lambda 表達(dá)式,請(qǐng)指定一個(gè)非泛型數(shù)組初始值設(shè)定項(xiàng),如清單 12 所示的 CallableArray
類。
import java.util.concurrent.Callable;public class CallableArray {public static void main(String[] args) {try{Callable<String>[] c=new Callable[]{ ()->"Hello from Callable a", ()->"Hello from Callable b", ()->"Hello from Callable c" };System.out.println(c[1].call());}catch(Exception e){System.err.println(e.getMessage());}}}
CallableArray
中的每個(gè)數(shù)組初始值設(shè)定項(xiàng)變量都是一個(gè) Callable<String>
類型的 lambda 表達(dá)式。Lambda 表達(dá)式的參數(shù)列表為空,并且 lambda 表達(dá)式的結(jié)果是一個(gè) String
類型的表達(dá)式。每個(gè) lambda 表達(dá)式的目標(biāo)類型從上下文推斷為 Callable<String>
類型。CallableArray
的輸出如圖 19 所示。
圖 19
Lambda 表達(dá)式的目標(biāo)類型有時(shí)可能并不明確。例如,在下面的賦值語(yǔ)句中,lambda 表達(dá)式用作 AccessController.doPrivileged
方法的方法參數(shù)。Lambda 表達(dá)式的目標(biāo)類型不明確,因?yàn)槎鄠€(gè)功能接口(PrivilegedAction
和 PrivilegedExceptionAction
)都可以是 lambda 表達(dá)式的目標(biāo)類型。
String user = AccessController.doPrivileged(() -> System.getProperty("user.name"));
將產(chǎn)生編譯器錯(cuò)誤 The method doPrivileged(PrivilegedAction<String>) is ambiguous for the type AccessController
,如圖 20 所示。
圖 20
我們可以使用 lambda 表達(dá)式的轉(zhuǎn)換將目標(biāo)類型指定為 PrivilegedAction<String>
,如清單 13 所示的 UserPermissions
類。
import java.security.AccessController;import java.security.PrivilegedAction;public class UserPermissions { public static void main(String[] args) { String user = AccessController .doPrivileged((PrivilegedAction<String>) () -> System .getProperty("user.name")); System.out.println(user); }}
清單 13
Lambda 表達(dá)式中使用轉(zhuǎn)換后的 UserPermissions
輸出如圖 21 所示。
圖 21
Lambda 表達(dá)式可以用在三元條件表達(dá)式中,后者的值是這兩個(gè)操作數(shù)中的任何一個(gè),具體取決于 boolean
條件為 true
還是 false
。
在清單 14 所示的 HelloCallableConditional
類中,lambda 表達(dá)式 () -> "Hello from Callable:flag true")
和 () -> "Hello from Callable:flag false")
構(gòu)成了用于賦值的這兩個(gè)可供選擇的表達(dá)式。Lambda 表達(dá)式的目標(biāo)類型是從上下文(對(duì) Callable<String>
引用變量進(jìn)行賦值)推斷出來(lái)的。隨后,使用該引用變量調(diào)用 call()
方法。
import java.util.concurrent.Callablepublic class HelloCallableConditional { public static void main(String[] args) { try { boolean flag = true; Callable<String> c = flag ? (() -> "Hello from Callable: flag true") : (() -> "Hello from Callable: flag false"); System.out.println(c.call()); } catch (Exception e) { System.err.println(e.getMessage()); } }}
清單 14
HelloCallableConditional
的輸出如圖 22 所示。
圖 22
調(diào)用重載方法時(shí),將使用與 lambda 表達(dá)式最匹配的方法。我們將使用目標(biāo)類型和方法參數(shù)類型選擇最佳方法。
在清單 15 的 HelloRunnableOrCallable
類中,指定了兩個(gè)返回類型為 String
的 hello()
方法(hello()
方法被重載):它們的參數(shù)類型分別是 Callable
和 Runnable
。
將調(diào)用 hello()
方法,其中 lambda 表達(dá)式作為方法參數(shù)。由于 lambda 表達(dá)式 () -> "Hello Lambda"
返回 String
,因此會(huì)調(diào)用 hello(Callable)
方法并輸出 Hello from Callable
,因?yàn)?Callable
的 call()
方法具有返回類型,而 Runnable
的 run()
方法沒(méi)有。
import java.util.concurrent.Callable;public class HelloRunnableOrCallable { static String hello(Runnable r) { return "Hello from Runnable"; } static String hello(Callable c) { return "Hello from Callable"; } public static void main(String[] args) { String hello = hello(() -> "Hello Lambda"); System.out.println(hello); }}
清單 15
HelloCallableConditional
的輸出如圖 23 所示。
圖 23
this
在 lambda 表達(dá)式外面,this
引用當(dāng)前對(duì)象。在 lambda 表達(dá)式里面,this
引用封閉的當(dāng)前對(duì)象。不引用封閉實(shí)例成員的 lambda 表達(dá)式不會(huì)存儲(chǔ)對(duì)該成員的強(qiáng)引用,這解決了內(nèi)部類實(shí)例保留對(duì)封閉類的強(qiáng)引用時(shí)經(jīng)常出現(xiàn)的內(nèi)存泄漏問(wèn)題。
在清單 16 所示的示例中,Runnable
是 lambda 表達(dá)式的目標(biāo)類型。在 lambda 表達(dá)式主體中,指定了對(duì) this
的引用。創(chuàng)建 Runnable r
實(shí)例并調(diào)用 run()
方法后,this
引用將調(diào)用封閉實(shí)例,并從 toString()
方法獲得封閉實(shí)例的 String
值。將輸出 Hello from Class HelloLambda
消息。
public class HelloLambda { Runnable r = () -> { System.out.println(this); }; public String toString() { return "Hello from Class HelloLambda"; } public static void main(String args[]) { new HelloLambda().r.run(); } }
清單 16
HelloLambda
的輸出如圖 24 所示。
圖 24
為 lambda 表達(dá)式的形式參數(shù)創(chuàng)建新名稱。如果用作 lambda 表達(dá)式參數(shù)名稱的名稱與封閉上下文中局部變量名稱相同,將產(chǎn)生編譯器錯(cuò)誤。在以下示例中,lambda 表達(dá)式的參數(shù)名稱被指定為 e1
和 e2
,它們同時(shí)還用于局部變量 Employee e1
和 Employee e2
。
Employee e1 = new Employee(1,"A", "C"); Employee e2 = new Employee(2,"B","D" ); List<Employee> list = new ArrayList<Employee>(); list.add(e1);list.add(e2); Collections.sort(list, (Employee e1, Employee e2) -> e1.getLastName().compareTo(e2.getLastName()));
Lambda 表達(dá)式參數(shù) e1
將導(dǎo)致編譯器錯(cuò)誤,如圖 25 所示。
圖 25
Lambda 表達(dá)式參數(shù) e2
也將導(dǎo)致編譯器錯(cuò)誤,如圖 26 所示。
圖 26
在提供匿名內(nèi)部類的替代方案時(shí),局部變量必須處于終態(tài)才能在 lambda 表達(dá)式中訪問(wèn)的要求被取消。JDK 8 也取消了局部變量必須處于終態(tài)才能從內(nèi)部類訪問(wèn)的要求。在 JDK 7 中,必須將從內(nèi)部類訪問(wèn)的局部變量聲明為終態(tài)。
在內(nèi)部類或 lambda 表達(dá)式中使用局部變量的要求已從“終態(tài)”修改為“終態(tài)或等效終態(tài)”。
Lambda 表達(dá)式定義了一個(gè)匿名方法,其中功能接口作為目標(biāo)類型。可以使用方法引用來(lái)調(diào)用具有名稱的現(xiàn)有方法,而不是定義匿名方法。在清單 9 所示的 EmployeeSort
示例中,以下方法調(diào)用將 lambda 表達(dá)式作為方法參數(shù)。
Collections.sort(list, (x, y) -> x.getLastName().compareTo(y.getLastName()));
可以按以下方式將 lambda 表達(dá)式替換為方法引用:
Collections.sort(list, Employee::compareByLastName);
分隔符 (::
) 可用于方法引用。compareByLastName
方法是 Employee
類中的靜態(tài)方法。
public static int compareByLastName(Employee x, Employee y) { return x.getLastName().compareTo(y.getLastName());
對(duì)于非靜態(tài)方法,方法引用可與特定對(duì)象的實(shí)例一起使用。通過(guò)將 compareByLastName
方法非靜態(tài)化,可按如下方式將方法引用與與 Employee
實(shí)例結(jié)合使用:
Employee employee=new Employee();Collections.sort(list, employee::compareByLastName);
方法引用甚至不必是所屬對(duì)象的實(shí)例方法。方法引用可以是任何任意類對(duì)象的實(shí)例方法。例如,通過(guò)方法引用,可以使用 String
類的 compareTo
方法對(duì) String List
進(jìn)行排序。
String e1 = new String("A"); String e2 = new String("B"); List<String> list = new ArrayList<String>(); list.add(e1);list.add(e2);Collections.sort(list, String::compareTo);
方法引用是 lambda 表達(dá)式的進(jìn)一步簡(jiǎn)化。
方法引用的作用是方法調(diào)用,而構(gòu)造函數(shù)引用的作用是構(gòu)造函數(shù)調(diào)用。方法引用和構(gòu)造函數(shù)引用是 lambda 轉(zhuǎn)換,方法引用和構(gòu)造函數(shù)引用的目標(biāo)類型必須是功能接口。
在本節(jié)中,我們將以 Multimap
為例討論構(gòu)造函數(shù)引用。Multimap 是一個(gè) Google Collections 實(shí)用程序。圖像類型的 Multimap
按如下方式創(chuàng)建:
Multimap<ImageTypeEnum, String> imageTypeMultiMap = Multimaps .newListMultimap( Maps.<ImageTypeEnum, Collection<String>> newHashMap(), new Supplier<List<String>>() { public List<String> get() { return new ArrayList<String>(); } });
在 Multimap
示例中,使用構(gòu)造函數(shù)按如下方式創(chuàng)建 Supplier<List<String>>
:
new Supplier<List<String>>() { public List<String> get() { return new ArrayList<String>(); } }
構(gòu)造函數(shù)返回 ArrayList<String>
。通過(guò)構(gòu)造函數(shù)引用,可以使用簡(jiǎn)化的語(yǔ)法按如下方式創(chuàng)建 Multimap
:
Multimap<ImageTypeEnum, String> imageTypeMultiMap = Multimaps.newListMultimap(Maps.<ImageTypeEnum, Collection<String>> newHashMap(),ArrayList<String>::new);
清單 17 顯示了使用構(gòu)造函數(shù)引用的 Multimap
示例,即 ImageTypeMultiMap
類。
import java.util.ArrayList;import java.util.Collection;import java.util.List;import com.google.common.base.Supplier;import com.google.common.collect.Maps;import com.google.common.collect.Multimap;import com.google.common.collect.Multimaps;public class ImageTypeMultiMap { enum ImageTypeEnum { tiff, tif, gif, jpeg, jpg, png, bmp } public static void main(String[] args) { Multimap<ImageTypeEnum, String> imageTypeMultiMap = Multimaps .newListMultimap( Maps.<ImageTypeEnum, Collection<String>> newHashMap(), ArrayList<String>::new); imageTypeMultiMap.put(ImageTypeEnum.tiff, "tiff"); imageTypeMultiMap.put(ImageTypeEnum.tif, "tif"); imageTypeMultiMap.put(ImageTypeEnum.gif, "gif"); imageTypeMultiMap.put(ImageTypeEnum.jpeg, "jpeg"); imageTypeMultiMap.put(ImageTypeEnum.jpg, "jpg"); imageTypeMultiMap.put(ImageTypeEnum.png, "png"); imageTypeMultiMap.put(ImageTypeEnum.bmp, "bmp"); System.out.println("Result: " + imageTypeMultiMap); }}
清單 17
要測(cè)試 ImageTypeMultiMap
,我們需要從 https://code.google.com/p/guava-libraries/ 下載 Guava 庫(kù) guava-14.0.1.jar
,并將 guava-14.0.1.jar
添加到 Java 構(gòu)建路徑。ImageTypeMultiMap
的輸出如圖 27 所示。
圖 27
接口的封裝和可重用性是接口的主要優(yōu)點(diǎn)。但接口的缺點(diǎn)是實(shí)現(xiàn)接口的類必須實(shí)現(xiàn)所有接口方法。有時(shí)只需要接口的部分方法,但在實(shí)現(xiàn)接口時(shí)必須提供所有接口方法的方法實(shí)現(xiàn)。虛擬擴(kuò)展方法解決了這個(gè)問(wèn)題。
虛擬擴(kuò)展方法是接口中具有默認(rèn)實(shí)現(xiàn)的方法。如果實(shí)現(xiàn)類不提供方法的實(shí)現(xiàn),則使用默認(rèn)的實(shí)現(xiàn)。實(shí)現(xiàn)類可以重寫默認(rèn)實(shí)現(xiàn),或提供新的默認(rèn)實(shí)現(xiàn)。
虛擬擴(kuò)展方法添加配置來(lái)擴(kuò)展接口的功能,而不會(huì)破壞已實(shí)現(xiàn)接口較早版本的類的向后兼容性。虛擬擴(kuò)展方法中的默認(rèn)實(shí)現(xiàn)是用 default
關(guān)鍵字提供的。由于虛擬擴(kuò)展方法提供默認(rèn)實(shí)現(xiàn),因此不能是抽象方法。
JDK 8 中的 java.util.Map<K,V>
類提供了幾個(gè)具有默認(rèn)實(shí)現(xiàn)的方法:
default V getOrDefault(Object key,V defaultValue)
default void forEach(BiConsumer<? super K,? super V> action)
default void replaceAll(BiFunction<? super K,? super V,? extends V> function)
default V putIfAbsent(K key,V value)
default boolean remove(Object key,Object value)
default boolean replace(K key,V oldValue,V newValue)
default V replace(K key,V value)
default V computeIfAbsent(K key,Function<? super K,? extends V> mappingFunction)
default V computeIfPresent(K key,BiFunction<? super K,? super V,? extends V> remappingFunction)
default V compute(K key,BiFunction<? super K,? super V,? extends V> remappingFunction)
default V merge(K key,V value,BiFunction<? super V,? super V,? extends V> remappingFunction)
要證明類在實(shí)現(xiàn)接口時(shí)無(wú)需實(shí)現(xiàn)具有默認(rèn)實(shí)現(xiàn)的方法,請(qǐng)創(chuàng)建實(shí)現(xiàn) Map<K,V>
接口的 MapImpl
類:
public class MapImpl<K,V> implements Map<K, V> {
清單 18 顯示了完整的 MapImpl
類,實(shí)現(xiàn)了不提供默認(rèn)實(shí)現(xiàn)的方法。
import java.util.Collection;import java.util.Map;import java.util.Set;public class MapImpl<K,V> implements Map<K, V> { public static void main(String[] args) { } @Override public int size() { return 0; } @Override public boolean isEmpty() { return false; } @Override public boolean containsKey(Object key) { return false; } @Override public boolean containsValue(Object value) { return false; } @Override public V get(Object key) { return null; } @Override public V put(K key, V value) { return null; } @Override public V remove(Object key) { return null; } @Override public void putAll(Map<? extends K, ? extends V> m) { } @Override public void clear() { } @Override public Set<K> keySet() { return null; } @Override public Collection<V> values() { return null; } @Override public Set<java.util.Map.Entry<K, V>> entrySet() { return null; }}
清單 18
雖然 Map<K,V>
接口是一個(gè)預(yù)定義的接口,但也可以使用虛擬擴(kuò)展方法定義一個(gè)新接口。使用 default
關(guān)鍵字創(chuàng)建為所有方法提供默認(rèn)實(shí)現(xiàn)的 EmployeeDefault
接口,如清單 19 所示。
public interface EmployeeDefault { String name = "John Smith"; String title = "PHP Developer"; String dept = "PHP"; default void setName(String name) { System.out.println(name); } default String getName() { return name; } default void setTitle(String title) { System.out.println(title); } default String getTitle() { return title; } default void setDept(String dept) { System.out.println(dept); } default String getDept() { return dept; }}
清單 19
如果使用 default
關(guān)鍵字聲明接口方法,則該方法必須按編譯器錯(cuò)誤指出的方式提供實(shí)現(xiàn),如圖 28 所示。
圖 28
默認(rèn)情況下,接口的字段處于終態(tài),不能在默認(rèn)方法的默認(rèn)實(shí)現(xiàn)中賦值,如圖 29 中的編譯器錯(cuò)誤所示。
圖 29
沒(méi)有實(shí)現(xiàn) EmployeeDefault
接口的類,也可以提供虛擬擴(kuò)展方法的實(shí)現(xiàn)。EmployeeDefaultImpl
類實(shí)現(xiàn)了 EmployeeDefault
接口,沒(méi)有為從 EmployeeDefault
繼承的任何虛擬擴(kuò)展方法提供實(shí)現(xiàn)。EmployeeDefaultImpl
類使用方法調(diào)用表達(dá)式調(diào)用虛擬擴(kuò)展方法,如清單 20 所示。
public class EmployeeDefaultImpl implements EmployeeDefault { public static void main(String[] args) { EmployeeDefaultImpl employeeDefaultImpl=new EmployeeDefaultImpl(); System.out.println(employeeDefaultImpl.getName()); System.out.println(employeeDefaultImpl.getTitle()); System.out.println(employeeDefaultImpl.getDept()); }}
清單 20
本文介紹了 JDK 8 的新特性 — lambda 表達(dá)式,其語(yǔ)法簡(jiǎn)潔,是匿名類的簡(jiǎn)短形式。此外,還介紹了虛擬擴(kuò)展方法,它非常有用,因?yàn)樗峁┝司哂心J(rèn)方法實(shí)現(xiàn)的接口;如果實(shí)現(xiàn)類不提供方法的實(shí)現(xiàn),則會(huì)使用該默認(rèn)的方法實(shí)現(xiàn)。
Deepak Vohra 是一名 NuBean 顧問(wèn)、Web 開(kāi)發(fā)人員、Sun 認(rèn)證的 Java 1.4 程序員和 Sun 認(rèn)證的 Java EE Web 組件開(kāi)發(fā)人員。
請(qǐng)?jiān)?Facebook、Twitter 和 Oracle Java 博客上加入 Java 社區(qū)對(duì)話!
聯(lián)系客服