Lambda表達式

爲什麼使用Lambda表達式

在Java中引進Lambda表達式的動機和行爲參數化模式有關,這個模式可以讓你在需求變化時寫出更加靈活的代碼。
在Java 8 之前這個模式非常的繁瑣,Lambda表達式可以讓你非常簡潔地使用行爲參數化模式。
有這樣一個事例:你需要查找大於特定值數量的費用清單。你可能會創建一個方法findInvoicesGreaterThanAmount

List<Invoice> findInvoicesGreaterThanAmount(List<Invoice> invoices, double amount) {
	List<Invoice> result = new ArrayList<>();
	for(Invoice inv: invoices) {
		if(inv.getAmount() > amount) {
			result.add(inv);
		}
	}
	return result;
}

這個方法很好的實現我們想要的功能。
但是,如果還需要查找小於特定值數量的費用清單呢?
或者,需要查找的費用清單是特定客戶的特定數量的費用清單呢?
或者,需要查詢費用清單的其他屬性呢?
這個時候我們需要參數化過濾條件,我們可能會定義一個InvoicePredicate來代表這些條件來實現過濾。
看一下重構後的代碼:

interface InvoicePredicate {
	boolean test(invoice inv);
}
List<Invoice> findInvoices(List<Invoice> invoices, InvoicePredicate p) {
	List<Invoice> result = new ArrayList<>();
	for(Invoice inv: invoices) {
		if(p.test(inv)) {
			result.add(inv);
		}
	}
	return result;
}

有了這段代碼,我們可以很好地應對來自於Invoice對象的任何屬性的需求變化。
現在只要根據需求創建不同的InvoicePredicate對象來傳遞給findInvoices方法就可以了。
換句話說,現在可以參數化findInvoices方法的行爲。然而新方法的使用導致代碼更加繁瑣囉嗦。

List<Invoice> expensiveInvoicesFromOracle
	= findInvoices(invoices, new InvoicePredicate() {
		public test(Invoice inv) {
			return inv.getAmount() > 10_000 
					&& inv.getCustomer() == Customer.ORACLE;
		}
	});

現在情況是靈活性有了,但是易讀性太差。理想情況下,靈活性和簡潔性我們都想要。那麼,這時候Lambda表達式就派上用場了。我們利用這一特性來重構一下代碼:

List<Invoice> expensiveInvoicesFromOracle
	= findInvoices(invoices, inv ->
								inv.getAmount() > 10_000
								&& inv.getCustomer() == Customer.ORACLE);

Lambda表達式定位

現在你已經知道爲什麼需要Lambda表達式了,那麼我們再來詳細瞭解一下它。簡單來說,Lambda表達式就是一個匿名函數且當做參數傳遞。看一下詳細說明:

  • 匿名(Anonymous)
    一個Lambda表達式是匿名的,因爲它不需要想正常方法那樣有一個名字。類似匿名類一樣沒有聲明名字。
  • 函數(Function)
    一個Lambda表達式和普通方法一樣,有參數,方法體,返回值和可能拋出的異常。當然不像普通方法那樣在類中預先聲明。
  • 傳參(Passed around)
    一個Lambda表達式可以作爲一個參數傳遞給方法,存儲在一個變量中,也可以作爲結果返回。

Lambda表達式語法

在我們使用Lambda表達式之前,先來認識一下它的語法。看一下下面的兩個Lambda表達式:

Runnable r = () -> System.out.println("Hi");
FileFilter isXml = (File f) -> f.getName().endsWith(".xml");

這兩個Lambda表達式都是由三部分組成。
一組參數,例如(File f)
一個由兩個字符(-和>)組成的箭頭。
一個方法體,例如f.getName().endsWith(".xml")
有兩種Lambda表達式的形式。如果是單行語句表達式你可以使用第一種方式:

(parameters) -> expression

如果是單行或者多行語句可以使用第二種方式,這種方式需要使用大括號包起來:

(parameters) -> { statements;}

通常情況下,如果參數類型可以推斷出來那麼參數類型可以省略,如果只有一個參數,圓括號也可以省略掉。

list.sort((o1, o2) -> -o1.compareTo(o2));
list.removeIf(s -> false);

在哪裏使用Lambda表達式

現在知道了如何編寫Lambda表達式了,那麼再看一下怎樣和何地使用Lambda表達式。
簡單地說,你可以在函數式接口上下文中使用Lambda表達式。
函數式接口就是隻有一個抽象方法的接口。
編者按:接口不能存在多個抽象方法,但是可以存在默認方法和靜態方法。
看一下之前的兩個Lambda表達式:

Runnable r = () -> System.out.println("Hi");
FileFilter isXml = (File f) -> f.getName().endsWith(".xml");

Runnable是一個函數式接口,它定義了一個單一的抽象方法run
FileFilter也是一個函數式接口,它定義了一個單一的抽象方法accept

@FunctionalInterface
public interface Runnable {
	void run();
}

@FunctionalInterface
public interface FileFilter {
	boolean accept(File pathname);
}

很重要的一點就是Lambda表達式讓我們創建了一個函數式接口實例。Lambda表達式方法體提供了函數式接口中的唯一方法的實現。
下文代碼關於Runnable的內部類和Lambda表達式產生同樣的輸出結果。

