lambda 表達式的類型是什麼?一些語言使用函數值或函數對象來表示 lambda 表達式,但 Java™ 語言沒有這麼作。Java 使用函數接口來表示 lambda 表達式類型。乍一看彷佛有點奇怪,但事實上這是一種確保對 Java 語言舊版本的向後兼容性的有效途徑。html
您應該很是熟悉下面這段代碼:java
Thread thread = new Thread(new Runnable() { public void run() { System.out.println("In another thread"); }}); thread.start(); System.out.println("In main");
Thread
類和它的構造函數是在 Java 1.0 中引入的,距今已有超過 20 年的時間。從那時起,構造函數從未改變過。將 Runnable
的匿名實例傳遞給構造函數已成爲一種傳統。可是從 Java 8 開始,能夠選擇傳遞 lambda 表達式:程序員
Thread thread = new Thread(() -> System.out.println("In another thread"));
關於本系列Java 8 是自 Java 語言誕生以來進行的一次最重大更新—包含了很是豐富的新功能,您可能想知道從何處開始着手瞭解它。在本系列中,做家兼教師 Venkat Subramaniam 提供了一種慣用的 Java 8 編程方法:這些簡短的探索會激發您反思您認爲理所固然的 Java 約定,同時逐步將新技術和語法集成到您的程序中。
Thread
類的構造函數想要一個實現 Runnable
的實例。在本例中,咱們傳遞了一個 lambda 表達式,而不是傳遞一個對象。咱們能夠選擇向各類各樣的方法和構造函數傳遞 lambda 表達式,包括在 Java 8 以前建立的一些方法和構造函數。這頗有效,由於 lambda 表達式在 Java 中表示爲函數接口。編程
函數接口有 3 條重要法則:安全
Object
類中屬於公共方法的抽象方法不會被視爲單一抽象方法。任何知足單一抽象方法法則的接口,都會被自動視爲函數接口。這包括 Runnable
和 Callable
等傳統接口,以及您本身構建的自定義接口。app
除了已經提到的單一抽象方法以外,JDK 8 還包含多個新函數接口。最經常使用的接口包括 Function<T, R>
、Predicate<T>
和Consumer<T>
,它們是在 java.util.function
包中定義的。Stream
的 map
方法接受 Function<T, R>
做爲參數。相似地,filter
使用 Predicate<T>
,forEach
使用 Consumer<T>
。該包還有其餘函數接口,好比 Supplier<T>
、BiConsumer<T, U>
和 BiFunction<T, U, R>
。ide
能夠將內置函數接口用做咱們本身的方法的參數。例如,假設咱們有一個 Device
類,它包含方法 checkout
和 checkin
來指示是否正在使用某個設備。當用戶請求一個新設備時,方法 getFromAvailable
從可用設備池中返回一個設備,或在必要時建立一個新設備。函數
咱們能夠實現一個函數來借用設備,就象這樣:學習
public void borrowDevice(Consumer<Device> use) { Device device = getFromAvailable(); device.checkout(); try { use.accept(device); } finally { device.checkin(); }}
borrowDevice
方法:測試
Consumer<Device>
做爲參數。checkout
方法將設備狀態設置爲 checked out。在完成設備調用後返回到 Consumer
的 accept
方法時,經過調用 checkin
方法將設備狀態更改成 checked in。
下面給出了一種使用 borrowDevice
方法的方式:
new Sample().borrowDevice(device -> System.out.println("using " + device));
由於該方法接收一個函數接口做爲參數,因此傳入一個 lambda 表達式做爲參數是能夠接受的。
儘管最好儘可能使用內置函數接口,但有時須要自定義函數接口。
要建立本身的函數接口,須要作兩件事:
@FunctionalInterface
註釋該接口,這是 Java 8 對自定義函數接口的約定。該約定清楚地代表該接口應接收 lambda 表達式。當編譯器看到該註釋時,它會驗證該接口是否只有一個抽象方法。
使用 @FunctionalInterface
註釋能夠確保,若是在將來更改該接口時意外違反抽象方法數量規則,您會得到錯誤消息。這頗有用,由於您會當即發現問題,而不是留給另外一位開發人員在之後處理它。沒有人但願在將 lambda 表達式傳遞給其餘人的自定義接口時得到錯誤消息。
做爲一個示例,咱們將建立一個 Order
類,它有一系列 OrderItem
以及一個轉換並輸出它們的方法。咱們首先建立一個接口。
下面的代碼將建立一個 Transformer
函數接口。
@FunctionalInterfacepublic interface Transformer<T> { T transform(T input);}
該接口用 @FunctionalInterface
註釋作了標記,代表它是一個函數接口。由於該註釋包含在 java.lang
包中,因此沒有必要導入。該接口有一個名爲 transform
的方法,後者接受一個參數化爲 T
類型的對象,並返回一個相同類型的轉換後對象。轉換的語義將由該接口的實現來決定。
這是 OrderItem
類:
public class OrderItem { private final int id; private final int price; public OrderItem(int theId, int thePrice) { id = theId; price = thePrice; } public int getId() { return id; } public int getPrice() { return price; } public String toString() { return String.format("id: %d price: %d", id, price); }}
OrderItem
是一個簡單的類,它有兩個屬性:id
和 price
,以及一個 toString
方法。
如今來看看 Order
類。
import java.util.*;import java.util.stream.Stream; public class Order { List<OrderItem> items; public Order(List<OrderItem> orderItems) { items = orderItems; } public void transformAndPrint( Transformer<Stream<OrderItem>> transformOrderItems) { transformOrderItems.transform(items.stream()) .forEach(System.out::println); }}
transformAndPrint
方法接受 Transform<Stream<OrderItem>
做爲參數,調用 transform
方法來轉換屬於 Order
實例的訂單項,而後按轉換後的順序輸出這些訂單項。
這是一個使用該方法的樣本:
import java.util.*;import static java.util.Comparator.comparing;import java.util.stream.Stream;import java.util.function.*; class Sample { public static void main(String[] args) { Order order = new Order(Arrays.asList( new OrderItem(1, 1225), new OrderItem(2, 983), new OrderItem(3, 1554) )); order.transformAndPrint(new Transformer<Stream<OrderItem>>() { public Stream<OrderItem> transform(Stream<OrderItem> orderItems) { return orderItems.sorted(comparing(OrderItem::getPrice)); } }); }}
咱們傳遞一個匿名內部類做爲 transformAndPrint
方法的參數。在 transform
方法內,調用給定流的 sorted
方法,這會對訂單項進行排序。這是咱們的代碼的輸出,其中顯示了按價格升序排列的訂單項:
id: 2 price: 983id: 1 price: 1225id: 3 price: 1554
在任何須要函數接口的地方,咱們都有 3 種選擇:
傳遞匿名內部類的過程很複雜,咱們只能傳遞方法引用來替代直通 lambda 表達式。考慮若是咱們重寫對 transformAndPrint
函數的調用,以使用 lambda 表達式來代替匿名內部類,將會發生什麼:
order.transformAndPrint(orderItems -> orderItems.sorted(comparing(OrderItem::getPrice)));
與咱們最初提供的匿名內部類相比,這簡潔得多且更容易閱讀。
咱們的自定義函數接口演示了建立自定義接口的優點和不足。首先考慮優點:
Transformer
、Validator
和ApplicationEvaluator
這樣的名稱是特定於領域的,能夠幫助讀取接口方法的人推斷對參數的預期是什麼。Transformer
接口來使用 OrderItems
而不是參數化類型 T
。固然,使用自定義函數接口也存在不足之處:
String
做爲參數並返回 Integer
。儘管方法的名稱可能有所不一樣,但它們大部分都是多餘的,可替換爲一個具備通用名稱的接口。java.lang
包中的 Runnable
。咱們一次又一次地看到它,因此能夠輕鬆地記住它的用途。可是,若是我使用了一個自定義 Executor
,您在使用該接口以前必須仔細瞭解它。在某些狀況下,投入一些精力是值得的,可是若是 Executor
與 Runnable
很是類似,就會浪費精力。瞭解自定義函數接口與內置函數接口的優缺點後,如何肯定採用哪一種接口?咱們回顧一下 Transformer
接口來尋找答案。
回想一下,Transformer
的存在是爲了傳達將一個對象轉換爲另外一個對象的語義。這裏,咱們按名稱來引用它:
public void transformAndPrint(Transformer<Stream<OrderItem>> transformOrderItems) {
方法 transformAndPrint
接收一個負責執行轉換的參數。該轉換可能對 OrderItems
集合中的元素進行從新排序。或者,它可能屏蔽每一個訂單項的部分細節。或者該轉換能夠決定什麼都不作,僅返回原始集合。將實現工做留給調用方。
重要的是,調用方知道它們能夠將轉換實現做爲參數提供給 transformAndPrint
方法。函數接口的名稱和它的文檔應該提供這些細節。在本例中,從參數名稱 (transformOrderItems
) 也能夠清楚瞭解這些細節,並且它們應包含在 transformAndPrint
函數的文檔中。儘管函數接口的名稱頗有用,但它不是瞭解函數接口用途和用法的惟一途徑。
仔細查看 Transformer
接口,並將它的用途與 JDK 的內置函數接口進行比較,咱們看到 Function<T, R>
能夠取代 Transformer
。要測試 Transformer
函數接口,能夠從代碼中刪除它並更改 transformAndPrint
函數,就像這樣:
public void transformAndPrint(Function<Stream<OrderItem>, Stream<OrderItem>> transformOrderItems) { transformOrderItems.apply(items.stream()) .forEach(System.out::println);}
改動很小 —除了將 Transformer<Stream<OrderItem>>
更改成 Function<Stream<OrderItem>>
、Stream<OrderItem>>
,咱們還將方法調用從 transform()
更改成 apply()
。
對 transformAndPrint
的調用使用了一個匿名內部類,咱們還須要更改這一點。可是,咱們已更改該調用來使用 lambda 表達式:
order.transformAndPrint(orderItems -> orderItems.sorted(comparing(OrderItem::getPrice)));
函數接口的名稱與 lambda 表達式無關—它僅與編譯器相關,編譯器將 lambda 表達式參數與方法參數聯繫起來。方法的名稱是transform
仍是 apply
,一樣與調用方無關。
使用內置的函數接口讓咱們的接口減小了一個,調用該方法也具備一樣功效。咱們也沒有損害代碼的可讀性。這個練習告訴咱們,咱們能夠輕鬆地將自定義函數接口替換爲內置接口。咱們只需提供 transformAndPrint
的文檔(未顯示)並採用含義更明確的方式命名該參數。
將 lambda 表達式設置爲函數接口類型的設計決策,有助於在 Java 8 與早期 Java 版本之間實現向後兼容性。能夠將 lambda 表達式傳遞給任何一般接收單一抽象方法接口的舊函數。要接收 lambda 表達式,方法的參數類型應爲函數接口。
在某些狀況下,建立本身的函數接口是合情合理的,但在這麼作時應該當心謹慎。僅在應用程序須要高度專業化的方法時,或者現有接口沒法知足您的需求時,才考慮自定義函數接口。請始終檢查一個 JDK 的內置函數接口中是否存在該功能。儘可能使用內置函數接口。
原做者:Venkat Subramaniam
原文連接: Java 8 習慣用語
原出處: IBM Developer