Runnable r1 = new Runnable() {
	public void run() {
		System.out.println("Hi!");
	}
};
r1.run();

Runnable r2 = () -> System.out.println("Hi!");
r2.run();

注意:
你經常會在接口上看到註解@FunctionalInterface,它和註解@Override的用途類似,註解@Override用於標識一個方法被重寫。
而註解@FunctionalInterface主要用於在代碼文檔化告知這個接口是一個函數式接口。
如果一個帶有註解@FunctionalInterface的接口不能符合函數式接口的定義,那麼編譯器會報錯。
我們在包java.util.function中可以看到,新增了函數式接口例如Function<T, R>Supplier<T>,可以利用這些接口嘗試Lambda表達式的各種格式。

方法引用

方法引用可以讓我們重新使用已經定義好的方法並且像lambda表達式那樣作爲參數傳遞。
在特定的場景下使用方法引用相較於使用Lambda表達式更加自然和易讀。
例如,使用lambda表達式查找隱藏文件:

File[] hiddenFiles = mainDirectory.listFiles(f -> f.isHidden());

使用方法引用可以直接通過使用兩個英文冒號(::)的語法來調用方法:

File[] hiddenFiles = mainDirectory.listFiles(File::isHidden);

可以把方法引用當做lambda表達式調用特定方法的縮寫。這兒有四種方法引用的方式:

  • 靜態方法引用
Function<String, Integer> converter = Integer::parseInt;
Integer number = converter.apply("10");
  • 實例方法引用。確切地說是引用一個被提供給lambda表達式第一個參數的對象方法。
Function<Invoice, Integer> invoiceToId = Invoice::getId;
  • 實例對象的方法引用。具體點說,這種方法引用在你想引用私有的幫助方法且將它注入到其他方法是非常有效。
Consumer<Object> print = System.out::println;
  • 構造引用
Supplier<List<String>> listOfString = List::new;

一起組合起來

在開頭我們看到一段用於排序費用清單的繁瑣代碼:

Collections.sort(invoices, new Comparator<Invoice>() {
	public int compare(Invoice inv1, Invoice inv2) {
		return Double.compare(inv2.getAmount(), 
			inv1.getAmount());
	}
});

現在我們已經知道怎樣利用java 8 的新特性,讓我們重構一下讓它更加簡潔易讀。
首先,看到Comparator是一個函數式接口。它僅僅聲明瞭一個抽象方法compare,這個方法有兩個同類型的參數且返回一個整形值。
這是一個理想的lambda表達式應用場景:

Collections.sort(invoices,
				(Invoice inv1, Invoice inv2) -> {
					return Double.compare(inv2.getAmount(), 
						inv1.getAmount());
});

既然lambda表達式方法體僅僅返回一個表達式的值,我們可以讓代碼更加簡潔一點。

Collections.sort(invoices,
				(Invoice inv1, Invoice inv2)
					-> Double.compare(inv2.getAmount(),
						inv1.getAmount())
);

在Java 8 中,List接口本身已經支持sort方法,如此我們可以使用它來代替Collections.sort

invoices.sort((Invoice inv1, Invoice inv2)
	 -> Double.compare(inv2.getAmount(), inv1.getAmount()));

其次,Java 8 提供了一個靜態工具方法Comparator.comparing
它利用lambda表達式作爲參數得到比較值,然後生成一個Comparator對象。
可以這用使用:

Comparator<Invoice> byAmount 
	= Comparator.comparing((Invoice inv) -> inv.getAmount());
invoices.sort(byAmount);

你可能注意到了更加簡潔的方法引用Invoice::getAmount可以簡單地替換lambda表達式(Invoice inv) -> inv.getAmount()

Comparator<Invoice> byAmount
	= Comparator.comparing(Invoice::getAmount);
invoices.sort(byAmount);

因爲方法getAmount返回基本類型double,你還可以使用方法Comparator.comparingDouble,這是方法Comparator.comparing的基本類型特殊版本,可以有效避免沒有必要的裝箱操作。

Comparator<Invoice> byAmount
	= Comparator.comparingDouble(Invoice::getAmount);
invoices.sort(byAmount);

最後,我們再來整理一下代碼,我們使用一個靜態引入這樣可以去掉一個存儲Comparator對象的局部變量。

import static java.util.Comparator.comparingDouble;
invoices.sort(comparingDouble(Invoice::getAmount));

測試Lambda表達式

你可能關心lambda表達式有多麼的影響測試。畢竟lambda表達式的行爲也是需要測試的。何時決定怎樣去測試包含lambda表達式的代碼可以根據下列兩種情況判斷:

  1. 如果lambda表達式非常簡單,那麼直接測試使用lambda表達式的代碼。
  2. 如果lambda表達式相對複雜,那麼把它抽取出來作爲一個單獨的方法引用,這樣你可以直接使用和測試。

總結

  • Lambda表達式可以理解爲一個匿名的函數。
  • Lambda表達式和行爲參數化模式讓你的代碼更加靈活和簡潔。
    函數式接口只有一個抽象方法。
  • Lambda表達式只能用在函數式接口的上下文中。
  • 在利用現成的方法和通過傳參情況下,方法引用作爲lambda表達式的更好替代。
  • 在測試時,可以抽取複雜lambda表達式成多個方法。這樣可以直接注入使用方法引用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章